This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 23adb32ea0a7 CAMEL-26749: camel-jbang - doctor: the versions of the 
web installer are one installation, and the active one is found through its 
symlink (#29427)
23adb32ea0a7 is described below

commit 23adb32ea0a71aa8446b85c6d3b50d8dc4fd0f5e
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 9 09:30:41 2026 +0100

    CAMEL-26749: camel-jbang - doctor: the versions of the web installer are 
one installation, and the active one is found through its symlink (#29427)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../camel/dsl/jbang/core/commands/Doctor.java      | 12 +++++--
 .../dsl/jbang/core/common/InstallDetector.java     | 22 +++++++++++--
 .../camel/dsl/jbang/core/commands/DoctorTest.java  | 37 ++++++++++++++++++++++
 .../dsl/jbang/core/common/InstallDetectorTest.java | 28 ++++++++++++++++
 4 files changed, 95 insertions(+), 4 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Doctor.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Doctor.java
index 3af5e81972f7..fecbf04365c8 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Doctor.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Doctor.java
@@ -267,8 +267,16 @@ public class Doctor extends CamelCommand {
     // "more than one installation found -> conflict" contract against fully 
controlled data, rather than
     // whatever InstallDetector.scanKnownLocations() happens to find on the 
machine running the test suite.
     boolean checkInstallLocations(List<InstallDetector.InstallInfo> installs, 
Optional<Path> active) {
-        String found = "Found %d Camel CLI 
installation%s".formatted(installs.size(), installs.size() == 1 ? "" : "s");
-        boolean conflict = installs.size() > 1;
+        // The version directories of the web installer are one installation: 
it keeps the previous versions after an
+        // upgrade, and runs the version its symlink points at.
+        long webVersions = installs.stream()
+                .filter(i -> i.method() == 
InstallDetector.InstallMethod.WEB_INSTALLER).count();
+        long count = installs.size() - Math.max(0, webVersions - 1);
+        String found = "Found %d Camel CLI installation%s".formatted(count, 
count == 1 ? "" : "s");
+        if (webVersions > 1) {
+            found += " (%d versions of the web 
installer)".formatted(webVersions);
+        }
+        boolean conflict = count > 1;
         checks.add(new Check("installs", conflict ? FAIL : OK, found));
         if (jsonOutput) {
             return conflict;
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/InstallDetector.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/InstallDetector.java
index 1d13aadcb87b..be4b6e5159ce 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/InstallDetector.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/InstallDetector.java
@@ -233,7 +233,8 @@ public final class InstallDetector {
     }
 
     /**
-     * Walks {@code PATH} the way a shell would and returns the first {@code 
camel} executable found.
+     * Walks {@code PATH} the way a shell would and returns the first {@code 
camel} executable found, with its symbolic
+     * links followed (the web installer puts a symlink to {@code 
versions/<version>/bin/camel.sh} on {@code PATH}).
      */
     public static Optional<Path> resolveActiveOnPath() {
         String path = System.getenv("PATH");
@@ -251,9 +252,26 @@ public final class InstallDetector {
             }
             Path candidate = Paths.get(dir, exeName);
             if (Files.exists(candidate)) {
-                return Optional.of(candidate);
+                return Optional.of(followLinks(candidate));
             }
         }
         return Optional.empty();
     }
+
+    // Follows the symbolic links of the file itself (a chain of them, at most 
16), so the active install is the
+    // version directory the link points into. The parent directories are not 
resolved (unlike toRealPath, which turns
+    // macOS /var into /private/var), as the scanned install locations are not 
resolved either.
+    static Path followLinks(Path path) {
+        Path current = path;
+        for (int i = 0; i < 16 && Files.isSymbolicLink(current); i++) {
+            try {
+                Path target = Files.readSymbolicLink(current);
+                Path parent = current.toAbsolutePath().getParent();
+                current = (parent != null ? parent.resolve(target) : 
target).normalize();
+            } catch (IOException | UnsupportedOperationException | 
SecurityException e) {
+                break;
+            }
+        }
+        return current;
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DoctorTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DoctorTest.java
index 273db13b450f..35f27a82ff0f 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DoctorTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DoctorTest.java
@@ -296,6 +296,43 @@ class DoctorTest extends CamelCommandBaseTestSupport {
                 .contains("Warning: more than one Camel CLI installation was 
found");
     }
 
+    @Test
+    void versionsOfTheWebInstallerAreOneInstall() {
+        Path versions = Path.of("/home/user/.local/share/camel-cli/versions");
+        List<InstallDetector.InstallInfo> installs = List.of(
+                new InstallDetector.InstallInfo(versions.resolve("4.23.0"), 
InstallDetector.InstallMethod.WEB_INSTALLER),
+                new InstallDetector.InstallInfo(versions.resolve("4.22.2"), 
InstallDetector.InstallMethod.WEB_INSTALLER));
+        Doctor command = new Doctor(new CamelJBangMain().withPrinter(printer));
+
+        boolean conflict = command.checkInstallLocations(installs, 
Optional.of(versions.resolve("4.23.0/bin/camel.sh")));
+
+        assertThat(conflict).isFalse();
+        assertThat(command.getChecks()).singleElement()
+                .satisfies(c -> 
assertThat(c.status()).isNotEqualTo(Doctor.FAIL));
+        assertThat(printer.getOutput())
+                .contains("Found 1 Camel CLI installation (2 versions of the 
web installer)")
+                .contains("4.23.0 (WEB_INSTALLER) <- active")
+                .doesNotContain("4.22.2 (WEB_INSTALLER) <- active")
+                .doesNotContain("Warning: more than one");
+    }
+
+    @Test
+    void versionsOfTheWebInstallerNextToAnotherInstallFail() {
+        Path versions = Path.of("/home/user/.local/share/camel-cli/versions");
+        List<InstallDetector.InstallInfo> installs = List.of(
+                new InstallDetector.InstallInfo(versions.resolve("4.23.0"), 
InstallDetector.InstallMethod.WEB_INSTALLER),
+                new InstallDetector.InstallInfo(versions.resolve("4.22.2"), 
InstallDetector.InstallMethod.WEB_INSTALLER),
+                new InstallDetector.InstallInfo(
+                        
Path.of("/opt/homebrew/Cellar/apache-camel/4.21.0/libexec"),
+                        InstallDetector.InstallMethod.HOMEBREW));
+        Doctor command = new Doctor(new CamelJBangMain().withPrinter(printer));
+
+        boolean conflict = command.checkInstallLocations(installs, 
Optional.empty());
+
+        assertThat(conflict).isTrue();
+        assertThat(printer.getOutput()).contains("Found 2 Camel CLI 
installations (2 versions of the web installer)");
+    }
+
     @Test
     void reportsNoConflictWhenSingleInstallFound() {
         Path location = 
Path.of("/home/user/.local/share/camel-cli/versions/4.22.0");
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/InstallDetectorTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/InstallDetectorTest.java
index 172ee66904dc..8e14e3e2cd01 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/InstallDetectorTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/InstallDetectorTest.java
@@ -22,6 +22,8 @@ import java.nio.file.Path;
 
 import org.apache.camel.util.FileUtil;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
 import org.junit.jupiter.api.io.TempDir;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -155,6 +157,32 @@ class InstallDetectorTest {
         assertThat(result).contains(exe);
     }
 
+    @Test
+    @DisabledOnOs(OS.WINDOWS)
+    void resolveActiveOnPathFollowsTheSymlinkOfTheWebInstaller(@TempDir Path 
temp) throws Exception {
+        Path camelSh = 
Files.createDirectories(temp.resolve("share/camel-cli/versions/4.23.0/bin")).resolve("camel.sh");
+        Files.createFile(camelSh);
+        Path binDir = Files.createDirectories(temp.resolve("bin"));
+        // a relative link, as the web installer writes, and a second link to 
that one in another PATH entry
+        Files.createSymbolicLink(binDir.resolve("camel"), 
Path.of("../share/camel-cli/versions/4.23.0/bin/camel.sh"));
+        Path otherBin = Files.createDirectories(temp.resolve("other-bin"));
+        Files.createSymbolicLink(otherBin.resolve("camel"), 
binDir.resolve("camel"));
+
+        
assertThat(InstallDetector.resolveActiveOnPath(binDir.toString())).contains(camelSh);
+        
assertThat(InstallDetector.resolveActiveOnPath(otherBin.toString())).contains(camelSh);
+    }
+
+    @Test
+    @DisabledOnOs(OS.WINDOWS)
+    void followLinksStopsAtALinkCycle(@TempDir Path temp) throws Exception {
+        Path a = temp.resolve("a");
+        Path b = temp.resolve("b");
+        Files.createSymbolicLink(a, b);
+        Files.createSymbolicLink(b, a);
+
+        assertThat(InstallDetector.followLinks(a)).isIn(a, b);
+    }
+
     @Test
     void resolveActiveOnPathReturnsEmptyWhenNotFound() {
         var result = InstallDetector.resolveActiveOnPath("/nonexistent/dir-a" 
+ File.pathSeparator + "/nonexistent/dir-b");

Reply via email to