diff --git a/src/main/java/com/jfrog/ide/common/npm/NpmComponentUpdater.java b/src/main/java/com/jfrog/ide/common/npm/NpmComponentUpdater.java index 2e96ddf5..393b6ca1 100644 --- a/src/main/java/com/jfrog/ide/common/npm/NpmComponentUpdater.java +++ b/src/main/java/com/jfrog/ide/common/npm/NpmComponentUpdater.java @@ -1,12 +1,18 @@ package com.jfrog.ide.common.npm; import com.jfrog.ide.common.updateversion.ComponentUpdater; +import com.jfrog.ide.common.utils.WslUtils; +import org.apache.commons.lang3.StringUtils; import org.jfrog.build.api.util.Log; +import org.jfrog.build.extractor.executor.CommandExecutor; +import org.jfrog.build.extractor.executor.CommandResults; import org.jfrog.build.extractor.npm.NpmDriver; import java.io.IOException; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Map; /** @@ -19,10 +25,33 @@ public class NpmComponentUpdater extends ComponentUpdater { public static final String NPM_VERSION_DELIMITER = "@"; private final NpmDriver npmDriver; + private final CommandExecutor wslExecutor; + private final boolean isWsl; public NpmComponentUpdater(Path projectDir, Log logger, Map env) { super(projectDir, logger); - this.npmDriver = new NpmDriver(env); + this.isWsl = WslUtils.isWslPath(projectDir); + if (isWsl) { + this.npmDriver = null; + this.wslExecutor = new CommandExecutor("wsl.exe", env); + } else { + this.npmDriver = new NpmDriver(env); + this.wslExecutor = null; + } + } + + /** + * Prefix arguments for {@code wsl.exe} so npm runs in the project directory inside WSL + * (aligned with {@link NpmTreeBuilder}). + */ + private List wslNpmInvocationPrefix() { + String linuxPath = WslUtils.toLinuxPath(projectDir.toString()); + List args = new ArrayList<>(); + args.add("--cd"); + args.add(linuxPath); + args.add("--exec"); + args.add("npm"); + return args; } /** @@ -35,7 +64,21 @@ public NpmComponentUpdater(Path projectDir, Log logger, Map env) @Override public void run(String componentName, String componentVersion) throws IOException { super.run(componentName, componentVersion); - npmDriver.install(projectDir.toFile(), Collections.singletonList(this.componentFullName), this.logger); + if (isWsl) { + List args = new ArrayList<>(wslNpmInvocationPrefix()); + args.add("install"); + args.add(this.componentFullName); + try { + CommandResults res = wslExecutor.exeCommand(null, args, null, logger); + if (!res.isOk()) { + throw new IOException(StringUtils.defaultString(res.getErr()) + StringUtils.defaultString(res.getRes())); + } + } catch (IOException | InterruptedException e) { + throw new IOException("npm install failed via WSL", e); + } + } else { + npmDriver.install(projectDir.toFile(), Collections.singletonList(this.componentFullName), this.logger); + } } @Override @@ -45,6 +88,16 @@ public String getVersionDelimiter() { @Override public boolean isDriverInstalled() { + if (isWsl) { + try { + List args = new ArrayList<>(wslNpmInvocationPrefix()); + args.add("--version"); + CommandResults results = wslExecutor.exeCommand(null, args, null, null); + return results.isOk() && !StringUtils.isBlank(results.getRes()); + } catch (Exception e) { + return false; + } + } return npmDriver.isNpmInstalled(); } diff --git a/src/main/java/com/jfrog/ide/common/npm/NpmTreeBuilder.java b/src/main/java/com/jfrog/ide/common/npm/NpmTreeBuilder.java index 1bf16923..ea2bbf53 100644 --- a/src/main/java/com/jfrog/ide/common/npm/NpmTreeBuilder.java +++ b/src/main/java/com/jfrog/ide/common/npm/NpmTreeBuilder.java @@ -2,16 +2,24 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.collect.Lists; import com.jfrog.ide.common.deptree.DepTree; import com.jfrog.ide.common.deptree.DepTreeNode; import com.jfrog.ide.common.utils.Utils; +import com.jfrog.ide.common.utils.WslUtils; +import org.apache.commons.lang3.StringUtils; import org.jfrog.build.api.util.Log; +import org.jfrog.build.extractor.executor.CommandExecutor; +import org.jfrog.build.extractor.executor.CommandResults; import org.jfrog.build.extractor.npm.NpmDriver; import java.io.IOException; import java.nio.file.Path; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -21,14 +29,24 @@ */ public class NpmTreeBuilder { private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final ObjectReader jsonReader = objectMapper.reader(); private final NpmDriver npmDriver; + private final CommandExecutor wslExecutor; + private final boolean isWsl; private final Path projectDir; private final String descriptorFilePath; public NpmTreeBuilder(Path projectDir, String descriptorFilePath, Map env) { this.projectDir = projectDir; this.descriptorFilePath = descriptorFilePath; - this.npmDriver = new NpmDriver(env); + this.isWsl = WslUtils.isWslPath(projectDir); + if (isWsl) { + this.npmDriver = null; + this.wslExecutor = new CommandExecutor("wsl.exe", env); + } else { + this.npmDriver = new NpmDriver(env); + this.wslExecutor = null; + } } /** @@ -39,15 +57,15 @@ public NpmTreeBuilder(Path projectDir, String descriptorFilePath, Map nodes = new HashMap<>(); String packageId = getPackageId(prodResults); addDepTreeNodes(nodes, prodResults, packageId, "prod"); @@ -57,6 +75,64 @@ public DepTree buildTree(Log logger) throws IOException { return tree; } + /** + * Check whether npm is available, accounting for WSL projects. + * For WSL projects, npm is invoked through {@code wsl.exe}. + */ + private boolean isNpmInstalled() { + if (isWsl) { + try { + List args = wslNpmInvocationPrefix(); + args.add("--version"); + CommandResults results = wslExecutor.exeCommand(null, args, null, null); + return results.isOk() && !StringUtils.isBlank(results.getRes()); + } catch (Exception e) { + return false; + } + } + return npmDriver.isNpmInstalled(); + } + + /** + * Prefix arguments for {@code wsl.exe} so npm runs in the project directory inside WSL + * (same {@code --cd} context as {@link #npmList}). + */ + private List wslNpmInvocationPrefix() { + String linuxPath = WslUtils.toLinuxPath(projectDir.toString()); + List args = new ArrayList<>(); + args.add("--cd"); + args.add(linuxPath); + args.add("--exec"); + args.add("npm"); + return args; + } + + /** + * Run {@code npm ls} and return the parsed JSON output. + * For WSL projects, the command is routed through {@code wsl.exe --cd --exec npm ...}. + */ + private JsonNode npmList(List extraArgs) throws IOException { + if (isWsl) { + List args = new ArrayList<>(wslNpmInvocationPrefix()); + args.add("ls"); + args.add("--json"); + args.add("--all"); + args.addAll(extraArgs); + try { + CommandResults commandRes = wslExecutor.exeCommand(null, args, null, null); + String res = StringUtils.isBlank(commandRes.getRes()) ? "{}" : commandRes.getRes(); + JsonNode results = jsonReader.readTree(res); + if (!commandRes.isOk() && !results.has("problems") && results.isObject()) { + ((ObjectNode) results).put("problems", commandRes.getErr()); + } + return results; + } catch (IOException | InterruptedException e) { + throw new IOException("npm ls failed via WSL", e); + } + } + return npmDriver.list(projectDir.toFile(), extraArgs); + } + private void addDepTreeNodes(Map nodes, JsonNode jsonDep, String depId, String scope) { DepTreeNode depNode; if (nodes.containsKey(depId)) { diff --git a/src/main/java/com/jfrog/ide/common/utils/WslUtils.java b/src/main/java/com/jfrog/ide/common/utils/WslUtils.java new file mode 100644 index 00000000..46eb505e --- /dev/null +++ b/src/main/java/com/jfrog/ide/common/utils/WslUtils.java @@ -0,0 +1,87 @@ +package com.jfrog.ide.common.utils; + +import java.nio.file.Path; + +/** + * Utility methods for WSL (Windows Subsystem for Linux) path handling. + * On Windows, WSL filesystems are exposed as UNC paths under the wsl.localhost or wsl$ hosts. + */ +public class WslUtils { + // UNC-style prefixes used by Windows to expose WSL filesystems + private static final String WSL_LOCALHOST_PREFIX = "\\\\wsl.localhost\\"; + private static final String WSL_DOLLAR_PREFIX = "\\\\wsl$\\"; + + private WslUtils() { + } + + private static boolean startsWithIgnoreCase(String s, String prefix) { + return s.length() >= prefix.length() && s.regionMatches(true, 0, prefix, 0, prefix.length()); + } + + /** + * Normalizes Windows extended-length UNC prefixes so WSL detection sees {@code \\wsl$\...} / {@code \\wsl.localhost\...}. + * Example: {@code \\?\UNC\wsl$\Ubuntu\home\...} becomes {@code \\wsl$\Ubuntu\home\...}. + */ + static String normalizePathStringForWsl(String path) { + if (path == null || path.isEmpty()) { + return path; + } + String p = path; + if (startsWithIgnoreCase(p, "\\\\?\\UNC\\")) { + return "\\\\" + p.substring("\\\\?\\UNC\\".length()); + } + if (startsWithIgnoreCase(p, "\\\\?\\")) { + return p.substring("\\\\?\\".length()); + } + return p; + } + + /** + * Returns true if the given path string refers to a WSL filesystem + * (i.e. it is a UNC path rooted at the wsl.localhost or wsl$ host). + */ + public static boolean isWslPath(String path) { + if (path == null) { + return false; + } + String normalized = normalizePathStringForWsl(path); + return startsWithIgnoreCase(normalized, WSL_LOCALHOST_PREFIX) || startsWithIgnoreCase(normalized, WSL_DOLLAR_PREFIX); + } + + /** + * Returns true if the given {@link Path} refers to a WSL filesystem. + */ + public static boolean isWslPath(Path path) { + return path != null && isWslPath(path.toString()); + } + + /** + * Converts a Windows-style WSL UNC path to the equivalent Linux path inside WSL. + * The distro name (first UNC component after the host) is stripped, and + * backslashes are replaced with forward slashes. + * + * @param wslWindowsPath the Windows-style WSL path + * @return the Linux path, or the original string unchanged if it is not a WSL path + */ + public static String toLinuxPath(String wslWindowsPath) { + if (wslWindowsPath == null) { + return null; + } + String p = normalizePathStringForWsl(wslWindowsPath); + String withoutPrefix; + if (startsWithIgnoreCase(p, WSL_LOCALHOST_PREFIX)) { + withoutPrefix = p.substring(WSL_LOCALHOST_PREFIX.length()); + } else if (startsWithIgnoreCase(p, WSL_DOLLAR_PREFIX)) { + withoutPrefix = p.substring(WSL_DOLLAR_PREFIX.length()); + } else { + return wslWindowsPath; + } + // withoutPrefix is now: \ + // Strip the distro name (first path component) to obtain the Linux path. + int firstBackslash = withoutPrefix.indexOf('\\'); + if (firstBackslash == -1) { + return "/"; // Path pointed at the distro root + } + return withoutPrefix.substring(firstBackslash).replace('\\', '/'); + } +} diff --git a/src/main/java/com/jfrog/ide/common/yarn/YarnComponentUpdater.java b/src/main/java/com/jfrog/ide/common/yarn/YarnComponentUpdater.java index 0bb13f54..84ecce87 100644 --- a/src/main/java/com/jfrog/ide/common/yarn/YarnComponentUpdater.java +++ b/src/main/java/com/jfrog/ide/common/yarn/YarnComponentUpdater.java @@ -1,6 +1,7 @@ package com.jfrog.ide.common.yarn; import com.jfrog.ide.common.updateversion.ComponentUpdater; +import com.jfrog.ide.common.utils.WslUtils; import org.jfrog.build.api.util.Log; import java.io.IOException; @@ -14,7 +15,7 @@ public class YarnComponentUpdater extends ComponentUpdater { public YarnComponentUpdater(Path projectDir, Log logger, Map env) { super(projectDir, logger); - this.yarnDriver = new YarnDriver(env); + this.yarnDriver = new YarnDriver(env, logger, WslUtils.isWslPath(projectDir)); } /** @@ -37,7 +38,7 @@ public String getVersionDelimiter() { @Override public boolean isDriverInstalled() { - return yarnDriver.isYarnInstalled(); + return yarnDriver.isYarnInstalled(projectDir.toFile()); } @Override diff --git a/src/main/java/com/jfrog/ide/common/yarn/YarnDriver.java b/src/main/java/com/jfrog/ide/common/yarn/YarnDriver.java index d2d71d0d..c7114b1a 100644 --- a/src/main/java/com/jfrog/ide/common/yarn/YarnDriver.java +++ b/src/main/java/com/jfrog/ide/common/yarn/YarnDriver.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; +import com.jfrog.ide.common.utils.WslUtils; import org.apache.commons.lang3.StringUtils; import org.jfrog.build.api.util.Log; import org.jfrog.build.api.util.NullLog; @@ -12,7 +13,6 @@ import java.io.File; import java.io.IOException; import java.util.*; -import java.util.stream.Collectors; import java.util.stream.Stream; /** @@ -22,20 +22,41 @@ public class YarnDriver { private static final ObjectReader jsonReader = new ObjectMapper().reader(); private final CommandExecutor commandExecutor; private final Log log; + private final boolean useWsl; public YarnDriver(Map env) { - this(env, new NullLog()); + this(env, new NullLog(), false); } public YarnDriver(Map env, Log log) { - this.commandExecutor = new CommandExecutor("yarn", env); + this(env, log, false); + } + + public YarnDriver(Map env, Log log, boolean useWsl) { + this.useWsl = useWsl; + this.commandExecutor = useWsl ? new CommandExecutor("wsl.exe", env) : new CommandExecutor("yarn", env); this.log = log; } + /** + * @return whether Yarn commands are executed via {@code wsl.exe} (WSL UNC project path). + */ + public boolean runsThroughWsl() { + return useWsl; + } + @SuppressWarnings("unused") public boolean isYarnInstalled() { + return isYarnInstalled(null); + } + + /** + * @param projectWorkingDirectory project root, or {@code null} for a global Yarn check (non-WSL only; + * WSL mode should pass the project directory so the check uses the same {@code --cd} as scans). + */ + public boolean isYarnInstalled(File projectWorkingDirectory) { try { - return !version(null).isEmpty(); + return !version(projectWorkingDirectory).isEmpty(); } catch (IOException | InterruptedException e) { return false; } @@ -128,8 +149,20 @@ private CommandResults runCommand(File workingDirectory, String[] args) throws I } private CommandResults runCommand(File workingDirectory, String[] args, List extraArgs) throws IOException, InterruptedException { - List finalArgs = Stream.concat(Arrays.stream(args), extraArgs.stream()).collect(Collectors.toList()); - CommandResults commandRes = commandExecutor.exeCommand(workingDirectory, finalArgs, null, null); + List finalArgs = new ArrayList<>(); + File wdForExecutor = workingDirectory; + if (useWsl) { + // Route through wsl.exe. If a working directory is given, convert it to a Linux path via --cd. + if (workingDirectory != null) { + finalArgs.add("--cd"); + finalArgs.add(WslUtils.toLinuxPath(workingDirectory.getPath())); + } + finalArgs.add("--exec"); + finalArgs.add("yarn"); + wdForExecutor = null; // Working directory is handled by --cd above + } + Stream.concat(Arrays.stream(args), extraArgs.stream()).forEach(finalArgs::add); + CommandResults commandRes = commandExecutor.exeCommand(wdForExecutor, finalArgs, null, null); if (!commandRes.isOk()) { throw new IOException(commandRes.getErr() + commandRes.getRes()); } diff --git a/src/main/java/com/jfrog/ide/common/yarn/YarnTreeBuilder.java b/src/main/java/com/jfrog/ide/common/yarn/YarnTreeBuilder.java index 5736991a..89c1a7d8 100644 --- a/src/main/java/com/jfrog/ide/common/yarn/YarnTreeBuilder.java +++ b/src/main/java/com/jfrog/ide/common/yarn/YarnTreeBuilder.java @@ -5,6 +5,7 @@ import com.jfrog.ide.common.deptree.DepTree; import com.jfrog.ide.common.deptree.DepTreeNode; import com.jfrog.ide.common.nodes.subentities.ImpactTree; +import com.jfrog.ide.common.utils.WslUtils; import org.apache.commons.lang3.StringUtils; import org.jfrog.build.api.util.Log; @@ -29,7 +30,7 @@ public class YarnTreeBuilder { public YarnTreeBuilder(Path projectDir, String descriptorFilePath, Map env, Log log) { this.projectDir = projectDir; this.descriptorFilePath = descriptorFilePath; - this.yarnDriver = new YarnDriver(env, log); + this.yarnDriver = new YarnDriver(env, log, WslUtils.isWslPath(projectDir)); } /** @@ -39,8 +40,8 @@ public YarnTreeBuilder(Path projectDir, String descriptorFilePath, Map