Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: we can now create junctions on Windows #1804

Merged
merged 5 commits into from
Sep 12, 2024
Merged
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
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 @@ -419,11 +420,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 @@ -433,8 +434,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 @@ -500,22 +501,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
Loading