Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/main/java/com/jfrog/ide/common/go/GoTreeBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -32,7 +33,7 @@ public class GradleTreeBuilder {
public GradleTreeBuilder(Path projectDir, String descriptorFilePath, Map<String, String> 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"));
}

/**
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/jfrog/ide/common/npm/NpmTreeBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -28,7 +29,7 @@ public class NpmTreeBuilder {
public NpmTreeBuilder(Path projectDir, String descriptorFilePath, Map<String, String> env) {
this.projectDir = projectDir;
this.descriptorFilePath = descriptorFilePath;
this.npmDriver = new NpmDriver(env);
this.npmDriver = new NpmDriver(WslUtils.augmentForWsl(env, projectDir, "npm"));
}

/**
Expand Down
98 changes: 98 additions & 0 deletions src/main/java/com/jfrog/ide/common/utils/WslUtils.java
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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.
* <p>
* 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.
* <p>
* Returns the original map unchanged when:
* <ul>
* <li>the JVM is not running on Windows, or</li>
* <li>{@code projectDir} is {@code null}, or</li>
* <li>{@code projectDir} is not a WSL path.</li>
* </ul>
* 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<String, String> augmentForWsl(Map<String, String> 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<String, String> augmented = new HashMap<>(env);
String originalPath = augmented.getOrDefault("PATH", "");
augmented.put("PATH", shimDir + File.pathSeparator + originalPath);
return augmented;
} catch (IOException e) {
return env;
}
}
}
3 changes: 2 additions & 1 deletion src/main/java/com/jfrog/ide/common/yarn/YarnTreeBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -29,7 +30,7 @@ public class YarnTreeBuilder {
public YarnTreeBuilder(Path projectDir, String descriptorFilePath, Map<String, String> 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);
}

/**
Expand Down
123 changes: 123 additions & 0 deletions src/test/java/com/jfrog/ide/common/utils/WslUtilsTest.java
Original file line number Diff line number Diff line change
@@ -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<String, String> env = new HashMap<>();
env.put("PATH", "/some/path");
Map<String, String> result = augmentForWsl(env, null, "npm");
assertSame(result, env, "Should return original map when projectDir is null");
}

@Test
public void testAugmentForWslWithNonWslPath() {
Map<String, String> env = new HashMap<>();
env.put("PATH", "C:\\Windows\\system32");
Path nonWslDir = Paths.get("C:\\Users\\user\\project");
Map<String, String> result = augmentForWsl(env, nonWslDir, "npm");
assertSame(result, env, "Should return original map for non-WSL path");
}

@Test
public void testAugmentForWslWithLinuxPath() {
Map<String, String> env = new HashMap<>();
env.put("PATH", "/usr/bin");
Path linuxDir = Paths.get("/home/user/project");
Map<String, String> 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<String, String> 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<String, String> 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);
}
}
}
Loading