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");