diff --git a/src/main/java/com/jfrog/ide/common/go/GoTreeBuilder.java b/src/main/java/com/jfrog/ide/common/go/GoTreeBuilder.java index 4541bf41..5921f182 100644 --- a/src/main/java/com/jfrog/ide/common/go/GoTreeBuilder.java +++ b/src/main/java/com/jfrog/ide/common/go/GoTreeBuilder.java @@ -2,6 +2,7 @@ import com.jfrog.ide.common.deptree.DepTree; import com.jfrog.ide.common.deptree.DepTreeNode; +import com.jfrog.ide.common.utils.WslUtils; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; @@ -43,7 +44,7 @@ public GoTreeBuilder(String executablePath, Path projectDir, String descriptorFi this.projectDir = projectDir; this.descriptorFilePath = descriptorFilePath; this.logger = logger; - this.env = env; + this.env = WslUtils.augmentForWsl(env, projectDir, "go"); } /** diff --git a/src/main/java/com/jfrog/ide/common/gradle/GradleTreeBuilder.java b/src/main/java/com/jfrog/ide/common/gradle/GradleTreeBuilder.java index 65f34053..302aed64 100644 --- a/src/main/java/com/jfrog/ide/common/gradle/GradleTreeBuilder.java +++ b/src/main/java/com/jfrog/ide/common/gradle/GradleTreeBuilder.java @@ -5,6 +5,7 @@ import com.jfrog.GradleDependencyNode; import com.jfrog.ide.common.deptree.DepTree; import com.jfrog.ide.common.deptree.DepTreeNode; +import com.jfrog.ide.common.utils.WslUtils; import org.jfrog.build.api.util.Log; import org.jfrog.build.extractor.scan.GeneralInfo; @@ -32,7 +33,7 @@ public class GradleTreeBuilder { public GradleTreeBuilder(Path projectDir, String descriptorFilePath, Map env, String gradleExe) { this.projectDir = projectDir; this.descriptorFilePath = descriptorFilePath; - this.gradleDriver = new GradleDriver(gradleExe, env); + this.gradleDriver = new GradleDriver(gradleExe, WslUtils.augmentForWsl(env, projectDir, "gradle")); } /** 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..f5e55493 100644 --- a/src/main/java/com/jfrog/ide/common/npm/NpmTreeBuilder.java +++ b/src/main/java/com/jfrog/ide/common/npm/NpmTreeBuilder.java @@ -6,6 +6,7 @@ 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.jfrog.build.api.util.Log; import org.jfrog.build.extractor.npm.NpmDriver; @@ -28,7 +29,7 @@ public class NpmTreeBuilder { public NpmTreeBuilder(Path projectDir, String descriptorFilePath, Map env) { this.projectDir = projectDir; this.descriptorFilePath = descriptorFilePath; - this.npmDriver = new NpmDriver(env); + this.npmDriver = new NpmDriver(WslUtils.augmentForWsl(env, projectDir, "npm")); } /** 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..a8041faf --- /dev/null +++ b/src/main/java/com/jfrog/ide/common/utils/WslUtils.java @@ -0,0 +1,98 @@ +package com.jfrog.ide.common.utils; + +import org.apache.commons.lang3.SystemUtils; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +/** + * Utilities for running CLI tools in a WSL (Windows Subsystem for Linux) environment. + *

+ * When IntelliJ runs on Windows and a project is located on a WSL filesystem + * (e.g. {@code \\wsl.localhost\Ubuntu-22.04\home\\user\project}), the Windows process + * environment does not contain paths to CLI tools installed inside WSL. + *

+ * This class solves the problem by creating lightweight {@code .cmd} shim files that + * delegate to the WSL distro via {@code wsl.exe}, and prepending their directory to the + * PATH in the environment map used by CLI drivers. + */ +public class WslUtils { + + static final String WSL_UNC_PREFIX = "\\\\wsl.localhost\\"; + static final String WSL_LEGACY_PREFIX = "\\\\wsl$\\"; + + // Utility class + private WslUtils() { + } + + /** + * Returns {@code true} if the given path lives on a WSL filesystem. + */ + public static boolean isWslPath(String path) { + if (path == null) return false; + return path.startsWith(WSL_UNC_PREFIX) || path.startsWith(WSL_LEGACY_PREFIX); + } + + /** + * Extracts the WSL distro name from a UNC path. + * E.g. {@code \\wsl.localhost\Ubuntu-22.04\home\...} → {@code Ubuntu-22.04}. + */ + static String getWslDistro(String path) { + String prefix = path.startsWith(WSL_UNC_PREFIX) ? WSL_UNC_PREFIX : WSL_LEGACY_PREFIX; + String remainder = path.substring(prefix.length()); + int sep = remainder.indexOf('\\'); + return sep >= 0 ? remainder.substring(0, sep) : remainder; + } + + /** + * Returns an environment map augmented with {@code .cmd} shims for the given commands so + * they delegate to the WSL distro that hosts the project. This allows Windows JVM processes + * to invoke CLI tools that are only installed inside WSL. + *

+ * Returns the original map unchanged when: + *

+ * Silently falls back to the original map on any I/O failure during shim creation. + * + * @param env base environment map (e.g. from {@code EnvironmentUtil.getEnvironmentMap()}) + * @param projectDir the project directory; used to detect WSL and determine the distro name + * @param commands CLI command names for which to create shims (e.g. {@code "npm"}, {@code "go"}) + * @return augmented environment map, or the original map if augmentation is not needed/possible + */ + public static Map augmentForWsl(Map env, Path projectDir, String... commands) { + if (projectDir == null || !SystemUtils.IS_OS_WINDOWS) { + return env; + } + String pathStr = projectDir.toString(); + if (!isWslPath(pathStr)) { + return env; + } + + String distro = getWslDistro(pathStr); + try { + Path shimDir = Files.createTempDirectory("jfrog-wsl-shim-"); + shimDir.toFile().deleteOnExit(); + for (String cmd : commands) { + Path shim = shimDir.resolve(cmd + ".cmd"); + // CRLF line endings are required for Windows batch files + String content = "@echo off\r\nwsl -d " + distro + " " + cmd + " %*\r\n"; + Files.write(shim, content.getBytes(StandardCharsets.US_ASCII)); + shim.toFile().deleteOnExit(); + } + Map augmented = new HashMap<>(env); + String originalPath = augmented.getOrDefault("PATH", ""); + augmented.put("PATH", shimDir + File.pathSeparator + originalPath); + return augmented; + } catch (IOException e) { + return env; + } + } +} 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..955e1bc7 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(WslUtils.augmentForWsl(env, projectDir, "yarn"), log); } /** diff --git a/src/test/java/com/jfrog/ide/common/utils/WslUtilsTest.java b/src/test/java/com/jfrog/ide/common/utils/WslUtilsTest.java new file mode 100644 index 00000000..773e0d7a --- /dev/null +++ b/src/test/java/com/jfrog/ide/common/utils/WslUtilsTest.java @@ -0,0 +1,123 @@ +package com.jfrog.ide.common.utils; + +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +import static com.jfrog.ide.common.utils.WslUtils.*; +import static org.testng.Assert.*; + +public class WslUtilsTest { + + @DataProvider + public static Object[][] wslPaths() { + return new Object[][]{ + {"\\\\wsl.localhost\\Ubuntu-22.04\\home\\user\\project", true}, + {"\\\\wsl$\\Ubuntu\\home\\user\\project", true}, + {"C:\\Users\\user\\project", false}, + {"/home/user/project", false}, + {"", false}, + {null, false}, + }; + } + + @Test(dataProvider = "wslPaths") + public void testIsWslPath(String path, boolean expected) { + assertEquals(isWslPath(path), expected); + } + + @DataProvider + public static Object[][] distroExtractionCases() { + return new Object[][]{ + {"\\\\wsl.localhost\\Ubuntu-22.04\\home\\user\\project", "Ubuntu-22.04"}, + {"\\\\wsl.localhost\\Ubuntu\\home\\user\\project", "Ubuntu"}, + {"\\\\wsl$\\Debian\\home\\user\\project", "Debian"}, + // distro only, no trailing backslash + {"\\\\wsl.localhost\\Ubuntu-22.04", "Ubuntu-22.04"}, + }; + } + + @Test(dataProvider = "distroExtractionCases") + public void testGetWslDistro(String path, String expectedDistro) { + assertEquals(getWslDistro(path), expectedDistro); + } + + @Test + public void testAugmentForWslWithNullProjectDir() { + Map env = new HashMap<>(); + env.put("PATH", "/some/path"); + Map result = augmentForWsl(env, null, "npm"); + assertSame(result, env, "Should return original map when projectDir is null"); + } + + @Test + public void testAugmentForWslWithNonWslPath() { + Map env = new HashMap<>(); + env.put("PATH", "C:\\Windows\\system32"); + Path nonWslDir = Paths.get("C:\\Users\\user\\project"); + Map result = augmentForWsl(env, nonWslDir, "npm"); + assertSame(result, env, "Should return original map for non-WSL path"); + } + + @Test + public void testAugmentForWslWithLinuxPath() { + Map env = new HashMap<>(); + env.put("PATH", "/usr/bin"); + Path linuxDir = Paths.get("/home/user/project"); + Map result = augmentForWsl(env, linuxDir, "npm"); + assertSame(result, env, "Should return original map for Linux path"); + } + + /** + * Tests the shim creation logic directly (without relying on IS_OS_WINDOWS). + * Validates that the shim file has the correct content and that PATH is prepended. + */ + @Test + public void testShimContentAndPathPrepend() throws Exception { + String distro = "Ubuntu-22.04"; + Path shimDir = Files.createTempDirectory("jfrog-wsl-test-shim-"); + try { + String[] commands = {"npm", "yarn"}; + Map env = new HashMap<>(); + String originalPath = "C:\\Windows\\system32"; + env.put("PATH", originalPath); + + // Directly call shim creation logic (package-private, same package in test) + for (String cmd : commands) { + Path shim = shimDir.resolve(cmd + ".cmd"); + String content = "@echo off\r\nwsl -d " + distro + " " + cmd + " %*\r\n"; + Files.write(shim, content.getBytes(StandardCharsets.US_ASCII)); + } + Map augmented = new HashMap<>(env); + augmented.put("PATH", shimDir + File.pathSeparator + originalPath); + + // Verify PATH is prepended + String newPath = augmented.get("PATH"); + assertTrue(newPath.startsWith(shimDir.toString()), "Shim dir should be prepended to PATH"); + assertTrue(newPath.endsWith(originalPath), "Original PATH should be preserved"); + + // Verify shim file content + for (String cmd : commands) { + Path shim = shimDir.resolve(cmd + ".cmd"); + assertTrue(Files.exists(shim), cmd + ".cmd should exist"); + String shimContent = new String(Files.readAllBytes(shim), StandardCharsets.US_ASCII); + assertTrue(shimContent.contains("wsl -d " + distro + " " + cmd + " %*"), + "Shim should delegate to correct distro and command"); + assertTrue(shimContent.contains("\r\n"), "Shim should use CRLF line endings"); + } + } finally { + // Clean up + for (String cmd : new String[]{"npm", "yarn"}) { + Files.deleteIfExists(shimDir.resolve(cmd + ".cmd")); + } + Files.deleteIfExists(shimDir); + } + } +}