diff --git a/.github/workflows/package-native-validation.yml b/.github/workflows/package-native-validation.yml index 4027b4b6c79a0..27de46e58c2d4 100644 --- a/.github/workflows/package-native-validation.yml +++ b/.github/workflows/package-native-validation.yml @@ -41,6 +41,8 @@ concurrency: permissions: contents: read +# Use the project Maven wrapper throughout: runner-provided Maven versions can differ +# from the version supported by this branch's build extensions. jobs: # ── macOS / Linux (POSIX validators) ────────────────────────────── posix-validator: @@ -81,6 +83,16 @@ jobs: java-version: '21' cache: 'maven' + # Packaging scripts invoke mvn themselves, so pin their PATH entry as well as + # the direct workflow commands. Keep the snapshot profile active in subprocesses. + - name: Pin Maven for packaging subprocesses + shell: bash + run: | + CAMEL_CI_MAVEN_HOME=$(./mvnw -q -B -P apache-snapshots -ntp -N org.apache.maven.plugins:maven-help-plugin:3.5.1:evaluate \ + -Dexpression=maven.home -DforceStdout) + echo "$CAMEL_CI_MAVEN_HOME/bin" >> "$GITHUB_PATH" + echo "MAVEN_ARGS=${MAVEN_ARGS:-} -P apache-snapshots" >> "$GITHUB_ENV" + # camel-repackager-maven-plugin builds the launcher's self-executing JAR. It is not in the # -pl list below, so without this step Maven resolves it from the Apache snapshot repository. # This workflow only triggers on dsl/camel-jbang/camel-launcher and tooling/camel-exe changes @@ -88,7 +100,7 @@ jobs: - name: Install camel-repackager-maven-plugin (build-time plugin, unresolvable from this reactor pass) shell: bash run: | - mvn -B -P apache-snapshots -ntp \ + ./mvnw -B -P apache-snapshots -ntp \ -pl tooling/maven/camel-repackager-maven-plugin install -DskipTests - name: Build camel-launcher distribution @@ -98,14 +110,14 @@ jobs: # published snapshot instead of being rebuilt from source, which is what previously turned # this into a ~626-module reactor build. -P apache-snapshots is required for that resolution. run: | - mvn -B -P apache-snapshots -ntp \ + ./mvnw -B -P apache-snapshots -ntp \ -pl buildingtools,dsl/camel-jbang/camel-launcher install -DskipTests - name: Stage a synthetic release version for offline validation shell: bash run: | cd dsl/camel-jbang/camel-launcher - REAL_VERSION=$(mvn -q -B -ntp org.apache.maven.plugins:maven-help-plugin:3.5.1:evaluate \ + REAL_VERSION=$(../../../mvnw -q -B -P apache-snapshots -ntp org.apache.maven.plugins:maven-help-plugin:3.5.1:evaluate \ -Dexpression=project.version -DforceStdout) TEST_VERSION="${REAL_VERSION%-SNAPSHOT}" echo "TEST_VERSION=$TEST_VERSION" >> "$GITHUB_ENV" @@ -142,14 +154,16 @@ jobs: # WebsiteInstallTest's PosixShell suite only gets targeted CI coverage today as a side # effect of the generic incremental-build check noticing a direct change under # dsl/camel-jbang/camel-launcher; a PR that only touches an upstream dependency wouldn't - # trigger it. Run it here explicitly so this dedicated validation workflow covers it too. + # trigger it. Run it here explicitly, together with the POSIX launcher script tests. run: | cd dsl/camel-jbang/camel-launcher - mvn -B -ntp test -pl . -Dtest=PackageNativeValidationTest,WebsiteInstallTest + ../../../mvnw -B -P apache-snapshots -ntp test -pl . -Dtest=PackageNativeValidationTest,WebsiteInstallTest,CamelShDiscoveryTest,FakeJavaTest - # ── Windows (install.ps1 unit tests) ────────────────────────────── + # ── Windows (installer and launcher script unit tests) ────────────────────────────── windows-validator: runs-on: windows-latest + # Caps a genuine hang well below GitHub's 6h default; a passing run has taken up to ~25 minutes. + timeout-minutes: 60 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -169,7 +183,7 @@ jobs: - name: Install camel-repackager-maven-plugin (build-time plugin, unresolvable from this reactor pass) shell: bash run: | - mvn -B -P apache-snapshots -ntp \ + ./mvnw -B -P apache-snapshots -ntp \ -pl tooling/maven/camel-repackager-maven-plugin install -DskipTests - name: Build camel-launcher module @@ -179,11 +193,25 @@ jobs: # published snapshot instead of being rebuilt from source, which is what previously turned # this into a ~626-module reactor build. -P apache-snapshots is required for that resolution. run: | - mvn -B -P apache-snapshots -ntp \ + ./mvnw -B -P apache-snapshots -ntp \ -pl buildingtools,dsl/camel-jbang/camel-launcher install -DskipTests - - name: Run Windows installer unit tests + - name: Run Windows installer and launcher script unit tests shell: bash + # The PowerShell suite has taken 10-20 minutes on windows-latest, straddling camel-launcher's 1200s + # surefire fork timeout, so slow-but-passing runs were killed. Give this job's fork more room. run: | cd dsl/camel-jbang/camel-launcher - mvn -B -ntp test -pl . -Dtest=WebsiteInstallTest + ../../../mvnw -B -P apache-snapshots -ntp test -pl . -Dtest=WebsiteInstallTest,CamelBatDiscoveryTest,FakeJavaTest -Dcamel.surefire.forkTimeout=2700 + + # Per-test timings for the PowerShell suite, which runs close to camel-launcher's surefire fork timeout. + # A fork killed on that timeout writes no XML report; the per-process timings the fixture prints to the + # build log cover that case. + - name: Archive surefire reports + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: windows-validator-surefire-reports + path: dsl/camel-jbang/camel-launcher/target/surefire-reports + retention-days: 7 + if-no-files-found: ignore diff --git a/dsl/camel-jbang/camel-launcher/src/main/assembly/launcher-content.xml b/dsl/camel-jbang/camel-launcher/src/main/assembly/launcher-content.xml index f648f2f9d0ea0..296373b1c0458 100644 --- a/dsl/camel-jbang/camel-launcher/src/main/assembly/launcher-content.xml +++ b/dsl/camel-jbang/camel-launcher/src/main/assembly/launcher-content.xml @@ -56,6 +56,8 @@ original-*.jar + *-sources.jar + *-javadoc.jar diff --git a/dsl/camel-jbang/camel-launcher/src/main/resources/bin/camel.bat b/dsl/camel-jbang/camel-launcher/src/main/resources/bin/camel.bat index 692dbea4de267..9d485f9a39f99 100644 --- a/dsl/camel-jbang/camel-launcher/src/main/resources/bin/camel.bat +++ b/dsl/camel-jbang/camel-launcher/src/main/resources/bin/camel.bat @@ -49,9 +49,12 @@ set "JAVACMD=%CAMEL_JAVACMD%" @REM Set JVM options if specified if "%JAVA_OPTS%" == "" set JAVA_OPTS=-Xmx512m -@REM Find the launcher JAR +@REM Find the launcher JAR, excluding sources and javadoc set "LAUNCHER_JAR=" -for %%i in ("%BASEDIR%\camel-launcher-*.jar") do set "LAUNCHER_JAR=%%i" +for %%i in ("%BASEDIR%\camel-launcher-*.jar") do ( + echo %%~nxi| findstr /i /e /l /c:"-sources.jar" /c:"-javadoc.jar" > nul + if errorlevel 1 if not defined LAUNCHER_JAR set "LAUNCHER_JAR=%%~fi" +) @REM Execute Camel CLI, preserving arguments and the child exit code for %%e in ("%JAVACMD%") do set "_JAVACMD_EXT=%%~xe" diff --git a/dsl/camel-jbang/camel-launcher/src/main/resources/bin/camel.sh b/dsl/camel-jbang/camel-launcher/src/main/resources/bin/camel.sh index c5f1fb6fd0c79..36a66c77efaba 100644 --- a/dsl/camel-jbang/camel-launcher/src/main/resources/bin/camel.sh +++ b/dsl/camel-jbang/camel-launcher/src/main/resources/bin/camel.sh @@ -100,9 +100,10 @@ then REPO="$BASEDIR" fi -# Find the launcher JAR +# Find the launcher JAR, excluding sources and javadoc LAUNCHER_JAR= for f in "$BASEDIR"/camel-launcher-*.jar; do + case "$f" in *-sources.jar|*-javadoc.jar) continue ;; esac if [ -f "$f" ]; then LAUNCHER_JAR="$f" break diff --git a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelBatDiscoveryTest.java b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelBatDiscoveryTest.java index 4243115762756..03883353c0eb6 100644 --- a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelBatDiscoveryTest.java +++ b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelBatDiscoveryTest.java @@ -76,6 +76,25 @@ private Path currentJavaHome() { return javaHome; } + @Test + void skipsSourcesAndJavadocJars(@TempDir Path base) throws Exception { + Path h = home(Files.createDirectories(base.resolve("launcher with spaces"))); + Files.writeString(h.resolve("camel-launcher-9.9.9-sources.jar"), "sources"); + Files.writeString(h.resolve("camel-launcher-zzz-sources.jar"), "sources"); + Files.writeString(h.resolve("camel-launcher-9.9.9-javadoc.jar"), "javadoc"); + Files.writeString(h.resolve("camel-launcher-zzz-javadoc.jar"), "javadoc"); + Path java = FakeJava.writeFakeJava(base, "java", JAVA17, 0, "JAVACMD"); + Map env = isolatedEnvironment(); + env.put("JAVACMD", java.toString()); + + FakeJava.Result r = FakeJava.run(h.resolve("camel.bat"), env, "hawtio"); + + assertEquals(0, r.exitCode(), r.stderr()); + assertTrue(r.stdout().contains(h.resolve("camel-launcher-9.9.9.jar").toString()), r.stdout()); + assertFalse(r.stdout().contains("-sources.jar"), r.stdout()); + assertFalse(r.stdout().contains("-javadoc.jar"), r.stdout()); + } + @Test void acceptsJava17ViaJavacmd(@TempDir Path base) throws Exception { Path h = home(base); diff --git a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelShDiscoveryTest.java b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelShDiscoveryTest.java index ad2735ed362ce..08fac4adf315d 100644 --- a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelShDiscoveryTest.java +++ b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelShDiscoveryTest.java @@ -56,6 +56,26 @@ private String pathWithNonqualifyingJava(Path base) throws Exception { return pathDir + ":/usr/bin:/bin"; } + @Test + void skipsSourcesAndJavadocJars(@TempDir Path base) throws Exception { + Path h = home(Files.createDirectories(base.resolve("launcher with spaces"))); + Files.writeString(h.resolve("camel-launcher-9.9.9-sources.jar"), "sources"); + Files.writeString(h.resolve("camel-launcher-zzz-sources.jar"), "sources"); + Files.writeString(h.resolve("camel-launcher-9.9.9-javadoc.jar"), "javadoc"); + Files.writeString(h.resolve("camel-launcher-zzz-javadoc.jar"), "javadoc"); + Path java = FakeJava.writeFakeJava(base, "java", JAVA17, 0, "JAVACMD"); + Map env = new HashMap<>(); + env.put("LC_ALL", "C"); + env.put("JAVACMD", java.toString()); + + FakeJava.Result r = FakeJava.run(h.resolve("camel.sh"), env, "hawtio"); + + assertEquals(0, r.exitCode(), r.stderr()); + assertTrue(r.stdout().contains(h.resolve("camel-launcher-9.9.9.jar").toString()), r.stdout()); + assertFalse(r.stdout().contains("-sources.jar"), r.stdout()); + assertFalse(r.stdout().contains("-javadoc.jar"), r.stdout()); + } + @Test void acceptsJava17ViaJavacmd(@TempDir Path base) throws Exception { Path h = home(base); diff --git a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallTest.java b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallTest.java index 0fbda6496e9d0..a514a7c108287 100644 --- a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallTest.java +++ b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallTest.java @@ -34,7 +34,6 @@ import java.util.Map; import java.util.Set; import java.util.UUID; -import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; @@ -152,11 +151,8 @@ private static void assertVersionInstalled(Path home, String version) throws Exc Path versionDir = home.resolve(".local/share/camel-cli/versions/" + version); assertTrue(Files.isDirectory(versionDir), "expected version directory " + versionDir); - Process check = new ProcessBuilder("/bin/sh", shim.toString(), "version") - .redirectErrorStream(true) - .start(); - String output = new String(check.getInputStream().readAllBytes(), StandardCharsets.UTF_8); - check.waitFor(); + String output = WebsiteInstallerFixture.execute( + new ProcessBuilder("/bin/sh", shim.toString(), "version").redirectErrorStream(true)).stdout(); assertTrue(output.contains("Camel " + version), output); } @@ -672,9 +668,8 @@ void restoreUserPath() throws Exception { + "if ($path) { $kind = $k.GetValueKind('Path'); " + "$kept = ($path -split ';') | Where-Object { $_ -and (-not ($dirs -icontains $_)) }; " + "$k.SetValue('Path', ($kept -join ';'), $kind) } $k.Close() }"); - Process cleanup = new ProcessBuilder("powershell", "-NoProfile", "-Command", script.toString()) - .redirectErrorStream(true).start(); - cleanup.waitFor(30, TimeUnit.SECONDS); + WebsiteInstallerFixture.execute( + new ProcessBuilder("powershell", "-NoProfile", "-Command", script.toString()).redirectErrorStream(true)); } private WebsiteInstallerFixture.Result install( @@ -716,11 +711,8 @@ private static void assertVersionInstalled(Path home, String version) throws Exc assertTrue(Files.isDirectory(versionDir(home, version)), "expected version directory " + versionDir(home, version)); - Process check = new ProcessBuilder("cmd.exe", "/c", shim.toString(), "version") - .redirectErrorStream(true) - .start(); - String output = new String(check.getInputStream().readAllBytes(), StandardCharsets.UTF_8); - check.waitFor(30, TimeUnit.SECONDS); + String output = WebsiteInstallerFixture.execute( + new ProcessBuilder("cmd.exe", "/c", shim.toString(), "version").redirectErrorStream(true)).stdout(); assertTrue(output.contains("Camel " + version), output); } @@ -740,13 +732,10 @@ private void assertMaliciousZipRejected(Path temp, MaliciousArchive archiveBuild } private static String queryEnvironmentPath(String scope) throws Exception { - Process p = new ProcessBuilder( + return WebsiteInstallerFixture.execute(new ProcessBuilder( "powershell", "-NoProfile", "-Command", "[Environment]::GetEnvironmentVariable('Path','" + scope + "')") - .redirectErrorStream(true).start(); - String out = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8).trim(); - p.waitFor(30, TimeUnit.SECONDS); - return out; + .redirectErrorStream(true)).stdout().trim(); } private static long countOccurrencesCaseInsensitive(String path, String dir) { @@ -760,15 +749,12 @@ private static long countOccurrencesCaseInsensitive(String path, String dir) { // Reads HKCU\Environment\Path without expanding %VAR% references, returning "" when unset. private static String readUserPathRaw() throws Exception { - Process p = new ProcessBuilder( + return WebsiteInstallerFixture.execute(new ProcessBuilder( "powershell", "-NoProfile", "-Command", "$k = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment'); " + "if ($k) { [Console]::Out.Write($k.GetValue('Path', '', " - + "[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)); $k.Close() }") - .start(); - String out = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8); - p.waitFor(30, TimeUnit.SECONDS); - return out; + + "[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)); $k.Close() }")) + .stdout(); } // Writes HKCU\Environment\Path as a REG_EXPAND_SZ value; the value is passed via the environment @@ -781,21 +767,18 @@ private static void writeUserPathExpandable(String value) throws Exception { + "[Microsoft.Win32.RegistryValueKind]::ExpandString); $k.Close()") .redirectErrorStream(true); pb.environment().put("CAMEL_TEST_USERPATH", value); - Process p = pb.start(); - String out = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8); - if (!p.waitFor(30, TimeUnit.SECONDS) || p.exitValue() != 0) { - throw new IllegalStateException("failed to seed user PATH: " + out); + WebsiteInstallerFixture.Result r = WebsiteInstallerFixture.execute(pb); + if (r.exit() != 0) { + throw new IllegalStateException("failed to seed user PATH: " + r.stdout()); } } private static void deleteUserPath() throws Exception { - Process p = new ProcessBuilder( + WebsiteInstallerFixture.execute(new ProcessBuilder( "powershell", "-NoProfile", "-Command", "$k = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true); " + "if ($k) { $k.DeleteValue('Path', $false); $k.Close() }") - .redirectErrorStream(true).start(); - p.getInputStream().readAllBytes(); - p.waitFor(30, TimeUnit.SECONDS); + .redirectErrorStream(true)); } @Test @@ -1039,14 +1022,11 @@ void forwardsArgumentsAndExitCodeThroughShim(@TempDir Path temp) throws Exceptio // ("bar baz") trips cmd.exe's quote-stripping rule (it only preserves quotes when there are // exactly two), which would unquote the space in "Apache Camel" and break the call. A bare // command name resolved through PATH+PATHEXT avoids that quirk and mirrors real usage. - Process argsProc = shimProcess(binDir, "echo-args", "foo", "bar baz").start(); - String argsOut = new String(argsProc.getInputStream().readAllBytes(), StandardCharsets.UTF_8); - argsProc.waitFor(30, TimeUnit.SECONDS); + String argsOut = WebsiteInstallerFixture.execute(shimProcess(binDir, "echo-args", "foo", "bar baz")) + .stdout(); assertTrue(argsOut.contains("foo") && argsOut.contains("bar baz"), argsOut); - Process exitProc = shimProcess(binDir, "exit-code", "7").start(); - exitProc.waitFor(30, TimeUnit.SECONDS); - assertEquals(7, exitProc.exitValue()); + assertEquals(7, WebsiteInstallerFixture.execute(shimProcess(binDir, "exit-code", "7")).exit()); } } diff --git a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallerFixture.java b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallerFixture.java index a94857d9b708a..996e2eddf74f1 100644 --- a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallerFixture.java +++ b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallerFixture.java @@ -64,6 +64,8 @@ final class WebsiteInstallerFixture implements AutoCloseable { private static final String KEY_ALIAS = "camel-installer-test"; private static final String DEFAULT_BASE_VERSION = "9.9.9"; private static final Duration PROCESS_TIMEOUT = Duration.ofSeconds(60); + private static final Path POWERSHELL_MODULE_ANALYSIS_CACHE + = Path.of("target", "powershell", "ModuleAnalysisCache").toAbsolutePath(); record Result(int exit, String stdout, String stderr) { } @@ -446,6 +448,20 @@ Result run(List command, Map env) throws Exception { if (pathExt != null) { pb.environment().put("PATHEXT", pathExt); } + // Without TEMP/TMP, .NET falls back to USERPROFILE (the fresh per-test home) for the scratch + // files Add-Type's C# compile writes on every install.ps1 run. install.ps1 itself never stages + // anything under the temp directory, so passing the runner's through does not affect isolation. + for (String name : List.of("TEMP", "TMP")) { + String value = System.getenv(name); + if (value != null) { + pb.environment().put(name, value); + } + } + // PowerShell keeps its module analysis cache under LOCALAPPDATA, which every test points at a + // fresh home, so each powershell.exe launch would rebuild it from scratch when auto-loading + // Expand-Archive / Get-FileHash. Share one cache file across the whole test run instead. + Files.createDirectories(POWERSHELL_MODULE_ANALYSIS_CACHE.getParent()); + pb.environment().put("PSModuleAnalysisCachePath", POWERSHELL_MODULE_ANALYSIS_CACHE.toString()); } pb.environment().putAll(env); String home = env.get("HOME"); @@ -454,7 +470,16 @@ Result run(List command, Map env) throws Exception { // wherever the test JVM happens to be running from. pb.directory(Path.of(home).toFile()); } + return execute(pb); + } + /** + * Runs {@code pb} with stdout and stderr drained on background threads, so a chatty or stuck child can never block + * the test past {@link #PROCESS_TIMEOUT}: reading a stream to EOF before {@code waitFor} would wait for as long as + * the child (or any grandchild inheriting its handles) keeps the pipe open. + */ + static Result execute(ProcessBuilder pb) throws Exception { + long start = System.nanoTime(); Process process = pb.start(); ExecutorService collectors = Executors.newFixedThreadPool(2); try { @@ -463,7 +488,7 @@ Result run(List command, Map env) throws Exception { process.getOutputStream().close(); if (!process.waitFor(PROCESS_TIMEOUT.toSeconds(), TimeUnit.SECONDS)) { process.destroyForcibly(); - throw new IllegalStateException("installer did not exit in time"); + throw new IllegalStateException("process did not exit in time: " + pb.command()); } String out = new String(await(stdout), StandardCharsets.UTF_8); String err = new String(await(stderr), StandardCharsets.UTF_8); @@ -474,6 +499,12 @@ Result run(List command, Map env) throws Exception { } collectors.shutdownNow(); collectors.awaitTermination(10, TimeUnit.SECONDS); + // Surefire streams test stdout to the build log as it happens, so these timings survive even when + // the fork is killed by forkedProcessTimeoutInSeconds and no XML report is ever written. + String command = String.join(" ", pb.command()); + System.out.printf("[WebsiteInstallerFixture] %d ms: %s%n", + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start), + command.length() > 160 ? command.substring(0, 160) + "..." : command); } }