From 6699ba7d15b2acd31a09ef646b2ce5493a271687 Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Tue, 28 May 2024 21:41:43 +0200 Subject: [PATCH] feat: we can now create junctions on Windows Fixes #1793 --- build.gradle | 7 ++- src/main/java/dev/jbang/util/Util.java | 68 ++++++++++-------------- src/test/java/dev/jbang/cli/TestJdk.java | 13 ++--- 3 files changed, 40 insertions(+), 48 deletions(-) diff --git a/build.gradle b/build.gradle index 42bf2f492..057828faf 100644 --- a/build.gradle +++ b/build.gradle @@ -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() diff --git a/src/main/java/dev/jbang/util/Util.java b/src/main/java/dev/jbang/util/Util.java index 27dee43d3..c0de67a72 100644 --- a/src/main/java/dev/jbang/util/Util.java +++ b/src/main/java/dev/jbang/util/Util.java @@ -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; @@ -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) { @@ -1477,31 +1468,32 @@ 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 '."); - 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()); @@ -1509,20 +1501,16 @@ private static boolean createSymbolicLink(Path src, Path target) { 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) { diff --git a/src/test/java/dev/jbang/cli/TestJdk.java b/src/test/java/dev/jbang/cli/TestJdk.java index b2efc58df..be13cc40f 100644 --- a/src/test/java/dev/jbang/cli/TestJdk.java +++ b/src/test/java/dev/jbang/cli/TestJdk.java @@ -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 @@ -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 @@ -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