+ * This method respects quoted arguments. Meaning that whitespaces appearing phrases that are enclosed by an opening
+ * single or double quote and a closing single or double quote or the end of the string will not be considered.
+ *
+ * All characters excluding whitespaces considered for splitting stay in place.
+ *
+ * Examples:
+ * "foo bar" will be split to ["foo", "bar"]
+ * "foo \"bar foobar\"" will be split to ["foo", "\"bar foobar\""]
+ * "foo 'bar" will be split to ["foo", "'bar"]
+ *
+ * @param args a string of arguments
+ * @return an mutable copy of the list of all arguments
+ */
+ List parse(String args) {
+ if (args == null || "null".equals(args) || args.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ final List arguments = new LinkedList<>();
+ final StringBuilder argumentBuilder = new StringBuilder();
+ Character quote = null;
+
+ for (int i = 0, l = args.length(); i < l; i++) {
+ char c = args.charAt(i);
+
+ if (Character.isWhitespace(c) && quote == null) {
+ addArgument(argumentBuilder, arguments);
+ continue;
+ } else if (c == '"' || c == '\'') {
+ // explicit boxing allows us to use object caching of the Character class
+ Character currentQuote = Character.valueOf(c);
+ if (quote == null) {
+ quote = currentQuote;
+ } else if (quote.equals(currentQuote)){
+ quote = null;
+ } // else
+ // we ignore the case when a quoted argument contains the other kind of quote
+ }
+
+ argumentBuilder.append(c);
+ }
+
+ addArgument(argumentBuilder, arguments);
+
+ for (String argument : this.additionalArguments) {
+ if (!arguments.contains(argument)) {
+ arguments.add(argument);
+ }
+ }
+
+ return new ArrayList<>(arguments);
+ }
+
+ private static void addArgument(StringBuilder argumentBuilder, List arguments) {
+ if (argumentBuilder.length() > 0) {
+ String argument = argumentBuilder.toString();
+ arguments.add(argument);
+ argumentBuilder.setLength(0);
+ }
+ }
+}
diff --git a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/FileDownloader.java b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/FileDownloader.java
index 597aaef4e..9f5ff1fa1 100644
--- a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/FileDownloader.java
+++ b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/FileDownloader.java
@@ -53,7 +53,11 @@ public DefaultFileDownloader(ProxyConfig proxyConfig){
this.proxyConfig = proxyConfig;
}
+ @Override
public void download(String downloadUrl, String destination, String userName, String password) throws DownloadException {
+ // force tls to 1.2 since github removed weak cryptographic standards
+ // https://blog.github.com/2018-02-02-weak-cryptographic-standards-removal-notice/
+ System.setProperty("https.protocols", "TLSv1.2");
String fixedDownloadUrl = downloadUrl;
try {
fixedDownloadUrl = FilenameUtils.separatorsToUnix(fixedDownloadUrl);
@@ -73,11 +77,8 @@ public void download(String downloadUrl, String destination, String userName, St
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
fos.close();
}
- } catch (IOException e) {
- throw new DownloadException("Could not download "+fixedDownloadUrl, e);
- }
- catch (URISyntaxException e) {
- throw new DownloadException("Could not download "+fixedDownloadUrl, e);
+ } catch (IOException | URISyntaxException e) {
+ throw new DownloadException("Could not download " + fixedDownloadUrl, e);
}
}
diff --git a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/FrontendPluginFactory.java b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/FrontendPluginFactory.java
index 1e3ec0b85..799cd36aa 100644
--- a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/FrontendPluginFactory.java
+++ b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/FrontendPluginFactory.java
@@ -3,7 +3,7 @@
import java.io.File;
public final class FrontendPluginFactory {
-
+
private static final Platform defaultPlatform = Platform.guess();
private static final String DEFAULT_CACHE_PATH = "cache";
@@ -29,13 +29,17 @@ public NPMInstaller getNPMInstaller(ProxyConfig proxy) {
return new NPMInstaller(getInstallConfig(), new DefaultArchiveExtractor(), new DefaultFileDownloader(proxy));
}
+ public PnpmInstaller getPnpmInstaller(ProxyConfig proxy) {
+ return new PnpmInstaller(getInstallConfig(), new DefaultArchiveExtractor(), new DefaultFileDownloader(proxy));
+ }
+
public YarnInstaller getYarnInstaller(ProxyConfig proxy) {
return new YarnInstaller(getInstallConfig(), new DefaultArchiveExtractor(), new DefaultFileDownloader(proxy));
}
-
+
public BowerRunner getBowerRunner(ProxyConfig proxy) {
return new DefaultBowerRunner(getExecutorConfig(), proxy);
- }
+ }
public JspmRunner getJspmRunner() {
return new DefaultJspmRunner(getExecutorConfig());
@@ -45,6 +49,14 @@ public NpmRunner getNpmRunner(ProxyConfig proxy, String npmRegistryURL) {
return new DefaultNpmRunner(getExecutorConfig(), proxy, npmRegistryURL);
}
+ public PnpmRunner getPnpmRunner(ProxyConfig proxyConfig, String npmRegistryUrl) {
+ return new DefaultPnpmRunner(getExecutorConfig(), proxyConfig, npmRegistryUrl);
+ }
+
+ public NpxRunner getNpxRunner(ProxyConfig proxy, String npmRegistryURL) {
+ return new DefaultNpxRunner(getExecutorConfig(), proxy, npmRegistryURL);
+ }
+
public YarnRunner getYarnRunner(ProxyConfig proxy, String npmRegistryURL) {
return new DefaultYarnRunner(new InstallYarnExecutorConfig(getInstallConfig()), proxy, npmRegistryURL);
}
diff --git a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NPMInstaller.java b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NPMInstaller.java
index 68ff346f3..9df7a255d 100644
--- a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NPMInstaller.java
+++ b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NPMInstaller.java
@@ -1,12 +1,12 @@
package com.github.eirslett.maven.plugins.frontend.lib;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
-
import org.apache.commons.io.FileUtils;
-import org.codehaus.jackson.map.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -81,6 +81,7 @@ public void install() throws InstallationException {
if (!npmProvided() && !npmIsAlreadyInstalled()) {
installNpm();
}
+ copyNpmScripts();
}
}
@@ -139,12 +140,28 @@ private void installNpm() throws InstallationException {
this.logger.warn("Failed to delete existing NPM installation.");
}
- extractFile(archive, nodeModulesDirectory);
+ File packageDirectory = new File(nodeModulesDirectory, "package");
+ try {
+ extractFile(archive, nodeModulesDirectory);
+ } catch (ArchiveExtractionException e) {
+ if (e.getCause() instanceof EOFException) {
+ // https://github.com/eirslett/frontend-maven-plugin/issues/794
+ // The downloading was probably interrupted and archive file is incomplete:
+ // delete it to retry from scratch
+ this.logger.error("The archive file {} is corrupted and will be deleted. "
+ + "Please try the build again.", archive.getPath());
+ archive.delete();
+ if (packageDirectory.exists()) {
+ FileUtils.deleteDirectory(packageDirectory);
+ }
+ }
+
+ throw e;
+ }
// handles difference between old and new download root (nodejs.org/dist/npm and
// registry.npmjs.org)
// see https://github.com/eirslett/frontend-maven-plugin/issues/65#issuecomment-52024254
- File packageDirectory = new File(nodeModulesDirectory, "package");
if (packageDirectory.exists() && !npmDirectory.exists()) {
if (!packageDirectory.renameTo(npmDirectory)) {
this.logger.warn("Cannot rename NPM directory, making a copy.");
@@ -152,16 +169,6 @@ private void installNpm() throws InstallationException {
}
}
- // create a copy of the npm scripts next to the node executable
- for (String script : Arrays.asList("npm", "npm.cmd")) {
- File scriptFile = new File(npmDirectory, "bin" + File.separator + script);
- if (scriptFile.exists()) {
- File copy = new File(installDirectory, script);
- FileUtils.copyFile(scriptFile, copy);
- copy.setExecutable(true);
- }
- }
-
this.logger.info("Installed npm locally.");
} catch (DownloadException e) {
throw new InstallationException("Could not download npm", e);
@@ -172,6 +179,31 @@ private void installNpm() throws InstallationException {
}
}
+ private void copyNpmScripts() throws InstallationException{
+ File installDirectory = getNodeInstallDirectory();
+
+ File nodeModulesDirectory = new File(installDirectory, "node_modules");
+ File npmDirectory = new File(nodeModulesDirectory, "npm");
+ // create a copy of the npm scripts next to the node executable
+ for (String script : Arrays.asList("npm", "npm.cmd", "npx", "npx.cmd")) {
+ File scriptFile = new File(npmDirectory, "bin" + File.separator + script);
+ if (scriptFile.exists()) {
+ File copy = new File(installDirectory, script);
+ if (!copy.exists()) {
+ try
+ {
+ FileUtils.copyFile(scriptFile, copy);
+ }
+ catch (IOException e)
+ {
+ throw new InstallationException("Could not copy npm", e);
+ }
+ copy.setExecutable(true);
+ }
+ }
+ }
+ }
+
private File getNodeInstallDirectory() {
File installDirectory = new File(this.config.getInstallDirectory(), NodeInstaller.INSTALL_PATH);
if (!installDirectory.exists()) {
diff --git a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NodeExecutorConfig.java b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NodeExecutorConfig.java
index 27f6c9bb0..af8bb2ef6 100644
--- a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NodeExecutorConfig.java
+++ b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NodeExecutorConfig.java
@@ -5,6 +5,10 @@
public interface NodeExecutorConfig {
File getNodePath();
File getNpmPath();
+ File getPnpmPath();
+ File getPnpmCjsPath();
+
+ File getNpxPath();
File getInstallDirectory();
File getWorkingDirectory();
Platform getPlatform();
@@ -15,6 +19,9 @@ final class InstallNodeExecutorConfig implements NodeExecutorConfig {
private static final String NODE_WINDOWS = NodeInstaller.INSTALL_PATH.replaceAll("/", "\\\\") + "\\node.exe";
private static final String NODE_DEFAULT = NodeInstaller.INSTALL_PATH + "/node";
private static final String NPM = NodeInstaller.INSTALL_PATH + "/node_modules/npm/bin/npm-cli.js";
+ private static final String PNPM = NodeInstaller.INSTALL_PATH + "/node_modules/pnpm/bin/pnpm.js";
+ private static final String PNPM_CJS = NodeInstaller.INSTALL_PATH + "/node_modules/pnpm/bin/pnpm.cjs";
+ private static final String NPX = NodeInstaller.INSTALL_PATH + "/node_modules/npm/bin/npx-cli.js";
private final InstallConfig installConfig;
@@ -33,6 +40,22 @@ public File getNpmPath() {
return new File(installConfig.getInstallDirectory() + Utils.normalize(NPM));
}
+
+ @Override
+ public File getPnpmPath() {
+ return new File(installConfig.getInstallDirectory() + Utils.normalize(PNPM));
+ }
+
+ @Override
+ public File getPnpmCjsPath() {
+ return new File(installConfig.getInstallDirectory() + Utils.normalize(PNPM_CJS));
+ }
+
+ @Override
+ public File getNpxPath() {
+ return new File(installConfig.getInstallDirectory() + Utils.normalize(NPX));
+ }
+
@Override
public File getInstallDirectory() {
return installConfig.getInstallDirectory();
@@ -47,4 +70,4 @@ public File getWorkingDirectory() {
public Platform getPlatform() {
return installConfig.getPlatform();
}
-}
\ No newline at end of file
+}
diff --git a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NodeInstaller.java b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NodeInstaller.java
index 302f691a2..5b5c68190 100644
--- a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NodeInstaller.java
+++ b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NodeInstaller.java
@@ -1,8 +1,11 @@
package com.github.eirslett.maven.plugins.frontend.lib;
+import java.io.EOFException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import org.apache.commons.io.FileUtils;
@@ -13,8 +16,6 @@ public class NodeInstaller {
public static final String INSTALL_PATH = "/node";
- public static final String DEFAULT_NODEJS_DOWNLOAD_ROOT = "https://nodejs.org/dist/";
-
private static final Object LOCK = new Object();
private String npmVersion, nodeVersion, nodeDownloadRoot, userName, password;
@@ -77,7 +78,7 @@ public void install() throws InstallationException {
// use static lock object for a synchronized block
synchronized (LOCK) {
if (this.nodeDownloadRoot == null || this.nodeDownloadRoot.isEmpty()) {
- this.nodeDownloadRoot = DEFAULT_NODEJS_DOWNLOAD_ROOT;
+ this.nodeDownloadRoot = this.config.getPlatform().getNodeDownloadRoot();
}
if (!nodeIsAlreadyInstalled()) {
this.logger.info("Installing node version {}", this.nodeVersion);
@@ -117,6 +118,7 @@ private boolean nodeIsAlreadyInstalled() {
return false;
}
} catch (ProcessExecutionException e) {
+ this.logger.warn("Unable to determine current node version: {}", e.getMessage());
return false;
}
}
@@ -127,7 +129,7 @@ private void installNodeDefault() throws InstallationException {
this.config.getPlatform().getLongNodeFilename(this.nodeVersion, false);
String downloadUrl = this.nodeDownloadRoot
+ this.config.getPlatform().getNodeDownloadFilename(this.nodeVersion, false);
- String classifier = this.config.getPlatform().getNodeClassifier();
+ String classifier = this.config.getPlatform().getNodeClassifier(this.nodeVersion);
File tmpDirectory = getTempDirectory();
@@ -138,7 +140,21 @@ private void installNodeDefault() throws InstallationException {
downloadFileIfMissing(downloadUrl, archive, this.userName, this.password);
- extractFile(archive, tmpDirectory);
+ try {
+ extractFile(archive, tmpDirectory);
+ } catch (ArchiveExtractionException e) {
+ if (e.getCause() instanceof EOFException) {
+ // https://github.com/eirslett/frontend-maven-plugin/issues/794
+ // The downloading was probably interrupted and archive file is incomplete:
+ // delete it to retry from scratch
+ this.logger.error("The archive file {} is corrupted and will be deleted. "
+ + "Please try the build again.", archive.getPath());
+ archive.delete();
+ FileUtils.deleteDirectory(tmpDirectory);
+ }
+
+ throw e;
+ }
// Search for the node binary
File nodeBinary =
@@ -151,7 +167,12 @@ private void installNodeDefault() throws InstallationException {
File destination = new File(destinationDirectory, "node");
this.logger.info("Copying node binary from {} to {}", nodeBinary, destination);
- if (!nodeBinary.renameTo(destination)) {
+ if (destination.exists() && !destination.delete()) {
+ throw new InstallationException("Could not install Node: Was not allowed to delete " + destination);
+ }
+ try {
+ Files.move(nodeBinary.toPath(), destination.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ } catch (IOException e) {
throw new InstallationException("Could not install Node: Was not allowed to rename "
+ nodeBinary + " to " + destination);
}
@@ -196,7 +217,7 @@ private void installNodeWithNpmForWindows() throws InstallationException {
this.config.getPlatform().getLongNodeFilename(this.nodeVersion, true);
String downloadUrl = this.nodeDownloadRoot
+ this.config.getPlatform().getNodeDownloadFilename(this.nodeVersion, true);
- String classifier = this.config.getPlatform().getNodeClassifier();
+ String classifier = this.config.getPlatform().getNodeClassifier(this.nodeVersion);
File tmpDirectory = getTempDirectory();
@@ -219,7 +240,9 @@ private void installNodeWithNpmForWindows() throws InstallationException {
File destination = new File(destinationDirectory, "node.exe");
this.logger.info("Copying node binary from {} to {}", nodeBinary, destination);
- if (!nodeBinary.renameTo(destination)) {
+ try {
+ Files.move(nodeBinary.toPath(), destination.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ } catch (IOException e) {
throw new InstallationException("Could not install Node: Was not allowed to rename "
+ nodeBinary + " to " + destination);
}
@@ -252,7 +275,7 @@ private void installNodeForWindows() throws InstallationException {
File destination = new File(destinationDirectory, "node.exe");
- String classifier = this.config.getPlatform().getNodeClassifier();
+ String classifier = this.config.getPlatform().getNodeClassifier(this.nodeVersion);
CacheDescriptor cacheDescriptor =
new CacheDescriptor("node", this.nodeVersion, classifier, "exe");
diff --git a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NodeTaskExecutor.java b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NodeTaskExecutor.java
index 09196f38e..e9c6c07e2 100644
--- a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NodeTaskExecutor.java
+++ b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NodeTaskExecutor.java
@@ -21,8 +21,8 @@ abstract class NodeTaskExecutor {
private final Logger logger;
private final String taskName;
- private final String taskLocation;
- private final List additionalArguments;
+ private String taskLocation;
+ private final ArgumentsParser argumentsParser;
private final NodeExecutorConfig config;
private final Map proxy;
@@ -93,17 +93,7 @@ private String getAbsoluteTaskLocation() {
private List getArguments(String args) {
- List arguments = new ArrayList();
- if (args != null && !args.equals("null") && !args.isEmpty()) {
- arguments.addAll(Arrays.asList(args.split("\\s+")));
- }
-
- for (String argument : additionalArguments) {
- if (!arguments.contains(argument)) {
- arguments.add(argument);
- }
- }
- return arguments;
+ return argumentsParser.parse(args);
}
private static String taskToString(String taskName, List arguments) {
@@ -144,4 +134,8 @@ private static String maskPassword(String proxyString) {
}
return retVal;
}
+
+ public void setTaskLocation(String taskLocation) {
+ this.taskLocation = taskLocation;
+ }
}
diff --git a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NpmRunner.java b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NpmRunner.java
index 6c887691d..681f246b9 100644
--- a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NpmRunner.java
+++ b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NpmRunner.java
@@ -18,7 +18,8 @@ public DefaultNpmRunner(NodeExecutorConfig config, ProxyConfig proxyConfig, Stri
buildProxy(proxyConfig, npmRegistryURL));
}
- private static List buildArguments(ProxyConfig proxyConfig, String npmRegistryURL) {
+ // Visible for testing only.
+ static List buildArguments(ProxyConfig proxyConfig, String npmRegistryURL) {
List arguments = new ArrayList();
if(npmRegistryURL != null && !npmRegistryURL.isEmpty()){
@@ -30,6 +31,14 @@ private static List buildArguments(ProxyConfig proxyConfig, String npmRe
arguments.add("--https-proxy=" + proxy.getUri().toString());
arguments.add("--proxy=" + proxy.getUri().toString());
+
+ final String nonProxyHosts = proxy.getNonProxyHosts();
+ if (nonProxyHosts != null && !nonProxyHosts.isEmpty()) {
+ final String[] nonProxyHostList = nonProxyHosts.split("\\|");
+ for (String nonProxyHost: nonProxyHostList) {
+ arguments.add("--noproxy=" + nonProxyHost.replace("*", ""));
+ }
+ }
}
return arguments;
diff --git a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NpxRunner.java b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NpxRunner.java
new file mode 100644
index 000000000..b4f4db1d3
--- /dev/null
+++ b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/NpxRunner.java
@@ -0,0 +1,60 @@
+package com.github.eirslett.maven.plugins.frontend.lib;
+
+import com.github.eirslett.maven.plugins.frontend.lib.ProxyConfig.Proxy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public interface NpxRunner extends NodeTaskRunner {}
+
+final class DefaultNpxRunner extends NodeTaskExecutor implements NpxRunner {
+ static final String TASK_NAME = "npx";
+
+ public DefaultNpxRunner(NodeExecutorConfig config, ProxyConfig proxyConfig, String npmRegistryURL) {
+ super(config, TASK_NAME, config.getNpxPath().getAbsolutePath(), buildNpmArguments(proxyConfig, npmRegistryURL));
+ }
+
+ // Visible for testing only.
+ /**
+ * These are, in fact, npm arguments, that need to be split from the npx arguments by '--'.
+ *
+ * See an example:
+ * npx some-package -- --registry=http://myspecialregisty.com
+ */
+ static List buildNpmArguments(ProxyConfig proxyConfig, String npmRegistryURL) {
+ List arguments = new ArrayList<>();
+
+ if(npmRegistryURL != null && !npmRegistryURL.isEmpty()){
+ arguments.add ("--registry=" + npmRegistryURL);
+ }
+
+ if(!proxyConfig.isEmpty()){
+ Proxy proxy = null;
+ if(npmRegistryURL != null && !npmRegistryURL.isEmpty()){
+ proxy = proxyConfig.getProxyForUrl(npmRegistryURL);
+ }
+
+ if(proxy == null){
+ proxy = proxyConfig.getSecureProxy();
+ }
+
+ if(proxy == null){
+ proxy = proxyConfig.getInsecureProxy();
+ }
+
+ arguments.add("--https-proxy=" + proxy.getUri().toString());
+ arguments.add("--proxy=" + proxy.getUri().toString());
+ }
+
+ List npmArguments;
+ if (arguments.isEmpty()) {
+ npmArguments = arguments;
+ } else {
+ npmArguments = new ArrayList<>();
+ npmArguments.add("--");
+ npmArguments.addAll(arguments);
+ }
+
+ return npmArguments;
+ }
+}
diff --git a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/Platform.java b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/Platform.java
index ce7b808a9..64d088baa 100644
--- a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/Platform.java
+++ b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/Platform.java
@@ -1,27 +1,45 @@
package com.github.eirslett.maven.plugins.frontend.lib;
-enum Architecture { x86, x64, ppc64le, s390x, arm64;
+import java.io.File;
+import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+enum Architecture { x86, x64, ppc64le, s390x, arm64, armv7l, ppc, ppc64;
public static Architecture guess(){
String arch = System.getProperty("os.arch");
+ String version = System.getProperty("os.version");
+
if (arch.equals("ppc64le")) {
return ppc64le;
} else if (arch.equals("aarch64")) {
return arm64;
} else if (arch.equals("s390x")) {
- return s390x;
+ return s390x;
+ } else if (arch.equals("arm")) {
+ if (version.contains("v7")) {
+ return armv7l;
+ } else {
+ return arm64;
+ }
+ } else if (arch.equals("ppc64")) {
+ return ppc64;
+ } else if (arch.equals("ppc")) {
+ return ppc;
} else {
return arch.contains("64") ? x64 : x86;
}
}
}
-enum OS { Windows, Mac, Linux, SunOS;
+enum OS { Windows, Mac, Linux, SunOS, AIX;
public static OS guess() {
final String osName = System.getProperty("os.name");
return osName.contains("Windows") ? OS.Windows :
osName.contains("Mac") ? OS.Mac :
osName.contains("SunOS") ? OS.SunOS :
+ osName.toUpperCase().contains("AIX") ? OS.AIX :
OS.Linux;
}
@@ -40,6 +58,8 @@ public String getCodename(){
return "win";
} else if(this == OS.SunOS){
return "sunos";
+ } else if(this == OS.AIX){
+ return "aix";
} else {
return "linux";
}
@@ -47,18 +67,64 @@ public String getCodename(){
}
class Platform {
+
+ /**
+ * Node.js supports Apple silicon since v16
+ * https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V16.md#toolchain-and-compiler-upgrades
+ */
+ private static final int NODE_VERSION_THRESHOLD_MAC_ARM64 = 16;
+
+ private final String nodeDownloadRoot;
private final OS os;
private final Architecture architecture;
+ private final String classifier;
+
+ public Platform() {
+ this(OS.guess(), Architecture.guess());
+ }
public Platform(OS os, Architecture architecture) {
+ this("https://nodejs.org/dist/", os, architecture, null);
+ }
+
+ public Platform(
+ String nodeDownloadRoot,
+ OS os,
+ Architecture architecture,
+ String classifier
+ ) {
+ this.nodeDownloadRoot = nodeDownloadRoot;
this.os = os;
this.architecture = architecture;
+ this.classifier = classifier;
+ }
+
+ public static Platform guess() {
+ return Platform.guess(OS.guess(), Architecture.guess(), Platform::CHECK_FOR_ALPINE);
}
- public static Platform guess(){
- OS os = OS.guess();
- Architecture architecture = Architecture.guess();
- return new Platform(os,architecture);
+ // Default implementation
+ public static Boolean CHECK_FOR_ALPINE() {
+ return new File("/etc/alpine-release").exists();
+ }
+
+ public static Platform guess(OS os, Architecture architecture, Supplier checkForAlpine){
+ // The default libc is glibc, but Alpine uses musl. When not default, the nodejs download
+ // (and path within it) needs a classifier in the suffix (ex. -musl).
+ // We know Alpine is in use if the release file exists, and this is the simplest check.
+ if (os == OS.Linux && checkForAlpine.get()) {
+ return new Platform(
+ // Currently, musl is Experimental. The download root can be overridden with config
+ // if this changes and there's not been an update to this project, yet.
+ // See https://github.com/nodejs/node/blob/master/BUILDING.md#platform-list
+ "https://unofficial-builds.nodejs.org/download/release/",
+ os, architecture, "musl");
+ }
+ return new Platform(os, architecture);
+ }
+
+ public String getNodeDownloadRoot(){
+ return nodeDownloadRoot;
}
public String getArchiveExtension(){
@@ -81,7 +147,7 @@ public String getLongNodeFilename(String nodeVersion, boolean archiveOnWindows)
if(isWindows() && !archiveOnWindows){
return "node.exe";
} else {
- return "node-" + nodeVersion + "-" + this.getNodeClassifier();
+ return "node-" + nodeVersion + "-" + this.getNodeClassifier(nodeVersion);
}
}
@@ -105,7 +171,30 @@ public String getNodeDownloadFilename(String nodeVersion, boolean archiveOnWindo
}
}
- public String getNodeClassifier() {
- return this.getCodename() + "-" + this.architecture.name();
+ public String getNodeClassifier(String nodeVersion) {
+ String result = getCodename() + "-" + resolveArchitecture(nodeVersion).name();
+ return classifier != null ? result + "-" + classifier : result;
+ }
+
+ private Architecture resolveArchitecture(String nodeVersion) {
+ if (isMac() && architecture == Architecture.arm64) {
+ Integer nodeMajorVersion = getNodeMajorVersion(nodeVersion);
+ if (nodeMajorVersion == null || nodeMajorVersion < NODE_VERSION_THRESHOLD_MAC_ARM64) {
+ return Architecture.x64;
+ }
+ }
+
+ return architecture;
+ }
+
+ static Integer getNodeMajorVersion(String nodeVersion) {
+ Matcher matcher = Pattern.compile("^v(\\d+)\\..*$").matcher(nodeVersion);
+ if (matcher.matches()) {
+ return Integer.parseInt(matcher.group(1));
+ } else {
+ // malformed node version
+ return null;
+ }
}
+
}
diff --git a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/PnpmInstaller.java b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/PnpmInstaller.java
new file mode 100644
index 000000000..9e6618c93
--- /dev/null
+++ b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/PnpmInstaller.java
@@ -0,0 +1,237 @@
+package com.github.eirslett.maven.plugins.frontend.lib;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.EOFException;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.HashMap;
+import org.apache.commons.io.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class PnpmInstaller {
+
+ private static final String VERSION = "version";
+
+ public static final String DEFAULT_PNPM_DOWNLOAD_ROOT = "https://registry.npmjs.org/pnpm/-/";
+
+ private static final Object LOCK = new Object();
+
+ private String pnpmVersion, pnpmDownloadRoot, userName, password;
+
+ private final Logger logger;
+
+ private final InstallConfig config;
+
+ private final ArchiveExtractor archiveExtractor;
+
+ private final FileDownloader fileDownloader;
+
+ PnpmInstaller(InstallConfig config, ArchiveExtractor archiveExtractor, FileDownloader fileDownloader) {
+ this.logger = LoggerFactory.getLogger(getClass());
+ this.config = config;
+ this.archiveExtractor = archiveExtractor;
+ this.fileDownloader = fileDownloader;
+ }
+
+ public PnpmInstaller setNodeVersion(String nodeVersion) {
+ return this;
+ }
+
+ public PnpmInstaller setPnpmVersion(String pnpmVersion) {
+ this.pnpmVersion = pnpmVersion;
+ return this;
+ }
+
+ public PnpmInstaller setPnpmDownloadRoot(String pnpmDownloadRoot) {
+ this.pnpmDownloadRoot = pnpmDownloadRoot;
+ return this;
+ }
+
+ public PnpmInstaller setUserName(String userName) {
+ this.userName = userName;
+ return this;
+ }
+
+ public PnpmInstaller setPassword(String password) {
+ this.password = password;
+ return this;
+ }
+
+ public void install() throws InstallationException {
+ // use static lock object for a synchronized block
+ synchronized (LOCK) {
+ if (this.pnpmDownloadRoot == null || this.pnpmDownloadRoot.isEmpty()) {
+ this.pnpmDownloadRoot = DEFAULT_PNPM_DOWNLOAD_ROOT;
+ }
+ if (!pnpmIsAlreadyInstalled()) {
+ installPnpm();
+ }
+ copyPnpmScripts();
+ }
+ }
+
+ private boolean pnpmIsAlreadyInstalled() {
+ try {
+ final File pnpmPackageJson = new File(
+ this.config.getInstallDirectory() + Utils.normalize("/node/node_modules/pnpm/package.json"));
+ if (pnpmPackageJson.exists()) {
+ HashMap data = new ObjectMapper().readValue(pnpmPackageJson, HashMap.class);
+ if (data.containsKey(VERSION)) {
+ final String foundPnpmVersion = data.get(VERSION).toString();
+ if (foundPnpmVersion.equals(this.pnpmVersion.replaceFirst("^v", ""))) {
+ this.logger.info("PNPM {} is already installed.", foundPnpmVersion);
+ return true;
+ } else {
+ this.logger.info("PNPM {} was installed, but we need version {}", foundPnpmVersion,
+ this.pnpmVersion);
+ return false;
+ }
+ } else {
+ this.logger.info("Could not read PNPM version from package.json");
+ return false;
+ }
+ } else {
+ return false;
+ }
+ } catch (IOException ex) {
+ throw new RuntimeException("Could not read package.json", ex);
+ }
+ }
+
+ private void installPnpm() throws InstallationException {
+ try {
+ this.logger.info("Installing pnpm version {}", this.pnpmVersion);
+ String pnpmVersionClean = this.pnpmVersion.replaceFirst("^v(?=[0-9]+)", "");
+ final String downloadUrl = this.pnpmDownloadRoot + "pnpm-" + pnpmVersionClean + ".tgz";
+
+ CacheDescriptor cacheDescriptor = new CacheDescriptor("pnpm", pnpmVersionClean, "tar.gz");
+
+ File archive = this.config.getCacheResolver().resolve(cacheDescriptor);
+
+ downloadFileIfMissing(downloadUrl, archive, this.userName, this.password);
+
+ File installDirectory = getNodeInstallDirectory();
+ File nodeModulesDirectory = new File(installDirectory, "node_modules");
+
+ // We need to delete the existing pnpm directory first so we clean out any old files, and
+ // so we can rename the package directory below.
+ File oldNpmDirectory = new File(installDirectory, "pnpm");
+ File pnpmDirectory = new File(nodeModulesDirectory, "pnpm");
+ try {
+ if (oldNpmDirectory.isDirectory()) {
+ FileUtils.deleteDirectory(oldNpmDirectory);
+ }
+ FileUtils.deleteDirectory(pnpmDirectory);
+ } catch (IOException e) {
+ this.logger.warn("Failed to delete existing PNPM installation.");
+ }
+
+ File packageDirectory = new File(nodeModulesDirectory, "package");
+ try {
+ extractFile(archive, nodeModulesDirectory);
+ } catch (ArchiveExtractionException e) {
+ if (e.getCause() instanceof EOFException) {
+ // https://github.com/eirslett/frontend-maven-plugin/issues/794
+ // The downloading was probably interrupted and archive file is incomplete:
+ // delete it to retry from scratch
+ this.logger.error("The archive file {} is corrupted and will be deleted. "
+ + "Please try the build again.", archive.getPath());
+ archive.delete();
+ if (packageDirectory.exists()) {
+ FileUtils.deleteDirectory(packageDirectory);
+ }
+ }
+
+ throw e;
+ }
+
+ // handles difference between old and new download root (nodejs.org/dist/npm and
+ // registry.npmjs.org)
+ // see https://github.com/eirslett/frontend-maven-plugin/issues/65#issuecomment-52024254
+ if (packageDirectory.exists() && !pnpmDirectory.exists()) {
+ if (!packageDirectory.renameTo(pnpmDirectory)) {
+ this.logger.warn("Cannot rename PNPM directory, making a copy.");
+ FileUtils.copyDirectory(packageDirectory, pnpmDirectory);
+ }
+ }
+
+ this.logger.info("Installed pnpm locally.");
+
+ } catch (DownloadException e) {
+ throw new InstallationException("Could not download pnpm", e);
+ } catch (ArchiveExtractionException e) {
+ throw new InstallationException("Could not extract the pnpm archive", e);
+ } catch (IOException e) {
+ throw new InstallationException("Could not copy pnpm", e);
+ }
+ }
+
+ private void copyPnpmScripts() throws InstallationException{
+ File installDirectory = getNodeInstallDirectory();
+
+ File nodeModulesDirectory = new File(installDirectory, "node_modules");
+ File pnpmDirectory = new File(nodeModulesDirectory, "pnpm");
+ // create a copy of the pnpm scripts next to the node executable
+ for (String script : Arrays.asList("pnpm", "pnpm.cmd")) {
+ File scriptFile = new File(pnpmDirectory, "bin" + File.separator + script);
+ if (scriptFile.exists()) {
+ File copy = new File(installDirectory, script);
+ if (!copy.exists()) {
+ try
+ {
+ FileUtils.copyFile(scriptFile, copy);
+ }
+ catch (IOException e)
+ {
+ throw new InstallationException("Could not copy pnpm", e);
+ }
+ copy.setExecutable(true);
+ }
+ }
+ }
+ // On non-windows platforms, if no predefined executables exist, symlink the .cjs executable
+ File pnpmExecutable = new File(installDirectory, "pnpm");
+ if (!pnpmExecutable.exists() && !this.config.getPlatform().isWindows()) {
+
+ File pnpmJsExecutable = new File(pnpmDirectory, "bin" + File.separator + "pnpm.cjs");
+ if (pnpmJsExecutable.exists()) {
+ this.logger.info("No pnpm executable found, creating symlink to {}", pnpmJsExecutable.toPath());
+ try {
+ Files.createSymbolicLink(pnpmExecutable.toPath(), pnpmJsExecutable.toPath());
+ } catch (IOException e) {
+ throw new InstallationException("Could not copy pnpm", e);
+ }
+ }
+ }
+ }
+
+ private File getNodeInstallDirectory() {
+ File installDirectory = new File(this.config.getInstallDirectory(), NodeInstaller.INSTALL_PATH);
+ if (!installDirectory.exists()) {
+ this.logger.debug("Creating install directory {}", installDirectory);
+ installDirectory.mkdirs();
+ }
+ return installDirectory;
+ }
+
+ private void extractFile(File archive, File destinationDirectory) throws ArchiveExtractionException {
+ this.logger.info("Unpacking {} into {}", archive, destinationDirectory);
+ this.archiveExtractor.extract(archive.getPath(), destinationDirectory.getPath());
+ }
+
+ private void downloadFileIfMissing(String downloadUrl, File destination, String userName, String password)
+ throws DownloadException {
+ if (!destination.exists()) {
+ downloadFile(downloadUrl, destination, userName, password);
+ }
+ }
+
+ private void downloadFile(String downloadUrl, File destination, String userName, String password)
+ throws DownloadException {
+ this.logger.info("Downloading {} to {}", downloadUrl, destination);
+ this.fileDownloader.download(downloadUrl, destination.getPath(), userName, password);
+ }
+}
diff --git a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/PnpmRunner.java b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/PnpmRunner.java
new file mode 100644
index 000000000..0d735f445
--- /dev/null
+++ b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/PnpmRunner.java
@@ -0,0 +1,54 @@
+package com.github.eirslett.maven.plugins.frontend.lib;
+
+import com.github.eirslett.maven.plugins.frontend.lib.ProxyConfig.Proxy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public interface PnpmRunner extends NodeTaskRunner {}
+
+final class DefaultPnpmRunner extends NodeTaskExecutor implements PnpmRunner {
+ static final String TASK_NAME = "pnpm";
+
+ public DefaultPnpmRunner(NodeExecutorConfig config, ProxyConfig proxyConfig, String npmRegistryURL) {
+ super(config, TASK_NAME, config.getPnpmPath().getAbsolutePath(), buildArguments(proxyConfig, npmRegistryURL));
+
+ if (!config.getPnpmPath().exists() && config.getPnpmCjsPath().exists()) {
+ setTaskLocation(config.getPnpmCjsPath().getAbsolutePath());
+ }
+ }
+
+ // Visible for testing only.
+ static List buildArguments(ProxyConfig proxyConfig, String npmRegistryURL) {
+ List arguments = new ArrayList();
+
+ if(npmRegistryURL != null && !npmRegistryURL.isEmpty()){
+ arguments.add ("--registry=" + npmRegistryURL);
+ }
+
+ if(!proxyConfig.isEmpty()){
+ Proxy proxy = null;
+ if(npmRegistryURL != null && !npmRegistryURL.isEmpty()){
+ proxy = proxyConfig.getProxyForUrl(npmRegistryURL);
+ }
+
+ if(proxy == null){
+ proxy = proxyConfig.getSecureProxy();
+ }
+
+ if(proxy == null){
+ proxy = proxyConfig.getInsecureProxy();
+ }
+
+ arguments.add("--https-proxy=" + proxy.getUri().toString());
+ arguments.add("--proxy=" + proxy.getUri().toString());
+
+ final String nonProxyHosts = proxy.getNonProxyHosts();
+ if (nonProxyHosts != null && !nonProxyHosts.isEmpty()) {
+ arguments.add("--noproxy=" + nonProxyHosts.replace('|',','));
+ }
+ }
+
+ return arguments;
+ }
+}
diff --git a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/ProcessExecutor.java b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/ProcessExecutor.java
index b3c224906..0e273f167 100644
--- a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/ProcessExecutor.java
+++ b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/ProcessExecutor.java
@@ -31,6 +31,8 @@ public ProcessExecutionException(Throwable cause) {
}
final class ProcessExecutor {
+ private final static String PATH_ENV_VAR = "PATH";
+
private final Map environment;
private CommandLine commandLine;
private final Executor executor;
@@ -59,9 +61,7 @@ public String executeAndGetResult(final Logger logger) throws ProcessExecutionEx
public int executeAndRedirectOutput(final Logger logger) throws ProcessExecutionException {
OutputStream stdout = new LoggerOutputStream(logger, 0);
- OutputStream stderr = new LoggerOutputStream(logger, 1);
-
- return execute(logger, stdout, stderr);
+ return execute(logger, stdout, stdout);
}
private int execute(final Logger logger, final OutputStream stdout, final OutputStream stderr)
@@ -96,33 +96,38 @@ private CommandLine createCommandLine(List command) {
return commmandLine;
}
- private Map createEnvironment(List paths, Platform platform, Map additionalEnvironment) {
+ private Map createEnvironment(final List paths, final Platform platform, final Map additionalEnvironment) {
final Map environment = new HashMap<>(System.getenv());
- String pathVarName = "PATH";
- String pathVarValue = environment.get(pathVarName);
+
+ if (additionalEnvironment != null) {
+ environment.putAll(additionalEnvironment);
+ }
+
if (platform.isWindows()) {
- for (Map.Entry entry : environment.entrySet()) {
- if ("PATH".equalsIgnoreCase(entry.getKey())) {
- pathVarName = entry.getKey();
- pathVarValue = entry.getValue();
+ for (final Map.Entry entry : environment.entrySet()) {
+ final String pathName = entry.getKey();
+ if (PATH_ENV_VAR.equalsIgnoreCase(pathName)) {
+ final String pathValue = entry.getValue();
+ environment.put(pathName, extendPathVariable(pathValue, paths));
}
}
+ } else {
+ final String pathValue = environment.get(PATH_ENV_VAR);
+ environment.put(PATH_ENV_VAR, extendPathVariable(pathValue, paths));
}
- StringBuilder pathBuilder = new StringBuilder();
- if (pathVarValue != null) {
- pathBuilder.append(pathVarValue).append(File.pathSeparator);
- }
- for (String path : paths) {
- pathBuilder.insert(0, File.pathSeparator).insert(0, path);
- }
- environment.put(pathVarName, pathBuilder.toString());
+ return environment;
+ }
- if (additionalEnvironment != null) {
- environment.putAll(additionalEnvironment);
+ private String extendPathVariable(final String existingValue, final List paths) {
+ final StringBuilder pathBuilder = new StringBuilder();
+ for (final String path : paths) {
+ pathBuilder.append(path).append(File.pathSeparator);
}
-
- return environment;
+ if (existingValue != null) {
+ pathBuilder.append(existingValue).append(File.pathSeparator);
+ }
+ return pathBuilder.toString();
}
private Executor createExecutor(File workingDirectory, long timeoutInSeconds) {
@@ -152,15 +157,7 @@ public final void flush() {
@Override
protected void processLine(final String line, final int logLevel) {
- if (logLevel == 0) {
- logger.info(line);
- } else {
- if (line.startsWith("npm WARN ")) {
- logger.warn(line);
- } else {
- logger.error(line);
- }
- }
+ logger.info(line);
}
}
}
diff --git a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/ProxyConfig.java b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/ProxyConfig.java
index 989794554..e10eb3fda 100644
--- a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/ProxyConfig.java
+++ b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/ProxyConfig.java
@@ -62,6 +62,7 @@ public static class Proxy {
public final int port;
public final String username;
public final String password;
+
public final String nonProxyHosts;
public Proxy(String id, String protocol, String host, int port, String username, String password, String nonProxyHosts) {
@@ -106,12 +107,26 @@ public boolean isNonProxyHost(String host) {
return false;
}
+ /**
+ * As per https://docs.npmjs.com/misc/config#noproxy , npm expects a comma (`,`) separated list but
+ * maven settings.xml usually specifies the no proxy hosts as a bar (`|`) separated list (see
+ * http://maven.apache.org/guides/mini/guide-proxies.html) .
+ *
+ * We could do the conversion here but npm seems to accept the bar separated list regardless
+ * of what the documentation says so we do no conversion for now.
+ * @return
+ */
+ public String getNonProxyHosts() {
+ return nonProxyHosts;
+ }
+
@Override
public String toString() {
return id + "{" +
"protocol='" + protocol + '\'' +
", host='" + host + '\'' +
", port=" + port +
+ ", nonProxyHosts='" + nonProxyHosts + '\'' +
(useAuthentication()? ", with username/passport authentication" : "") +
'}';
}
diff --git a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/YarnInstaller.java b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/YarnInstaller.java
index 244e98b76..b7445d99e 100644
--- a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/YarnInstaller.java
+++ b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/YarnInstaller.java
@@ -1,5 +1,6 @@
package com.github.eirslett.maven.plugins.frontend.lib;
+import java.io.EOFException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -22,6 +23,8 @@ public class YarnInstaller {
private String yarnVersion, yarnDownloadRoot, userName, password;
+ private boolean isYarnBerry;
+
private final Logger logger;
private final InstallConfig config;
@@ -42,6 +45,11 @@ public YarnInstaller setYarnVersion(String yarnVersion) {
return this;
}
+ public YarnInstaller setIsYarnBerry(boolean isYarnBerry) {
+ this.isYarnBerry = isYarnBerry;
+ return this;
+ }
+
public YarnInstaller setYarnDownloadRoot(String yarnDownloadRoot) {
this.yarnDownloadRoot = yarnDownloadRoot;
return this;
@@ -84,8 +92,13 @@ private boolean yarnIsAlreadyInstalled() {
logger.info("Yarn {} is already installed.", version);
return true;
} else {
- logger.info("Yarn {} was installed, but we need version {}", version, yarnVersion);
- return false;
+ if (isYarnBerry && Integer.parseInt(version.split("\\.")[0]) > 1) {
+ logger.info("Yarn Berry {} is installed.", version);
+ return true;
+ } else{
+ logger.info("Yarn {} was installed, but we need version {}", version, yarnVersion);
+ return false;
+ }
}
} else {
return false;
@@ -122,7 +135,23 @@ private void installYarn() throws InstallationException {
logger.warn("Failed to delete existing Yarn installation.");
}
- extractFile(archive, installDirectory);
+ try {
+ extractFile(archive, installDirectory);
+ } catch (ArchiveExtractionException e) {
+ if (e.getCause() instanceof EOFException) {
+ // https://github.com/eirslett/frontend-maven-plugin/issues/794
+ // The downloading was probably interrupted and archive file is incomplete:
+ // delete it to retry from scratch
+ this.logger.error("The archive file {} is corrupted and will be deleted. "
+ + "Please try the build again.", archive.getPath());
+ archive.delete();
+ if (installDirectory.exists()) {
+ FileUtils.deleteDirectory(installDirectory);
+ }
+ }
+
+ throw e;
+ }
ensureCorrectYarnRootDirectory(installDirectory, yarnVersion);
diff --git a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/YarnTaskExecutor.java b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/YarnTaskExecutor.java
index 6f948a838..87d826a05 100644
--- a/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/YarnTaskExecutor.java
+++ b/frontend-plugin-core/src/main/java/com/github/eirslett/maven/plugins/frontend/lib/YarnTaskExecutor.java
@@ -20,7 +20,7 @@ abstract class YarnTaskExecutor {
private final String taskName;
- private final List additionalArguments;
+ private final ArgumentsParser argumentsParser;
private final YarnExecutorConfig config;
@@ -42,7 +42,7 @@ public YarnTaskExecutor(YarnExecutorConfig config, String taskName, String taskL
logger = LoggerFactory.getLogger(getClass());
this.config = config;
this.taskName = taskName;
- this.additionalArguments = additionalArguments;
+ this.argumentsParser = new ArgumentsParser(additionalArguments);
}
private static String getTaskNameFromLocation(String taskLocation) {
@@ -66,17 +66,7 @@ public final void execute(String args, Map environment) throws T
}
private List getArguments(String args) {
- List arguments = new ArrayList<>();
- if (args != null && !args.equals("null") && !args.isEmpty()) {
- arguments.addAll(Arrays.asList(args.split("\\s+")));
- }
-
- for (String argument : additionalArguments) {
- if (!arguments.contains(argument)) {
- arguments.add(argument);
- }
- }
- return arguments;
+ return argumentsParser.parse(args);
}
private static String taskToString(String taskName, List arguments) {
diff --git a/frontend-plugin-core/src/test/java/com/github/eirslett/maven/plugins/frontend/lib/ArgumentsParserTest.java b/frontend-plugin-core/src/test/java/com/github/eirslett/maven/plugins/frontend/lib/ArgumentsParserTest.java
new file mode 100644
index 000000000..f92e89da3
--- /dev/null
+++ b/frontend-plugin-core/src/test/java/com/github/eirslett/maven/plugins/frontend/lib/ArgumentsParserTest.java
@@ -0,0 +1,72 @@
+package com.github.eirslett.maven.plugins.frontend.lib;
+
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+public class ArgumentsParserTest {
+
+ @Test
+ public void testNoArguments() {
+ ArgumentsParser parser = new ArgumentsParser();
+
+ assertEquals(0, parser.parse(null).size());
+ assertEquals(0, parser.parse("null").size());
+ assertEquals(0, parser.parse(String.valueOf("")).size());
+ }
+
+ @Test
+ public void testMultipleArgumentsNoQuotes() {
+ ArgumentsParser parser = new ArgumentsParser();
+
+ assertArrayEquals(new Object[] { "foo" }, parser.parse("foo").toArray());
+ assertArrayEquals(new Object[] { "foo", "bar" }, parser.parse("foo bar").toArray());
+ assertArrayEquals(new Object[] { "foo", "bar", "foobar" }, parser.parse("foo bar foobar").toArray());
+ }
+
+ @Test
+ public void testMultipleArgumentsWithQuotes() {
+ ArgumentsParser parser = new ArgumentsParser();
+
+ assertArrayEquals(new Object[] { "foo", "\"bar foobar\"" }, parser.parse("foo \"bar foobar\"").toArray());
+ assertArrayEquals(new Object[] { "\"foo bar\"", "foobar" }, parser.parse("\"foo bar\" foobar").toArray());
+ assertArrayEquals(new Object[] { "foo", "'bar foobar'" }, parser.parse("foo 'bar foobar'").toArray());
+ assertArrayEquals(new Object[] { "'foo bar'", "foobar" }, parser.parse("'foo bar' foobar").toArray());
+ // unclosed quotes
+ assertArrayEquals(new Object[] { "foo", "\"bar foobar" }, parser.parse("foo \"bar foobar").toArray());
+ }
+
+ @Test
+ public void testArgumentsWithMixedQuotes() {
+ ArgumentsParser parser = new ArgumentsParser();
+
+ assertArrayEquals(new Object[] { "foo", "\"bar 'foo bar'\"" }, parser.parse("foo \"bar 'foo bar'\"").toArray());
+ assertArrayEquals(new Object[] { "foo", "\"bar 'foo\"", "'bar " }, parser.parse("foo \"bar 'foo\" 'bar ").toArray());
+ }
+
+ @Test
+ public void repeatedArgumentsAreAccepted() {
+ ArgumentsParser parser = new ArgumentsParser();
+
+ assertArrayEquals(new Object[] { "echo", "echo" }, parser.parse("echo echo").toArray());
+ }
+
+ @Test
+ public void testAdditionalArgumentsNoIntersection() {
+ ArgumentsParser parser = new ArgumentsParser(Arrays.asList("foo", "bar"));
+
+ assertArrayEquals(new Object[] { "foobar", "foo", "bar" }, parser.parse("foobar").toArray());
+ }
+
+ @Test
+ public void testAdditionalArgumentsWithIntersection() {
+ ArgumentsParser parser = new ArgumentsParser(Arrays.asList("foo", "foobar"));
+
+ assertArrayEquals(new Object[] { "bar", "foobar", "foo" }, parser.parse("bar foobar").toArray());
+ }
+}
diff --git a/frontend-plugin-core/src/test/java/com/github/eirslett/maven/plugins/frontend/lib/DefaultArchiveExtractorTest.java b/frontend-plugin-core/src/test/java/com/github/eirslett/maven/plugins/frontend/lib/DefaultArchiveExtractorTest.java
new file mode 100644
index 000000000..9b8a3885d
--- /dev/null
+++ b/frontend-plugin-core/src/test/java/com/github/eirslett/maven/plugins/frontend/lib/DefaultArchiveExtractorTest.java
@@ -0,0 +1,67 @@
+package com.github.eirslett.maven.plugins.frontend.lib;
+
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+
+public class DefaultArchiveExtractorTest {
+
+ private final String BAD_TAR = "src/test/resources/bad.tgz";
+ private final String GOOD_TAR = "src/test/resources/good.tgz";
+
+ @TempDir
+ public File temp;
+
+ private ArchiveExtractor extractor;
+
+ @BeforeEach
+ public void setup() {
+ extractor = new DefaultArchiveExtractor();
+ }
+
+ @Test
+ public void extractGoodTarFile() throws Exception {
+ extractor.extract(GOOD_TAR, temp.getPath());
+ }
+
+ @Test
+ public void extractGoodTarFileSymlink() throws Exception {
+ File destination = new File(temp.getPath() + "/destination");
+ destination.mkdir();
+ Path link = createSymlinkOrSkipTest(temp.toPath().resolve("link"), destination.toPath());
+ extractor.extract(GOOD_TAR, link.toString());
+ }
+
+ @Test
+ public void extractBadTarFile() {
+ Assertions.assertThrows(ArchiveExtractionException.class, () ->
+ extractor.extract(BAD_TAR, temp.getPath()));
+ }
+
+ @Test
+ public void extractBadTarFileSymlink() {
+ File destination = new File(temp + "/destination");
+ destination.mkdir();
+ Path link = createSymlinkOrSkipTest(destination.toPath().resolve("link"), destination.toPath());
+ Assertions.assertThrows(ArchiveExtractionException.class, () -> extractor.extract(BAD_TAR, link.toString()));
+ }
+
+ private Path createSymlinkOrSkipTest(Path link, Path target) {
+ try {
+ return Files.createSymbolicLink(link, target);
+ } catch (UnsupportedOperationException | IOException e) {
+ assumeTrue(false, "symlinks not supported");
+ return null;
+ }
+ }
+}
diff --git a/frontend-plugin-core/src/test/java/com/github/eirslett/maven/plugins/frontend/lib/DefaultNpmRunnerTest.java b/frontend-plugin-core/src/test/java/com/github/eirslett/maven/plugins/frontend/lib/DefaultNpmRunnerTest.java
new file mode 100644
index 000000000..3f3dc6dd8
--- /dev/null
+++ b/frontend-plugin-core/src/test/java/com/github/eirslett/maven/plugins/frontend/lib/DefaultNpmRunnerTest.java
@@ -0,0 +1,90 @@
+package com.github.eirslett.maven.plugins.frontend.lib;
+
+import org.hamcrest.CoreMatchers;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+public class DefaultNpmRunnerTest {
+
+ private final String id = "id";
+ private final String protocol = "http";
+ private final String host = "localhost";
+ private final int port = 8888;
+ private final String username = "someusername";
+ private final String password = "somepassword";
+ private final String nonProxyHosts = "www.google.ca|www.google.com|*.google.de";
+ private final String[] expectedNonProxyHosts = new String[] {"www.google.ca","www.google.com",".google.de"};
+ private final String registryUrl = "www.npm.org";
+ private final String expectedUrl = "http://someusername:somepassword@localhost:8888";
+
+ @Test
+ public void buildArguments_basicTest() {
+ List strings = runBuildArguments(nonProxyHosts, registryUrl);
+
+ assertThat(strings, CoreMatchers.hasItem("--proxy=" + expectedUrl));
+ assertThat(strings, CoreMatchers.hasItem("--https-proxy=" + expectedUrl));
+ for (String expectedNonProxyHost: expectedNonProxyHosts) {
+ assertThat(strings, CoreMatchers.hasItem("--noproxy=" + expectedNonProxyHost));
+ }
+ assertThat(strings, CoreMatchers.hasItem("--registry=" + registryUrl));
+ assertEquals(6, strings.size());
+ }
+
+
+ @Test
+ public void buildArguments_emptyRegistryUrl() {
+ List strings = runBuildArguments(nonProxyHosts, "");
+
+ assertThat(strings, CoreMatchers.hasItem("--proxy=" + expectedUrl));
+ assertThat(strings, CoreMatchers.hasItem("--https-proxy=" + expectedUrl));
+ for (String expectedNonProxyHost: expectedNonProxyHosts) {
+ assertThat(strings, CoreMatchers.hasItem("--noproxy=" + expectedNonProxyHost));
+ }
+ assertEquals(5, strings.size());
+ }
+
+ @Test
+ public void buildArguments_nullRegistryUrl() {
+ List strings = runBuildArguments(nonProxyHosts, null);
+
+ assertThat(strings, CoreMatchers.hasItem("--proxy=" + expectedUrl));
+ assertThat(strings, CoreMatchers.hasItem("--https-proxy=" + expectedUrl));
+ for (String expectedNonProxyHost: expectedNonProxyHosts) {
+ assertThat(strings, CoreMatchers.hasItem("--noproxy=" + expectedNonProxyHost));
+ }
+ assertEquals(5, strings.size());
+ }
+
+ @Test
+ public void buildArguments_emptyNoProxy() {
+ List strings = runBuildArguments("", "");
+
+ assertThat(strings, CoreMatchers.hasItem("--proxy=" + expectedUrl));
+ assertThat(strings, CoreMatchers.hasItem("--https-proxy=" + expectedUrl));
+ assertEquals(2, strings.size());
+ }
+
+ @Test
+ public void buildArguments_nullNoProxy() {
+ List strings = runBuildArguments(null, "");
+
+ assertThat(strings, CoreMatchers.hasItem("--proxy=" + expectedUrl));
+ assertThat(strings, CoreMatchers.hasItem("--https-proxy=" + expectedUrl));
+ assertEquals(2, strings.size());
+ }
+
+ private List runBuildArguments(String nonProxyHost, String registryUrl) {
+ List proxyList = Stream.of(
+ new ProxyConfig.Proxy(id, protocol, host, port, username, password, nonProxyHost)
+ ).collect(Collectors.toList());
+
+ return DefaultNpmRunner.buildArguments(new ProxyConfig(proxyList), registryUrl);
+ }
+}
diff --git a/frontend-plugin-core/src/test/java/com/github/eirslett/maven/plugins/frontend/lib/DefaultNpxRunnerTest.java b/frontend-plugin-core/src/test/java/com/github/eirslett/maven/plugins/frontend/lib/DefaultNpxRunnerTest.java
new file mode 100644
index 000000000..d1618024b
--- /dev/null
+++ b/frontend-plugin-core/src/test/java/com/github/eirslett/maven/plugins/frontend/lib/DefaultNpxRunnerTest.java
@@ -0,0 +1,29 @@
+package com.github.eirslett.maven.plugins.frontend.lib;
+
+
+import java.util.Collections;
+import java.util.List;
+
+import org.hamcrest.CoreMatchers;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class DefaultNpxRunnerTest {
+
+ private final String registryUrl = "www.npm.org";
+
+ @Test
+ public void buildArgument_basicTest() {
+ List arguments = DefaultNpxRunner.buildNpmArguments(new ProxyConfig(Collections.emptyList()), null);
+ Assertions.assertEquals(0, arguments.size());
+ }
+
+ @Test
+ public void buildArgument_withRegistryUrl() {
+ List arguments = DefaultNpxRunner.buildNpmArguments(new ProxyConfig(Collections.emptyList()), registryUrl);
+ Assertions.assertEquals(2, arguments.size());
+ assertThat(arguments, CoreMatchers.hasItems("--", "--registry=" + registryUrl));
+ }
+}
diff --git a/frontend-plugin-core/src/test/java/com/github/eirslett/maven/plugins/frontend/lib/PlatformTest.java b/frontend-plugin-core/src/test/java/com/github/eirslett/maven/plugins/frontend/lib/PlatformTest.java
new file mode 100644
index 000000000..4df9f0411
--- /dev/null
+++ b/frontend-plugin-core/src/test/java/com/github/eirslett/maven/plugins/frontend/lib/PlatformTest.java
@@ -0,0 +1,65 @@
+package com.github.eirslett.maven.plugins.frontend.lib;
+
+import org.junit.jupiter.api.Test;
+import java.util.function.Supplier;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class PlatformTest {
+
+ private static final String NODE_VERSION_8 = "v8.17.2";
+ private static final String NODE_VERSION_15 = "v15.14.0";
+ private static final String NODE_VERSION_16 = "v16.1.0";
+
+ @Test
+ public void detect_win_doesntLookForAlpine() {
+ // Throw if called
+ Supplier checkForAlpine = () -> {
+ throw new RuntimeException("Shouldn't be called");
+ };
+
+ Platform platform = Platform.guess(OS.Windows, Architecture.x86, () -> false);
+ assertEquals("win-x86", platform.getNodeClassifier(NODE_VERSION_15));
+ }
+
+ @Test
+ public void detect_arm_mac_download_x64_binary_node15() {
+ Platform platform = Platform.guess(OS.Mac, Architecture.arm64, () -> false);
+ assertEquals("darwin-x64", platform.getNodeClassifier(NODE_VERSION_15));
+ }
+
+ @Test
+ public void detect_arm_mac_download_x64_binary_node16() {
+ Platform platform = Platform.guess(OS.Mac, Architecture.arm64, () -> false);
+ assertEquals("darwin-arm64", platform.getNodeClassifier(NODE_VERSION_16));
+ }
+
+ @Test
+ public void detect_linux_notAlpine() throws Exception {
+ Platform platform = Platform.guess(OS.Linux, Architecture.x86, () -> false);
+ assertEquals("linux-x86", platform.getNodeClassifier(NODE_VERSION_15));
+ assertEquals("https://nodejs.org/dist/", platform.getNodeDownloadRoot());
+ }
+
+ @Test
+ public void detect_linux_alpine() throws Exception {
+ Platform platform = Platform.guess(OS.Linux, Architecture.x86, () -> true);
+ assertEquals("linux-x86-musl", platform.getNodeClassifier(NODE_VERSION_15));
+ assertEquals("https://unofficial-builds.nodejs.org/download/release/",
+ platform.getNodeDownloadRoot());
+ }
+
+ @Test
+ public void detect_aix_ppc64() {
+ Platform platform = Platform.guess(OS.AIX, Architecture.ppc64, () -> false);
+ assertEquals("aix-ppc64", platform.getNodeClassifier(NODE_VERSION_15));
+ }
+
+ @Test
+ public void getNodeMajorVersion() {
+ assertEquals(Integer.valueOf(8), Platform.getNodeMajorVersion(NODE_VERSION_8));
+ assertEquals(Integer.valueOf(15), Platform.getNodeMajorVersion(NODE_VERSION_15));
+ assertEquals(Integer.valueOf(16), Platform.getNodeMajorVersion(NODE_VERSION_16));
+ }
+
+}
diff --git a/frontend-plugin-core/src/test/java/com/github/eirslett/maven/plugins/frontend/lib/UtilsTest.java b/frontend-plugin-core/src/test/java/com/github/eirslett/maven/plugins/frontend/lib/UtilsTest.java
new file mode 100644
index 000000000..c3a4b4100
--- /dev/null
+++ b/frontend-plugin-core/src/test/java/com/github/eirslett/maven/plugins/frontend/lib/UtilsTest.java
@@ -0,0 +1,52 @@
+package com.github.eirslett.maven.plugins.frontend.lib;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static java.util.Collections.singletonList;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class UtilsTest {
+
+ @Test
+ public void testImplode() {
+ final String separator = "Bar";
+ final List elements = new ArrayList<>();
+
+ assertEquals("", Utils.implode(separator, elements));
+
+ elements.add("foo");
+ elements.add("bar");
+ assertEquals("foo bar", Utils.implode(separator, elements));
+ }
+
+ @Test
+ public void testIsRelative() {
+ assertTrue(Utils.isRelative("foo/bar"));
+
+ assertFalse(Utils.isRelative("/foo/bar"));
+ assertFalse(Utils.isRelative("file:foo/bar"));
+ assertFalse(Utils.isRelative("C:\\SYSTEM"));
+ }
+
+ @Test
+ public void testMerge() {
+ assertEquals(
+ Arrays.asList("foo", "bar"),
+ Utils.merge(singletonList("foo"), singletonList("bar"))
+ );
+ }
+
+ @Test
+ public void testPrepend() {
+ assertEquals(
+ Arrays.asList("foo", "bar"),
+ Utils.prepend("foo", singletonList("bar"))
+ );
+ }
+}
diff --git a/frontend-plugin-core/src/test/resources/bad.tgz b/frontend-plugin-core/src/test/resources/bad.tgz
new file mode 100644
index 000000000..f287c1aae
Binary files /dev/null and b/frontend-plugin-core/src/test/resources/bad.tgz differ
diff --git a/frontend-plugin-core/src/test/resources/good.tgz b/frontend-plugin-core/src/test/resources/good.tgz
new file mode 100644
index 000000000..6d6153afa
Binary files /dev/null and b/frontend-plugin-core/src/test/resources/good.tgz differ
diff --git a/pom.xml b/pom.xml
index d14e44a13..1705c3d2d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,19 +1,16 @@
4.0.0
-
- org.sonatype.oss
- oss-parent
- 7
-
-
com.github.eirslettfrontend-plugins
- 1.7-SNAPSHOT
+ 1.13.4-SNAPSHOTpomUTF-8
+ 1.8
+ 1.8
+ 1.8Frontend Plugins
@@ -37,6 +34,7 @@
https://github.com/eirslett/frontend-maven-pluginscm:git:https://github.com/eirslett/frontend-maven-plugin.gitscm:git:git@github.com:eirslett/frontend-maven-plugin.git
+ HEAD
@@ -52,19 +50,137 @@
frontend-maven-plugin
+
+
+ ossrh
+ https://oss.sonatype.org/content/repositories/snapshots
+
+
+ ossrh
+ https://oss.sonatype.org/service/local/staging/deploy/maven2/
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-release-plugin
+ 3.0.1
+
+ true
+ true
+ release
+ master
+ deploy
+
+
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+ 3.1.1
+
+ true
+
+
+
+ org.sonatype.plugins
+ nexus-staging-maven-plugin
+ 1.6.13
+
+
+ default-deploy
+ deploy
+
+
+ deploy
+
+
+
+
+ https://oss.sonatype.org/
+ ossrh
+ true
+
+
+ org.apache.maven.pluginsmaven-compiler-plugin
- 3.1
+ 3.11.0
-
- 1.7
+
+ 1.8
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.3.0
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.5.0
+
+
+
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+
+
+ release
+
+ gpg
+ ${env.GPG_KEYNAME}
+ ${env.GPG_PASSPHRASE}
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.1.0
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+
+ --pinentry-mode
+ loopback
+
+
+
+
+
+
+
diff --git a/settings-github.xml b/settings-github.xml
new file mode 100644
index 000000000..84cb7a36e
--- /dev/null
+++ b/settings-github.xml
@@ -0,0 +1,9 @@
+
+
+
+ ossrh
+ ${env.OSSRH_JIRA_USERNAME}
+ ${env.OSSRH_JIRA_PASSWORD}
+
+
+