Skip to content

Commit

Permalink
feat: we can now create junctions on Windows (#1804)
Browse files Browse the repository at this point in the history
* feat: we can now create junctions on Windows

Fixes #1793

* chore: made variable names clearer

* fix: fixed setting default JDK on Windows

On Windows it was possible to leave a JDK in a broken state by changing the default JDK

* chore: remove Dev Mode check from jbang script

Checking for Developer Mode when it's only necessary for the `edit` command will only bother users, best to only give the warning when actually needed

* docs: refined special Windows setup instructions
  • Loading branch information
quintesse authored Sep 12, 2024
1 parent 75b4657 commit 5e986ab
Show file tree
Hide file tree
Showing 9 changed files with 85 additions and 101 deletions.
7 changes: 5 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ plugins {
id 'maven-publish'
}

//remove this to see all the missing tags/parameters.
javadoc.options.addStringOption('Xdoclint:none', '-quiet')
javadoc {
options.encoding = 'UTF-8'
//remove this to see all the missing tags/parameters.
options.addStringOption('Xdoclint:none', '-quiet')
}

repositories {
mavenCentral()
Expand Down
4 changes: 0 additions & 4 deletions docs/modules/ROOT/pages/javaversions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,6 @@ You can change the default JDK by running:

Running it without an argument will return the version of the JDK that is currently set as the default.

NOTE: On Windows you might need elevated privileges to create symbolic links. If you don't have permissions then
running the above command will result in an error. To use it https://stackoverflow.com/a/24353758[enable symbolic links]
for your user or run your shell/terminal as administrator to have this feature working.

When you `uninstall` a JDK by running:

jbang jdk uninstall 12
Expand Down
7 changes: 3 additions & 4 deletions docs/modules/ROOT/pages/usage.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -348,15 +348,15 @@ No one would want to do that (right?) but now you know.
== Usage on Windows

Some JBang commands need to create symbolic links when running on Windows.
For example, this is required for Managing JDKs or editing the files with the `edit` command.
For example, this is required for editing the files with the `edit` command.

If you encounter issues on Windows related to the creation of symbolic links follow
these instructions:

1. From Windows 10 onwards you can turn on "Developer Mode", this will automatically
enable the possibility to create symbolic links. Read here how to enable this mode:
https://learn.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development[Enable your device for development]. On Windows 11 this might already
be enabled by default.
https://learn.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development[Enable your device for development].
On Windows 11 this might already be enabled by default.

2. If you're using a Java version equal to or newer than 13 then you're good to go.
This Java version already works correctly. Make sure that JBang is actually using
Expand All @@ -369,4 +369,3 @@ is no other option than setting the correct privileges for your user by enablin
the `Create symbolic links` group policy setting. See the instruction on this page
for more information on how to do this:
https://superuser.com/a/105381[Permission to make symbolic links in Windows].

20 changes: 10 additions & 10 deletions src/main/java/dev/jbang/cli/Edit.java
Original file line number Diff line number Diff line change
Expand Up @@ -411,32 +411,32 @@ Path createProjectForLinkedEdit(Project prj, List<String> arguments, boolean rel
Path srcDir = tmpProjectDir.resolve("src");
Util.mkdirs(srcDir);

Path srcFile = srcDir.resolve(name);
Util.createLink(srcFile, originalFile);
Path link = srcDir.resolve(name);
Util.createLink(link, originalFile);

for (ResourceRef sourceRef : prj.getMainSourceSet().getSources()) {
Path sfile;
Path linkFile;
Source src = Source.forResourceRef(sourceRef, Function.identity());
if (src.getJavaPackage().isPresent()) {
Path packageDir = srcDir.resolve(src.getJavaPackage().get().replace(".", File.separator));
Util.mkdirs(packageDir);
sfile = packageDir.resolve(sourceRef.getFile().getFileName());
linkFile = packageDir.resolve(sourceRef.getFile().getFileName());
} else {
sfile = srcDir.resolve(sourceRef.getFile().getFileName());
linkFile = srcDir.resolve(sourceRef.getFile().getFileName());
}
Path destFile = sourceRef.getFile().toAbsolutePath();
Util.createLink(sfile, destFile);
Util.createLink(linkFile, destFile);
}

for (RefTarget ref : prj.getMainSourceSet().getResources()) {
Path target = ref.to(srcDir);
Util.mkdirs(target.getParent());
Util.createLink(target, ref.getSource().getFile().toAbsolutePath());
Path linkFile = ref.to(srcDir);
Util.mkdirs(linkFile.getParent());
Util.createLink(linkFile, ref.getSource().getFile().toAbsolutePath());
}

// create build gradle
Optional<String> packageName = Util.getSourcePackage(
new String(Files.readAllBytes(srcFile), Charset.defaultCharset()));
new String(Files.readAllBytes(link), Charset.defaultCharset()));
String baseName = Util.getBaseName(name);
String fullClassName;
fullClassName = packageName.map(s -> s + "." + baseName).orElse(baseName);
Expand Down
8 changes: 2 additions & 6 deletions src/main/java/dev/jbang/cli/Jdk.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.*;
import java.util.stream.Collectors;

import com.google.gson.Gson;
Expand Down Expand Up @@ -235,7 +231,7 @@ public Integer defaultJdk(
JdkProvider.Jdk defjdk = JdkManager.getDefaultJdk();
if (versionOrId != null) {
JdkProvider.Jdk jdk = JdkManager.getOrInstallJdk(versionOrId);
if (!jdk.equals(defjdk)) {
if (defjdk == null || (!jdk.equals(defjdk) && !Objects.equals(jdk.getHome(), defjdk.getHome()))) {
JdkManager.setDefaultJdk(jdk);
} else {
Util.infoMsg("Default JDK already set to " + defjdk.getMajorVersion());
Expand Down
44 changes: 27 additions & 17 deletions src/main/java/dev/jbang/net/JdkManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static dev.jbang.cli.BaseCommand.EXIT_UNEXPECTED_STATE;

import java.io.IOException;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
Expand Down Expand Up @@ -416,11 +417,11 @@ public static void uninstallJdk(JdkProvider.Jdk jdk) {
* @param version requested version to link.
*/
public static void linkToExistingJdk(String path, int version) {
Path jdkPath = JBangJdkProvider.getJdksPath().resolve(Integer.toString(version));
Util.verboseMsg("Trying to link " + path + " to " + jdkPath);
if (Files.exists(jdkPath) || Files.isSymbolicLink(jdkPath)) {
Path linkPath = JBangJdkProvider.getJdksPath().resolve(Integer.toString(version));
Util.verboseMsg("Trying to link " + path + " to " + linkPath);
if (Files.exists(linkPath) || Files.isSymbolicLink(linkPath)) {
Util.verboseMsg("JBang managed JDK already exists, must be deleted to make sure linking works");
Util.deletePath(jdkPath, false);
Util.deletePath(linkPath, false);
}
Path linkedJdkPath = Paths.get(path);
if (!Files.isDirectory(linkedJdkPath)) {
Expand All @@ -430,8 +431,8 @@ public static void linkToExistingJdk(String path, int version) {
if (ver.isPresent()) {
Integer linkedJdkVersion = ver.get();
if (linkedJdkVersion == version) {
Util.mkdirs(jdkPath.getParent());
Util.createLink(jdkPath, linkedJdkPath);
Util.mkdirs(linkPath.getParent());
Util.createLink(linkPath, linkedJdkPath);
Util.infoMsg("JDK " + version + " has been linked to: " + linkedJdkPath);
} else {
throw new ExitException(EXIT_INVALID_INPUT, "Java version in given path: " + path
Expand Down Expand Up @@ -497,22 +498,31 @@ public static JdkProvider.Jdk getDefaultJdk() {
public static void setDefaultJdk(JdkProvider.Jdk jdk) {
JdkProvider.Jdk defJdk = getDefaultJdk();
if (jdk.isInstalled() && !jdk.equals(defJdk)) {
removeDefaultJdk();
Util.createLink(getDefaultJdkPath(), jdk.getHome());
Util.infoMsg("Default JDK set to " + jdk);
Path defaultJdk = getDefaultJdkPath();
Path newDefaultJdk = defaultJdk.getParent().resolve(defaultJdk.getFileName() + ".new");
Util.createLink(newDefaultJdk, jdk.getHome());
removeJdk(defaultJdk);
try {
Files.move(newDefaultJdk, defaultJdk);
Util.infoMsg("Default JDK set to " + jdk);
} catch (IOException e) {
// Ignore
}
}
}

public static void removeDefaultJdk() {
Path link = getDefaultJdkPath();
if (Files.isSymbolicLink(link)) {
try {
Files.deleteIfExists(link);
} catch (IOException e) {
// Ignore
}
} else {
Util.deletePath(link, true);
removeJdk(link);
}

private static void removeJdk(Path jdkPath) {
try {
Files.deleteIfExists(jdkPath);
} catch (DirectoryNotEmptyException e) {
Util.deletePath(jdkPath, true);
} catch (IOException e) {
// Ignore
}
}

Expand Down
68 changes: 28 additions & 40 deletions src/main/java/dev/jbang/util/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,7 @@
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.AccessDeniedException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.security.MessageDigest;
Expand Down Expand Up @@ -1464,8 +1455,8 @@ public static boolean deletePath(Path path, boolean quiet) {
} else if (Files.exists(path)) {
verboseMsg("Deleting file " + path);
Files.delete(path);
} else if (Files.isSymbolicLink(path)) {
Util.verboseMsg("Deleting broken symbolic link " + path);
} else if (Files.exists(path, LinkOption.NOFOLLOW_LINKS)) {
Util.verboseMsg("Deleting broken link " + path);
Files.delete(path);
}
} catch (IOException e) {
Expand All @@ -1477,52 +1468,49 @@ public static boolean deletePath(Path path, boolean quiet) {
return err[0] == null;
}

public static void createLink(Path src, Path target) {
if (!Files.exists(src) && !createSymbolicLink(src, target.toAbsolutePath())) {
if (getOS() != OS.windows || !Files.isDirectory(src)) {
infoMsg("Now try creating a hard link instead of symbolic.");
if (createHardLink(src, target.toAbsolutePath())) {
public static void createLink(Path link, Path target) {
if (!Files.exists(link)) {
// On Windows we use junction for directories because their
// creation doesn't require any special privileges.
if (getOS() == OS.windows && Files.isDirectory(target)) {
if (createJunction(link, target.toAbsolutePath())) {
return;
}
} else {
if (createSymbolicLink(link, target.toAbsolutePath())) {
return;
}
}
throw new ExitException(BaseCommand.EXIT_GENERIC_ERROR, "Failed to create link " + src + " -> " + target);
throw new ExitException(BaseCommand.EXIT_GENERIC_ERROR, "Failed to create link " + link + " -> " + target);
}
}

private static boolean createSymbolicLink(Path src, Path target) {
private static boolean createSymbolicLink(Path link, Path target) {
try {
Files.createSymbolicLink(src, target);
Files.createSymbolicLink(link, target);
return true;
} catch (IOException e) {
infoMsg(String.format("Creation of symbolic link failed %s -> %s", src, target));
if (isWindows() && e instanceof AccessDeniedException && e.getMessage().contains("privilege")
&& JavaUtil.getCurrentMajorJavaVersion() < 13) {
if (isWindows() && e instanceof AccessDeniedException && e.getMessage().contains("privilege")) {
infoMsg(String.format("Creation of symbolic link failed %s -> %s", link, target));
infoMsg("This is a known issue with trying to create symbolic links on Windows.");
infoMsg("Either use a Java version equal to or newer than 13 and make sure that");
infoMsg("it is in your PATH (check by running 'java -version`) or if no Java is");
infoMsg("available on the PATH use 'jbang jdk default <version>'.");
infoMsg("The other solution is to change the privileges for your user, see:");
infoMsg("See the information available at the link below for a solution:");
infoMsg("https://www.jbang.dev/documentation/guide/latest/usage.html#usage-on-windows");
}
verboseMsg(e.toString());
}
return false;
}

private static boolean createHardLink(Path src, Path target) {
try {
if (getOS() == OS.windows && Files.isDirectory(src)) {
warnMsg(String.format("Creation of hard links to folders is not supported on Windows %s -> %s", src,
target));
return false;
}
Files.createLink(src, target);
return true;
} catch (IOException e) {
verboseMsg(e.toString());
private static boolean createJunction(Path link, Path target) {
if (!Files.exists(link) && Files.exists(link, LinkOption.NOFOLLOW_LINKS)) {
// We automatically remove broken links
deletePath(link, true);
}
infoMsg(String.format("Creation of hard link failed %s -> %s", src, target));
return false;
return runCommand("cmd.exe", "/c", "mklink", "/j", link.toString(), target.toString()) != null;
}

public static boolean isLink(Path path) throws IOException {
return !path.toAbsolutePath().equals(path.toRealPath());
}

public static Path getUrlCacheDir(String fileURL) {
Expand Down
9 changes: 0 additions & 9 deletions src/main/scripts/jbang.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,6 @@ if ([System.Enum]::GetNames([System.Net.SecurityProtocolType]) -notcontains 'Tls
break
}

$DevModRegistryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
if (!(Test-Path -Path $DevModRegistryPath) -or (Get-ItemProperty -Path `
$DevModRegistryPath -Name AllowDevelopmentWithoutDevLicense -ErrorAction `
SilentlyContinue).AllowDevelopmentWithoutDevLicense -ne 1) {
[Console]::Error.WriteLine("WARNING: Windows Developer Mode is not enabled on your system, this is necessary");
[Console]::Error.WriteLine("for JBang to be able to function correctly, see this page for more information:");
[Console]::Error.WriteLine("https://www.jbang.dev/documentation/guide/latest/usage.html#usage-on-windows");
}

# The Java version to install when it's not installed on the system yet
if (-not (Test-Path env:JBANG_DEFAULT_JAVA_VERSION)) { $javaVersion='17' } else { $javaVersion=$env:JBANG_DEFAULT_JAVA_VERSION }

Expand Down
19 changes: 10 additions & 9 deletions src/test/java/dev/jbang/cli/TestJdk.java
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,9 @@ void testJdkInstallWithLinkingToExistingJdkPathWhenJBangManagedVersionDoesNotExi
assertThat(result.result, equalTo(SUCCESS_EXIT));
assertThat(result.normalizedErr(),
equalTo("[jbang] JDK 11 has been linked to: " + javaDir.toPath().toString() + "\n"));
assertTrue(Files.isSymbolicLink(jdkPath.resolve("11")));
assertEquals(javaDir.toPath(), Files.readSymbolicLink(jdkPath.resolve("11")));
assertTrue(Util.isLink(jdkPath.resolve("11")));
System.err.println("ASSERT: " + javaDir.toPath() + " - " + jdkPath.resolve("11").toRealPath());
assertTrue(Files.isSameFile(javaDir.toPath(), jdkPath.resolve("11").toRealPath()));
}

@Test
Expand All @@ -292,8 +293,8 @@ void testJdkInstallWithLinkingToExistingJdkPathWhenJBangManagedVersionExistsAndI
assertThat(result.result, equalTo(SUCCESS_EXIT));
assertThat(result.normalizedErr(),
equalTo("[jbang] JDK 11 has been linked to: " + javaDir.toPath().toString() + "\n"));
assertTrue(Files.isSymbolicLink(jdkPath.resolve("11")));
assertEquals(javaDir.toPath(), Files.readSymbolicLink(jdkPath.resolve("11")));
assertTrue(Util.isLink(jdkPath.resolve("11")));
assertTrue(Files.isSameFile(javaDir.toPath(), jdkPath.resolve("11").toRealPath()));
}

@Test
Expand Down Expand Up @@ -362,8 +363,8 @@ void testJdkInstallWithLinkingToExistingBrokenLink(
assertThat(result.result, equalTo(SUCCESS_EXIT));
assertThat(result.normalizedErr(),
equalTo("[jbang] JDK 11 has been linked to: " + jdkOk + "\n"));
assertTrue(Files.isSymbolicLink(jdkPath.resolve("11")));
assertEquals(jdkOk, Files.readSymbolicLink(jdkPath.resolve("11")));
assertTrue(Util.isLink(jdkPath.resolve("11")));
assertTrue(Files.isSameFile(jdkOk, (jdkPath.resolve("11").toRealPath())));
}

@Test
Expand Down Expand Up @@ -435,9 +436,9 @@ private void createMockJdkRuntime(int jdkVersion) {
private void createMockJdk(int jdkVersion, BiConsumer<Path, String> init) {
Path jdkPath = JBangJdkProvider.getJdksPath().resolve(String.valueOf(jdkVersion));
init.accept(jdkPath, jdkVersion + ".0.7");
Path def = Settings.getCurrentJdkDir();
if (!Files.exists(def)) {
Util.createLink(def, jdkPath);
Path link = Settings.getCurrentJdkDir();
if (!Files.exists(link)) {
Util.createLink(link, jdkPath);
}
}

Expand Down

0 comments on commit 5e986ab

Please sign in to comment.