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/docs/modules/ROOT/pages/javaversions.adoc b/docs/modules/ROOT/pages/javaversions.adoc index 4d58be002..93ba6a591 100644 --- a/docs/modules/ROOT/pages/javaversions.adoc +++ b/docs/modules/ROOT/pages/javaversions.adoc @@ -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 diff --git a/docs/modules/ROOT/pages/usage.adoc b/docs/modules/ROOT/pages/usage.adoc index cd3a29075..d60b876bd 100644 --- a/docs/modules/ROOT/pages/usage.adoc +++ b/docs/modules/ROOT/pages/usage.adoc @@ -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 @@ -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]. - diff --git a/src/main/java/dev/jbang/cli/Edit.java b/src/main/java/dev/jbang/cli/Edit.java index 87cbd03f5..eb0a924eb 100644 --- a/src/main/java/dev/jbang/cli/Edit.java +++ b/src/main/java/dev/jbang/cli/Edit.java @@ -411,32 +411,32 @@ Path createProjectForLinkedEdit(Project prj, List 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 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); diff --git a/src/main/java/dev/jbang/cli/Jdk.java b/src/main/java/dev/jbang/cli/Jdk.java index 3d307d54a..7c26c63ef 100644 --- a/src/main/java/dev/jbang/cli/Jdk.java +++ b/src/main/java/dev/jbang/cli/Jdk.java @@ -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; @@ -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()); diff --git a/src/main/java/dev/jbang/net/JdkManager.java b/src/main/java/dev/jbang/net/JdkManager.java index 0102888b5..e4175f98f 100644 --- a/src/main/java/dev/jbang/net/JdkManager.java +++ b/src/main/java/dev/jbang/net/JdkManager.java @@ -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; @@ -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)) { @@ -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 @@ -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 } } 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/main/scripts/jbang.ps1 b/src/main/scripts/jbang.ps1 index 9fd7d935b..d8ffec9d0 100644 --- a/src/main/scripts/jbang.ps1 +++ b/src/main/scripts/jbang.ps1 @@ -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 } diff --git a/src/test/java/dev/jbang/cli/TestJdk.java b/src/test/java/dev/jbang/cli/TestJdk.java index b2efc58df..058e8009c 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 @@ -435,9 +436,9 @@ private void createMockJdkRuntime(int jdkVersion) { private void createMockJdk(int jdkVersion, BiConsumer 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); } }