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-website.git
The following commit(s) were added to refs/heads/main by this push:
new 065bea5e Camel CLI installers and documentation (#1699)
065bea5e is described below
commit 065bea5ebe5f2801b404853fd107b1c4a22f5e53
Author: Adriano Machado <[email protected]>
AuthorDate: Mon Jul 20 16:33:04 2026 -0400
Camel CLI installers and documentation (#1699)
* feat: publish Camel CLI Launcher manifests for 4.13.0 through 4.21.0
Per-version SHA-256 manifests (format=1, version, tar_sha256, zip_sha256)
for every camel-launcher release with -bin archives on Maven Central,
plus latest.properties pointing at 4.21.0. The 4.21.0 manifest matches
the test fixture in apache/camel's WebsiteInstallerFixture; the rest are
computed directly from the published -bin.tar.gz/-bin.zip archives.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
* feat: add Camel CLI Launcher manifest publishing helper
* feat: publish Camel CLI Launcher installers
install.sh / install.ps1 fetched byte-identical from apache/camel main
(dsl/camel-jbang/camel-launcher/src/install). Do not edit here; regenerate
with ./update-camel-cli-manifest.sh. Manifests for 4.13.0-4.21.0 were
published in a prior commit.
* docs: add Camel CLI Launcher installation guide
* blog: announce the Camel CLI Launcher installers
* blog: revise Camel CLI Launcher post for voice and a route example
- Link to the installers directory in apache/camel instead of the PR
- Match the voice of recent posts (direct, concrete, no filler)
- Lead with how much friction the installer removes vs. jbang setup
- Add a real camel init/camel run example with verified output
* blog: add featured image to Camel CLI Launcher installers post
---------
Co-authored-by: Claude Sonnet 5 <[email protected]>
---
.../07/camel-cli-launcher-installers/featured.png | Bin 0 -> 2173506 bytes
.../2026/07/camel-cli-launcher-installers/index.md | 88 +++++++
content/download/_index.md | 6 +
content/download/camel-cli-launcher.md | 67 +++++
static/camel-cli/releases/4.13.0.properties | 4 +
static/camel-cli/releases/4.14.0.properties | 4 +
static/camel-cli/releases/4.14.1.properties | 4 +
static/camel-cli/releases/4.14.2.properties | 4 +
static/camel-cli/releases/4.14.3.properties | 4 +
static/camel-cli/releases/4.14.4.properties | 4 +
static/camel-cli/releases/4.14.5.properties | 4 +
static/camel-cli/releases/4.14.6.properties | 4 +
static/camel-cli/releases/4.14.7.properties | 4 +
static/camel-cli/releases/4.14.8.properties | 4 +
static/camel-cli/releases/4.15.0.properties | 4 +
static/camel-cli/releases/4.16.0.properties | 4 +
static/camel-cli/releases/4.17.0.properties | 4 +
static/camel-cli/releases/4.18.0.properties | 4 +
static/camel-cli/releases/4.18.1.properties | 4 +
static/camel-cli/releases/4.18.2.properties | 4 +
static/camel-cli/releases/4.18.3.properties | 4 +
static/camel-cli/releases/4.19.0.properties | 4 +
static/camel-cli/releases/4.20.0.properties | 4 +
static/camel-cli/releases/4.21.0.properties | 4 +
static/camel-cli/releases/latest.properties | 4 +
static/install.ps1 | 292 +++++++++++++++++++++
static/install.sh | 290 ++++++++++++++++++++
update-camel-cli-manifest.sh | 131 +++++++++
28 files changed, 958 insertions(+)
diff --git a/content/blog/2026/07/camel-cli-launcher-installers/featured.png
b/content/blog/2026/07/camel-cli-launcher-installers/featured.png
new file mode 100644
index 00000000..0c007485
Binary files /dev/null and
b/content/blog/2026/07/camel-cli-launcher-installers/featured.png differ
diff --git a/content/blog/2026/07/camel-cli-launcher-installers/index.md
b/content/blog/2026/07/camel-cli-launcher-installers/index.md
new file mode 100644
index 00000000..29a07775
--- /dev/null
+++ b/content/blog/2026/07/camel-cli-launcher-installers/index.md
@@ -0,0 +1,88 @@
+---
+title: "Installing the Apache Camel CLI Launcher"
+date: 2026-07-20
+draft: false
+authors: [ ammachado ]
+categories: [ "Tooling" ]
+keywords: ["apache camel", "camel cli", "installer", "getting started",
"developer experience"]
+preview: "Install the Camel CLI Launcher with a single, checksum-verified
command on macOS, Linux, and Windows, no JDK setup or separate jbang install
required."
+---
+
+Getting started with the Camel CLI used to mean installing a JDK, then jbang,
then
+finally running `jbang camel@apache/camel init` for the first time. That
friction
+is gone. `install.sh` and `install.ps1` get the Camel CLI Launcher running on
your
+machine with a single command, no package manager and no separate jbang install
+required.
+
+## One-line install
+
+On macOS and Linux:
+
+```bash
+curl -fsSL https://camel.apache.org/install.sh | sh
+```
+
+On Windows, in PowerShell:
+
+```powershell
+irm https://camel.apache.org/install.ps1 | iex
+```
+
+Both installers resolve and install the latest published release by default, or
+a specific version if you request one with `--version` (`install.sh`) or
`-Version`
+(`install.ps1`). Installation is always per-user and never requires `sudo` or
+elevation.
+
+## From zero to a running route
+
+Once the installer finishes, `camel` is on your PATH. Create a route:
+
+```bash
+camel init hello.yaml
+```
+
+This generates a minimal route that fires a timer and logs a message:
+
+```yaml
+- route:
+ from:
+ uri: timer:yaml
+ parameters:
+ period: "1000"
+ steps:
+ - setBody:
+ simple: Hello Camel from ${routeId}
+ - log: ${body}
+```
+
+Run it:
+
+```bash
+camel run hello.yaml
+```
+
+```
+... Started route1 (timer://yaml)
+... hello.yaml:9 : Hello Camel from route1
+... hello.yaml:9 : Hello Camel from route1
+```
+
+That is the entire path from a fresh machine to a running integration: one
install
+command, `camel init`, `camel run`.
+
+## Verify before execute
+
+The installers are built around a simple security property: the launcher
archive
+is downloaded from [Maven
Central](https://repo1.maven.org/maven2/org/apache/camel/camel-launcher/),
+but it is verified against a SHA-256 checksum published on `camel.apache.org`
+before anything is extracted or run. The archive and the checksum come from two
+independent hosts, so tampering would require compromising both at the same
time.
+The installer also rejects unsafe archive contents and confirms a Java 17+
+runtime before activating the new version.
+
+## Learn more
+
+See the [installation guide](/download/camel-cli-launcher/) for pinned versions
+and the inspect-before-run workflow. The installer scripts themselves live in
the
+Apache Camel repository, under
+[dsl/camel-jbang/camel-launcher/src/install](https://github.com/apache/camel/tree/main/dsl/camel-jbang/camel-launcher/src/install).
diff --git a/content/download/_index.md b/content/download/_index.md
index 2714794b..d39b16bd 100644
--- a/content/download/_index.md
+++ b/content/download/_index.md
@@ -23,6 +23,12 @@ download above. These machine-readable inventories list
every dependency in the
tools like [OWASP Dependency-Track](https://dependencytrack.org/) for
automated vulnerability scanning.
To generate an SBOM for your own Camel application, see the [Generating
SBOMs](/manual/sbom.html) guide.
+## Camel CLI Launcher
+
+You can install the [Camel CLI Launcher](/download/camel-cli-launcher/) on a
+single machine with a one-line command, without a package manager. See the
+[installation guide](/download/camel-cli-launcher/) for details.
+
## Keys
You can verify your download by following these
[procedures](http://www.apache.org/info/verification.html) and using these
[KEYS](https://www.apache.org/dist/camel/KEYS).
diff --git a/content/download/camel-cli-launcher.md
b/content/download/camel-cli-launcher.md
new file mode 100644
index 00000000..1f146d2d
--- /dev/null
+++ b/content/download/camel-cli-launcher.md
@@ -0,0 +1,67 @@
+---
+title: "Install the Camel CLI Launcher"
+---
+
+The Camel CLI Launcher can be installed on a single machine without a package
+manager. The installer scripts published on this site download the launcher
+from Maven Central and verify it against a SHA-256 checksum published here
+before running it. Installation is always per-user and never requires `sudo`
+or elevation.
+
+## Requirements
+
+- A Java 17 or newer runtime discoverable on your system.
+- `curl` (macOS and Linux) or PowerShell 5.1+ (Windows).
+
+## Quick install
+
+With no arguments the installer resolves and installs the latest published
+release.
+
+### macOS and Linux
+
+```bash
+curl -fsSL https://camel.apache.org/install.sh | sh
+```
+
+### Windows (PowerShell)
+
+```powershell
+irm https://camel.apache.org/install.ps1 | iex
+```
+
+## Install a specific version, or inspect before running
+
+If you prefer not to pipe a script straight into a shell, download it, read it,
+then run it. The same approach lets you pin an exact `X.Y.Z` version.
+
+### macOS and Linux
+
+```bash
+curl -fsSL https://camel.apache.org/install.sh -o install.sh
+less install.sh
+sh install.sh --version 4.21.0
+```
+
+### Windows (PowerShell)
+
+```powershell
+irm https://camel.apache.org/install.ps1 -OutFile install.ps1
+notepad install.ps1
+./install.ps1 -Version 4.21.0
+```
+
+## How the security works
+
+The installer downloads the release archive from
+[Maven
Central](https://repo1.maven.org/maven2/org/apache/camel/camel-launcher/),
+but verifies it against a SHA-256 checksum recorded in a manifest served from
+`camel.apache.org`. The archive and the checksum come from two independent
+hosts, so tampering would require compromising both at once. The installer also
+rejects archives containing absolute paths, `../` traversal, escaping symlinks,
+or more than one top-level directory, and runs the staged launcher once to
+confirm a Java 17+ runtime before activating it. If that check fails, any
+previously installed version is left untouched.
+
+The installers are maintained in the Apache Camel repository
+([apache/camel#24682](https://github.com/apache/camel/pull/24682)).
diff --git a/static/camel-cli/releases/4.13.0.properties
b/static/camel-cli/releases/4.13.0.properties
new file mode 100644
index 00000000..0f9a940b
--- /dev/null
+++ b/static/camel-cli/releases/4.13.0.properties
@@ -0,0 +1,4 @@
+format=1
+version=4.13.0
+tar_sha256=3d1a31b2ecb6d345fa491574e400c6099d68b5a7349b402c7ee4c01cc840645b
+zip_sha256=80c45a45e500998c21a6b111b9ddf80ea2543913b7955f1d92a3ec16affbdd72
diff --git a/static/camel-cli/releases/4.14.0.properties
b/static/camel-cli/releases/4.14.0.properties
new file mode 100644
index 00000000..077e9654
--- /dev/null
+++ b/static/camel-cli/releases/4.14.0.properties
@@ -0,0 +1,4 @@
+format=1
+version=4.14.0
+tar_sha256=23efaa431f0faa1ee3183e333b83601952277ab9f881d448c52564abe7447ec0
+zip_sha256=9d428f765692c6d115a65b2c8199640593736ef0a7fd1a1f63bfdae69401a3de
diff --git a/static/camel-cli/releases/4.14.1.properties
b/static/camel-cli/releases/4.14.1.properties
new file mode 100644
index 00000000..28c78b56
--- /dev/null
+++ b/static/camel-cli/releases/4.14.1.properties
@@ -0,0 +1,4 @@
+format=1
+version=4.14.1
+tar_sha256=4a1157f2a6a40c992335a246c759c01706be209852a652cfd5633e37d61b8442
+zip_sha256=787881f4c9d7209137f067a79c0d2a0ea373e82e1e4f12c60ed744c1a32f0f72
diff --git a/static/camel-cli/releases/4.14.2.properties
b/static/camel-cli/releases/4.14.2.properties
new file mode 100644
index 00000000..0854fbea
--- /dev/null
+++ b/static/camel-cli/releases/4.14.2.properties
@@ -0,0 +1,4 @@
+format=1
+version=4.14.2
+tar_sha256=4c59276f1ffc965ef33c1f591abb5a6837db89611902d18ba2cd81747b620bce
+zip_sha256=c4af337b0e7edc18f3bc4d9487b0b897bab6ab3297d0881d26601654255a8f00
diff --git a/static/camel-cli/releases/4.14.3.properties
b/static/camel-cli/releases/4.14.3.properties
new file mode 100644
index 00000000..4e02380e
--- /dev/null
+++ b/static/camel-cli/releases/4.14.3.properties
@@ -0,0 +1,4 @@
+format=1
+version=4.14.3
+tar_sha256=bf07215a8a5612f13435cc002aee3b2833ba6d221eebe9202e5a649d95e5a5b7
+zip_sha256=d9d7683df2c3442055d7f454022ce08d0ccd1dc07935e10d6c4dea4e7b192435
diff --git a/static/camel-cli/releases/4.14.4.properties
b/static/camel-cli/releases/4.14.4.properties
new file mode 100644
index 00000000..9cab236f
--- /dev/null
+++ b/static/camel-cli/releases/4.14.4.properties
@@ -0,0 +1,4 @@
+format=1
+version=4.14.4
+tar_sha256=cf23867122e719d2e41dbf1975807f7a4b5e126c1e4e0adfd1f8b234a18b2cee
+zip_sha256=892163aa6002ff5f94b0a2ca1208d0e527e4a7f52f6b54855983c669b1fc6f56
diff --git a/static/camel-cli/releases/4.14.5.properties
b/static/camel-cli/releases/4.14.5.properties
new file mode 100644
index 00000000..2776611b
--- /dev/null
+++ b/static/camel-cli/releases/4.14.5.properties
@@ -0,0 +1,4 @@
+format=1
+version=4.14.5
+tar_sha256=77d4eda924c97687b6c8d09d34e5c15187ed214c5284edc8086c612d73c32896
+zip_sha256=1058f42ef770adc1b05c770ecadab02ebd08eb0334381d679fecb1ad10a74e56
diff --git a/static/camel-cli/releases/4.14.6.properties
b/static/camel-cli/releases/4.14.6.properties
new file mode 100644
index 00000000..d95ad3d9
--- /dev/null
+++ b/static/camel-cli/releases/4.14.6.properties
@@ -0,0 +1,4 @@
+format=1
+version=4.14.6
+tar_sha256=16826411fc4553a92d1df1c633be942f5ec315911ec48e062ca2f65cd228a68a
+zip_sha256=ece1939eedaad3b5d999deb21101ea50cbe35631d756ec8aefb01e0a64972522
diff --git a/static/camel-cli/releases/4.14.7.properties
b/static/camel-cli/releases/4.14.7.properties
new file mode 100644
index 00000000..61fe8184
--- /dev/null
+++ b/static/camel-cli/releases/4.14.7.properties
@@ -0,0 +1,4 @@
+format=1
+version=4.14.7
+tar_sha256=d74214be90d330721292a50d1922c553f438348ab5e05832f84c6d4857dd1f94
+zip_sha256=b28541f594c1bf6e168240acaae9f9e1ee4fe2e83e63fc6b4d76d19a3747f09d
diff --git a/static/camel-cli/releases/4.14.8.properties
b/static/camel-cli/releases/4.14.8.properties
new file mode 100644
index 00000000..ac58787d
--- /dev/null
+++ b/static/camel-cli/releases/4.14.8.properties
@@ -0,0 +1,4 @@
+format=1
+version=4.14.8
+tar_sha256=21b6e2ea881cf06c99dbf6502cbd3dc98f59e44f8491b8d22c4758b3942fa37f
+zip_sha256=4b09c9da80f6fb5f87323f626422844a1847731f49b5c62c7681221924798b61
diff --git a/static/camel-cli/releases/4.15.0.properties
b/static/camel-cli/releases/4.15.0.properties
new file mode 100644
index 00000000..cf55369c
--- /dev/null
+++ b/static/camel-cli/releases/4.15.0.properties
@@ -0,0 +1,4 @@
+format=1
+version=4.15.0
+tar_sha256=bc7d40e4ce18b8a195e1e6333e8ec147efe697a2500dd15840c6b9ca6abbf14a
+zip_sha256=38506074a7bb4f39cd48bbc3f9c3a25a2a93e9c0ed62962cd03e4fd53d1ac1aa
diff --git a/static/camel-cli/releases/4.16.0.properties
b/static/camel-cli/releases/4.16.0.properties
new file mode 100644
index 00000000..576c1d6b
--- /dev/null
+++ b/static/camel-cli/releases/4.16.0.properties
@@ -0,0 +1,4 @@
+format=1
+version=4.16.0
+tar_sha256=19e784eee53213b18cb620e3b3888dcc1fc4d925c41b48846db7825d38b28653
+zip_sha256=1ea3368b629f60443b907e28a9f8eb7d57bc0ba5036e4cfbfd30f1a9fed79602
diff --git a/static/camel-cli/releases/4.17.0.properties
b/static/camel-cli/releases/4.17.0.properties
new file mode 100644
index 00000000..0955bdcf
--- /dev/null
+++ b/static/camel-cli/releases/4.17.0.properties
@@ -0,0 +1,4 @@
+format=1
+version=4.17.0
+tar_sha256=6aae94b30a33a04c2daed938333956f6d41615fc0edd9a27219dbe9665595f72
+zip_sha256=9d6bdd6552020d6aac3dea83de32765e03e2aa5b9cf278598c1575d2416258c6
diff --git a/static/camel-cli/releases/4.18.0.properties
b/static/camel-cli/releases/4.18.0.properties
new file mode 100644
index 00000000..2692894b
--- /dev/null
+++ b/static/camel-cli/releases/4.18.0.properties
@@ -0,0 +1,4 @@
+format=1
+version=4.18.0
+tar_sha256=3194acbe30530a8814a58ad86042a43800435e459ab8e6fc17141d4a6d05d85f
+zip_sha256=a202622ef5800704e952dfb492bad3cc887f10c9f5043b40be6adbe15b389bdd
diff --git a/static/camel-cli/releases/4.18.1.properties
b/static/camel-cli/releases/4.18.1.properties
new file mode 100644
index 00000000..857c7901
--- /dev/null
+++ b/static/camel-cli/releases/4.18.1.properties
@@ -0,0 +1,4 @@
+format=1
+version=4.18.1
+tar_sha256=efe028eda43216d1150828aed2449c0af8c62f35446fd0c4b3835de2a02a8093
+zip_sha256=950fc433d3586be7893542d06fdb7175ed1b6999bcb77e895a0e0592670c8042
diff --git a/static/camel-cli/releases/4.18.2.properties
b/static/camel-cli/releases/4.18.2.properties
new file mode 100644
index 00000000..d6674c04
--- /dev/null
+++ b/static/camel-cli/releases/4.18.2.properties
@@ -0,0 +1,4 @@
+format=1
+version=4.18.2
+tar_sha256=5288962fee8cf22df2e3717350205fd1cdc47ba449436a43a677ba0cb84a49aa
+zip_sha256=513455358de4af716d3fcd524e506d40604ed417d86304292c59a23f87452845
diff --git a/static/camel-cli/releases/4.18.3.properties
b/static/camel-cli/releases/4.18.3.properties
new file mode 100644
index 00000000..47d97000
--- /dev/null
+++ b/static/camel-cli/releases/4.18.3.properties
@@ -0,0 +1,4 @@
+format=1
+version=4.18.3
+tar_sha256=8128d5c8259f72b29b25f857f97eb7f892067801fc2c2e2aa53ca1c9a780d01d
+zip_sha256=7d0cc0f74615497e1a640695f030e7747dc3a15780cb1ec5906ba4499849c627
diff --git a/static/camel-cli/releases/4.19.0.properties
b/static/camel-cli/releases/4.19.0.properties
new file mode 100644
index 00000000..0e7edbcc
--- /dev/null
+++ b/static/camel-cli/releases/4.19.0.properties
@@ -0,0 +1,4 @@
+format=1
+version=4.19.0
+tar_sha256=21b198c34b0028f3e0241e14f81af6e840a10e6ba0f2fb28f16055270a2c35d3
+zip_sha256=2eb1a146a9b3c6f9fd10a5493263296a428ad2ada11280df779c36524b510a2c
diff --git a/static/camel-cli/releases/4.20.0.properties
b/static/camel-cli/releases/4.20.0.properties
new file mode 100644
index 00000000..3e4bdeda
--- /dev/null
+++ b/static/camel-cli/releases/4.20.0.properties
@@ -0,0 +1,4 @@
+format=1
+version=4.20.0
+tar_sha256=5f2332580392f9c77b2489c728e3052848fe5b8c201ae701fe2bf37b83351782
+zip_sha256=086a7b16d0a9ffc951f2d5f4dcb8c09a15a08da73ced6e002d56279664667de6
diff --git a/static/camel-cli/releases/4.21.0.properties
b/static/camel-cli/releases/4.21.0.properties
new file mode 100644
index 00000000..8ca2416e
--- /dev/null
+++ b/static/camel-cli/releases/4.21.0.properties
@@ -0,0 +1,4 @@
+format=1
+version=4.21.0
+tar_sha256=24c247b9418292bce794b8ad2333691263c3d0de9a4a0fbc767942669b71984c
+zip_sha256=949d39606fc5d4f246a9ea5166f1f8597e75229332653f48b667da73fb87d80e
diff --git a/static/camel-cli/releases/latest.properties
b/static/camel-cli/releases/latest.properties
new file mode 100644
index 00000000..8ca2416e
--- /dev/null
+++ b/static/camel-cli/releases/latest.properties
@@ -0,0 +1,4 @@
+format=1
+version=4.21.0
+tar_sha256=24c247b9418292bce794b8ad2333691263c3d0de9a4a0fbc767942669b71984c
+zip_sha256=949d39606fc5d4f246a9ea5166f1f8597e75229332653f48b667da73fb87d80e
diff --git a/static/install.ps1 b/static/install.ps1
new file mode 100644
index 00000000..c57fc6b3
--- /dev/null
+++ b/static/install.ps1
@@ -0,0 +1,292 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+param(
+ [string] $Version
+)
+
+$ErrorActionPreference = 'Stop'
+
+# Test seams only: production installs never set these, so the defaults below
are always used.
+$ManifestBaseUrl = if ($env:CAMEL_INSTALL_MANIFEST_BASE_URL) {
$env:CAMEL_INSTALL_MANIFEST_BASE_URL } else {
'https://camel.apache.org/camel-cli/releases' }
+$MavenBaseUrl = if ($env:CAMEL_INSTALL_MAVEN_BASE_URL) {
$env:CAMEL_INSTALL_MAVEN_BASE_URL } else {
'https://repo1.maven.org/maven2/org/apache/camel/camel-launcher' }
+$CaCertPath = $env:CAMEL_INSTALL_CA_CERT
+
+$InstallRoot = Join-Path $env:LOCALAPPDATA 'Apache Camel'
+$DataRoot = Join-Path $InstallRoot 'cli\versions'
+$BinDir = Join-Path $InstallRoot 'bin'
+
+function Fail {
+ param([string] $Message)
+ [Console]::Error.WriteLine("install.ps1: $Message")
+ exit 1
+}
+
+function Test-ValidVersion {
+ param([string] $Value)
+ return $Value -match '\A[0-9]+\.[0-9]+\.[0-9]+\z'
+}
+
+function Test-ValidSha256 {
+ param([string] $Value, [string] $Label)
+ if ($Value -notmatch '\A[0-9a-f]{64}\z') {
+ Fail "$Label is not a 64-character lowercase hex value"
+ }
+}
+
+if ($CaCertPath) {
+ # Test seam only: trusts the loopback fixture's self-signed CA for this
process without touching
+ # the real Windows certificate store. Production installs never set
CAMEL_INSTALL_CA_CERT.
+ $installerCaCert = New-Object
System.Security.Cryptography.X509Certificates.X509Certificate2($CaCertPath)
+ [System.Net.ServicePointManager]::SecurityProtocol =
[System.Net.SecurityProtocolType]::Tls12
+ [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {
+ param($sender, $certificate, $chain, $sslPolicyErrors)
+ $verifyChain = New-Object
System.Security.Cryptography.X509Certificates.X509Chain
+ $verifyChain.ChainPolicy.ExtraStore.Add($installerCaCert) | Out-Null
+ $verifyChain.ChainPolicy.RevocationMode =
[System.Security.Cryptography.X509Certificates.X509RevocationMode]::NoCheck
+ $verifyChain.ChainPolicy.VerificationFlags =
[System.Security.Cryptography.X509Certificates.X509VerificationFlags]::AllowUnknownCertificateAuthority
+ $leaf = New-Object
System.Security.Cryptography.X509Certificates.X509Certificate2($certificate)
+ if (-not $verifyChain.Build($leaf)) {
+ return $false
+ }
+ $root = $verifyChain.ChainElements[$verifyChain.ChainElements.Count -
1].Certificate
+ return $root.Thumbprint -eq $installerCaCert.Thumbprint
+ }.GetNewClosure()
+}
+
+# Downloads $Url to $OutFile; used for both the manifest and archive fetches.
+function Save-RemoteFile {
+ param([string] $Url, [string] $OutFile)
+ try {
+ Invoke-WebRequest -Uri $Url -OutFile $OutFile -UseBasicParsing |
Out-Null
+ } catch {
+ Fail "failed to download $Url"
+ }
+}
+
+# Reads $Path line by line without ever dot-sourcing, invoking, or evaluating
its content.
+function Read-Manifest {
+ param([string] $Path)
+
+ $lines = @(Get-Content -LiteralPath $Path -Encoding UTF8)
+ if ($lines.Count -ne 4) {
+ Fail "manifest must contain exactly four lines"
+ }
+
+ $known = @('format', 'version', 'tar_sha256', 'zip_sha256')
+ $values = New-Object
'System.Collections.Generic.Dictionary[string,string]'
([StringComparer]::OrdinalIgnoreCase)
+ foreach ($line in $lines) {
+ if ([string]::IsNullOrEmpty($line)) {
+ Fail "manifest contains a blank line"
+ }
+ $parts = $line.Split('=', 2)
+ if ($parts.Count -ne 2 -or [string]::IsNullOrEmpty($parts[0])) {
+ Fail "manifest contains a blank line"
+ }
+ $key = $parts[0]
+ $value = $parts[1]
+ if ([string]::IsNullOrEmpty($value)) {
+ Fail "manifest key '$key' has an empty value"
+ }
+ if ($values.ContainsKey($key)) {
+ Fail "manifest has duplicate key: $key"
+ }
+ if ($known -notcontains $key.ToLowerInvariant()) {
+ Fail "manifest has unknown key: $key"
+ }
+ $values[$key] = $value
+ }
+
+ foreach ($required in $known) {
+ if (-not $values.ContainsKey($required)) {
+ Fail "manifest is missing a required key"
+ }
+ }
+
+ if ($values['format'] -ne '1') {
+ Fail "unsupported manifest format: $($values['format'])"
+ }
+ if (-not (Test-ValidVersion $values['version'])) {
+ Fail "manifest version is not a valid X.Y.Z value"
+ }
+ Test-ValidSha256 $values['tar_sha256'] 'manifest tar_sha256'
+ Test-ValidSha256 $values['zip_sha256'] 'manifest zip_sha256'
+
+ return $values
+}
+
+# Lists archive entries via System.IO.Compression before Expand-Archive ever
runs, and rejects absolute
+# paths, traversal, symlink/reparse-point entries, multiple top-level roots,
and a missing launcher.
+function Test-ArchiveEntry {
+ param([string] $ArchivePath, [string] $Version)
+
+ $expectedRoot = "camel-launcher-$Version"
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
+ $zip = [System.IO.Compression.ZipFile]::OpenRead($ArchivePath)
+ try {
+ $roots = New-Object 'System.Collections.Generic.HashSet[string]'
+ $foundBatchLauncher = $false
+ foreach ($entry in $zip.Entries) {
+ $name = $entry.FullName
+ if ([string]::IsNullOrEmpty($name)) {
+ continue
+ }
+ if ($name.StartsWith('/') -or $name.StartsWith('\') -or
($name.Length -ge 2 -and $name[1] -eq ':')) {
+ Fail "archive contains an absolute path entry: $name"
+ }
+ $normalized = $name.Replace('\', '/')
+ $segments = $normalized.Split('/')
+ if ($segments -contains '..') {
+ Fail "archive contains a path traversal entry: $name"
+ }
+ $unixMode = ([uint32]$entry.ExternalAttributes -shr 16) -band
0xF000
+ if ($unixMode -eq 0xA000) {
+ Fail "archive contains a symbolic link or reparse point entry,
which is not allowed"
+ }
+ [void]$roots.Add($segments[0])
+ if ($normalized -eq "$expectedRoot/bin/camel.bat") {
+ $foundBatchLauncher = $true
+ }
+ }
+ if ($roots.Count -ne 1) {
+ Fail "archive must contain exactly one top-level directory"
+ }
+ if (-not $roots.Contains($expectedRoot)) {
+ Fail "archive top-level directory does not match expected version:
$($roots -join ',')"
+ }
+ if (-not $foundBatchLauncher) {
+ Fail "archive is missing bin\camel.bat"
+ }
+ } finally {
+ $zip.Dispose()
+ }
+}
+
+# Runs the freshly staged upstream launcher; a nonzero exit (e.g. no Java 17+
available) aborts the
+# install and leaves the previously active installation untouched.
+function Test-StagedLauncher {
+ param([string] $LauncherPath)
+ try {
+ & $LauncherPath 'version' *> $null
+ } catch {
+ Fail "staged launcher failed verification (Java 17+ required)"
+ }
+ if ($LASTEXITCODE -ne 0) {
+ Fail "staged launcher failed verification (Java 17+ required)"
+ }
+}
+
+function Set-CamelShim {
+ param([string] $Version, [string] $StagedRoot)
+
+ $targetDir = Join-Path $DataRoot $Version
+ New-Item -ItemType Directory -Force -Path $DataRoot | Out-Null
+ if (Test-Path -LiteralPath $targetDir) {
+ Remove-Item -LiteralPath $targetDir -Recurse -Force
+ }
+ Move-Item -LiteralPath $StagedRoot -Destination $targetDir
+
+ New-Item -ItemType Directory -Force -Path $BinDir | Out-Null
+ $launcherPath = Join-Path $targetDir 'bin\camel.bat'
+ $shimContent = "@echo off`r`ncall `"$launcherPath`" %*`r`nexit /b
%ERRORLEVEL%`r`n"
+ $tempShim = Join-Path $BinDir ".camel.$PID.tmp.cmd"
+ # Write without a BOM: Windows PowerShell 5.1's 'Set-Content -Encoding
UTF8' prepends one, which
+ # cmd.exe treats as part of the first line, breaking '@echo off' and
emitting a stray error.
+ [System.IO.File]::WriteAllText($tempShim, $shimContent, (New-Object
System.Text.UTF8Encoding($false)))
+ $finalShim = Join-Path $BinDir 'camel.cmd'
+ Move-Item -LiteralPath $tempShim -Destination $finalShim -Force
+}
+
+# Adds $Dir once, case-insensitively, to the current user's PATH
(registry-level, no elevation) and to
+# this process; the machine PATH is never written. The value is read and
written through the registry
+# with DoNotExpandEnvironmentNames so existing %VAR% references are preserved
rather than flattened,
+# and the original value kind (typically REG_EXPAND_SZ) is kept intact.
+function Add-UserPath {
+ param([string] $Dir)
+
+ $key = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Environment')
+ try {
+ $userPath = $key.GetValue('Path', '',
[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
+ $kind = if ($userPath) { $key.GetValueKind('Path') } else {
[Microsoft.Win32.RegistryValueKind]::ExpandString }
+ $entries = @()
+ if ($userPath) {
+ $entries = $userPath.Split(';') | Where-Object { $_ -ne '' }
+ }
+ $present = $entries | Where-Object { $_.TrimEnd('\') -ieq
$Dir.TrimEnd('\') }
+ if (-not $present) {
+ $newPath = if ($entries.Count -gt 0) { ($entries + $Dir) -join ';'
} else { $Dir }
+ $key.SetValue('Path', $newPath, $kind)
+ }
+ } finally {
+ $key.Close()
+ }
+ if (($env:Path -split ';') -notcontains $Dir) {
+ $env:Path = "$env:Path;$Dir"
+ }
+}
+
+if ($Version -and -not (Test-ValidVersion $Version)) {
+ Fail "invalid -Version value: $Version (expected X.Y.Z)"
+}
+
+New-Item -ItemType Directory -Force -Path $InstallRoot | Out-Null
+$stagingRoot = Join-Path $InstallRoot ("staging." +
[Guid]::NewGuid().ToString('N'))
+New-Item -ItemType Directory -Path $stagingRoot | Out-Null
+
+try {
+ if ($Version) {
+ $manifestUrl = "$ManifestBaseUrl/$Version.properties"
+ } else {
+ $manifestUrl = "$ManifestBaseUrl/latest.properties"
+ }
+ $manifestFile = Join-Path $stagingRoot 'manifest.properties'
+ Save-RemoteFile -Url $manifestUrl -OutFile $manifestFile
+
+ $manifest = Read-Manifest -Path $manifestFile
+ $resolvedVersion = $manifest['version']
+
+ if ($Version -and $Version -ne $resolvedVersion) {
+ Fail "manifest version ($resolvedVersion) does not match requested
version ($Version)"
+ }
+
+ $archiveUrl =
"$MavenBaseUrl/$resolvedVersion/camel-launcher-$resolvedVersion-bin.zip"
+ $archiveFile = Join-Path $stagingRoot
"camel-launcher-$resolvedVersion-bin.zip"
+ Save-RemoteFile -Url $archiveUrl -OutFile $archiveFile
+
+ $actualHash = (Get-FileHash -Algorithm SHA256 -LiteralPath
$archiveFile).Hash.ToLowerInvariant()
+ if ($actualHash -ne $manifest['zip_sha256']) {
+ Fail "checksum mismatch for downloaded archive"
+ }
+
+ Test-ArchiveEntry -ArchivePath $archiveFile -Version $resolvedVersion
+
+ $extractDir = Join-Path $stagingRoot 'extract'
+ Expand-Archive -LiteralPath $archiveFile -DestinationPath $extractDir
-Force
+
+ $stagedRoot = Join-Path $extractDir "camel-launcher-$resolvedVersion"
+ $stagedLauncher = Join-Path $stagedRoot 'bin\camel.bat'
+ Test-StagedLauncher -LauncherPath $stagedLauncher
+
+ Set-CamelShim -Version $resolvedVersion -StagedRoot $stagedRoot
+ Add-UserPath -Dir $BinDir
+
+ Write-Host "Installed Camel CLI $resolvedVersion to $(Join-Path $DataRoot
$resolvedVersion)"
+} finally {
+ if (Test-Path -LiteralPath $stagingRoot) {
+ Remove-Item -LiteralPath $stagingRoot -Recurse -Force -ErrorAction
SilentlyContinue
+ }
+}
diff --git a/static/install.sh b/static/install.sh
new file mode 100644
index 00000000..7879686f
--- /dev/null
+++ b/static/install.sh
@@ -0,0 +1,290 @@
+#!/bin/sh
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+set -eu
+
+# Test seams only: production installs never set these, so the defaults below
are always used.
+manifest_base_url="${CAMEL_INSTALL_MANIFEST_BASE_URL:-https://camel.apache.org/camel-cli/releases}"
+maven_base_url="${CAMEL_INSTALL_MAVEN_BASE_URL:-https://repo1.maven.org/maven2/org/apache/camel/camel-launcher}"
+ca_cert="${CAMEL_INSTALL_CA_CERT:-}"
+
+data_root="${XDG_DATA_HOME:-$HOME/.local/share}/camel-cli/versions"
+camel_cli_root="${XDG_DATA_HOME:-$HOME/.local/share}/camel-cli"
+bin_dir="$HOME/.local/bin"
+
+fail() {
+ echo "install.sh: $1" 1>&2
+ exit 1
+}
+
+is_valid_version() {
+ v="$1"
+ case "$v" in
+ '' | .* | *. | *..* | *[!0-9.]*) return 1 ;;
+ esac
+ # The case above guarantees digits and single dots only, with no leading,
trailing, or doubled
+ # dot, so a valid X.Y.Z is exactly the value containing two dots (three
non-empty components).
+ rest="$v"
+ dots=0
+ while :; do
+ case "$rest" in
+ *.*)
+ dots=$((dots + 1))
+ rest="${rest#*.}"
+ ;;
+ *)
+ break
+ ;;
+ esac
+ done
+ [ "$dots" -eq 2 ]
+}
+
+validate_sha256() {
+ value="$1"
+ label="$2"
+ [ ${#value} -eq 64 ] || fail "$label is not a 64-character lowercase hex
value"
+ case "$value" in
+ *[!0-9a-f]*) fail "$label is not a 64-character lowercase hex value" ;;
+ esac
+}
+
+fetch() {
+ url="$1"
+ outfile="$2"
+ if command -v curl >/dev/null 2>&1; then
+ if [ -n "$ca_cert" ]; then
+ curl -fsSL --cacert "$ca_cert" -o "$outfile" "$url"
+ else
+ curl -fsSL -o "$outfile" "$url"
+ fi
+ elif command -v wget >/dev/null 2>&1; then
+ if [ -n "$ca_cert" ]; then
+ wget -q --ca-certificate="$ca_cert" -O "$outfile" "$url"
+ else
+ wget -q -O "$outfile" "$url"
+ fi
+ else
+ fail "curl or wget is required to install the Camel CLI"
+ fi
+}
+
+sha256() {
+ file="$1"
+ if command -v sha256sum >/dev/null 2>&1; then
+ sha256sum "$file" | awk '{print $1}'
+ elif command -v shasum >/dev/null 2>&1; then
+ shasum -a 256 "$file" | awk '{print $1}'
+ elif command -v openssl >/dev/null 2>&1; then
+ openssl dgst -sha256 "$file" | awk '{print $NF}'
+ else
+ fail "sha256sum, shasum, or openssl is required to verify the download"
+ fi
+}
+
+# Reads $manifest_file line by line without ever sourcing, dot-invoking, or
eval-ing its content.
+parse_manifest() {
+ file="$1"
+ cr=$(printf '\r')
+ p_format=""
+ p_version=""
+ p_tar=""
+ p_zip=""
+ seen_format=0
+ seen_version=0
+ seen_tar=0
+ seen_zip=0
+ line_count=0
+ while IFS='=' read -r key value || [ -n "$key" ]; do
+ # Tolerate a manifest served with CRLF line endings, matching the
PowerShell parser.
+ key="${key%"$cr"}"
+ value="${value%"$cr"}"
+ line_count=$((line_count + 1))
+ [ -n "$key" ] || fail "manifest contains a blank line"
+ [ -n "$value" ] || fail "manifest key '$key' has an empty value"
+ case "$key" in
+ format)
+ [ "$seen_format" -eq 0 ] || fail "manifest has duplicate key:
format"
+ seen_format=1
+ p_format="$value"
+ ;;
+ version)
+ [ "$seen_version" -eq 0 ] || fail "manifest has duplicate key:
version"
+ seen_version=1
+ p_version="$value"
+ ;;
+ tar_sha256)
+ [ "$seen_tar" -eq 0 ] || fail "manifest has duplicate key:
tar_sha256"
+ seen_tar=1
+ p_tar="$value"
+ ;;
+ zip_sha256)
+ [ "$seen_zip" -eq 0 ] || fail "manifest has duplicate key:
zip_sha256"
+ seen_zip=1
+ p_zip="$value"
+ ;;
+ *)
+ fail "manifest has unknown key: $key"
+ ;;
+ esac
+ done < "$file"
+
+ if [ "$seen_format" -ne 1 ] || [ "$seen_version" -ne 1 ] || [ "$seen_tar"
-ne 1 ] || [ "$seen_zip" -ne 1 ]; then
+ fail "manifest is missing a required key"
+ fi
+ [ "$line_count" -eq 4 ] || fail "manifest must contain exactly four lines"
+
+ [ "$p_format" = "1" ] || fail "unsupported manifest format: $p_format"
+ is_valid_version "$p_version" || fail "manifest version is not a valid
X.Y.Z value"
+ validate_sha256 "$p_tar" "manifest tar_sha256"
+ validate_sha256 "$p_zip" "manifest zip_sha256"
+}
+
+# Lists archive entries with 'tar -tzf' and rejects absolute paths, traversal,
symlinks/hardlinks,
+# multiple top-level roots, and a missing expected launcher, before anything
is extracted.
+validate_tar() {
+ archive="$1"
+ version="$2"
+ expected_root="camel-launcher-$version"
+
+ listing="$staging_dir/listing.txt"
+ tar -tzf "$archive" > "$listing" 2>/dev/null || fail "archive is not a
valid tar.gz"
+
+ verbose_listing="$staging_dir/listing-verbose.txt"
+ # LC_ALL=C keeps tar's hard-link annotation as the literal 'link to'
string; GNU tar localizes it.
+ LC_ALL=C tar -tvzf "$archive" > "$verbose_listing" 2>/dev/null || fail
"archive is not a valid tar.gz"
+ # tar -tv renders symlinks as 'name -> target' and hard links as 'name
link to target'. Matching
+ # those substrings could in theory over-reject a regular file whose name
contains ' -> ' or
+ # ' link to '; that fail-closed bias is deliberate, the release archive
never carries such names.
+ grep -E -- ' -> | link to ' "$verbose_listing" >/dev/null 2>&1 \
+ && fail "archive contains a symbolic or hard link entry, which is not
allowed"
+
+ saw_entry=0
+ while IFS= read -r entry; do
+ [ -n "$entry" ] || continue
+ case "$entry" in
+ /*) fail "archive contains an absolute path entry: $entry" ;;
+ esac
+ case "$entry" in
+ *"../"* | ".." | *"/..") fail "archive contains a path traversal
entry: $entry" ;;
+ esac
+ # Every entry must sit under the single expected top-level directory;
a differing root means a
+ # stray directory or the wrong version, so reject it directly rather
than collecting roots.
+ root="${entry%%/*}"
+ [ "$root" = "$expected_root" ] \
+ || fail "archive top-level directory does not match expected
version: $root"
+ saw_entry=1
+ done < "$listing"
+
+ [ "$saw_entry" -eq 1 ] || fail "archive must contain exactly one top-level
directory"
+
+ grep -Fqx "$expected_root/bin/camel.sh" "$listing" || fail "archive is
missing bin/camel.sh"
+}
+
+# Runs the freshly staged upstream launcher; a nonzero exit (e.g. no Java 17+
available) aborts
+# the install and leaves the previously active installation untouched.
+verify_staged() {
+ staged_sh="$1"
+ chmod +x "$staged_sh" 2>/dev/null || true
+ "$staged_sh" version >/dev/null 2>&1 || fail "staged launcher failed
verification (Java 17+ required)"
+}
+
+activate() {
+ version="$1"
+ staged_root_dir="$2"
+ target_dir="$data_root/$version"
+
+ mkdir -p "$data_root"
+ rm -rf "$target_dir"
+ mv "$staged_root_dir" "$target_dir"
+
+ mkdir -p "$bin_dir"
+ tmp_link="$bin_dir/.camel.tmp.$$"
+ rm -f "$tmp_link"
+ ln -s "$target_dir/bin/camel.sh" "$tmp_link"
+ mv -f "$tmp_link" "$bin_dir/camel"
+}
+
+requested_version=""
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --version)
+ [ $# -ge 2 ] || fail "Usage: install.sh [--version X.Y.Z]"
+ requested_version="$2"
+ shift 2
+ ;;
+ *)
+ fail "Usage: install.sh [--version X.Y.Z]"
+ ;;
+ esac
+done
+
+if [ -n "$requested_version" ]; then
+ is_valid_version "$requested_version" || fail "invalid --version value:
$requested_version (expected X.Y.Z)"
+fi
+
+staging_dir="$camel_cli_root/.staging.$$"
+cleanup() {
+ rm -rf "$staging_dir"
+}
+trap cleanup EXIT INT TERM
+
+mkdir -p "$camel_cli_root"
+rm -rf "$staging_dir"
+mkdir -m 700 "$staging_dir"
+
+if [ -n "$requested_version" ]; then
+ manifest_url="$manifest_base_url/$requested_version.properties"
+else
+ manifest_url="$manifest_base_url/latest.properties"
+fi
+
+manifest_file="$staging_dir/manifest.properties"
+fetch "$manifest_url" "$manifest_file" || fail "failed to download manifest
from $manifest_url"
+
+parse_manifest "$manifest_file"
+version="$p_version"
+
+if [ -n "$requested_version" ] && [ "$requested_version" != "$version" ]; then
+ fail "manifest version ($version) does not match requested version
($requested_version)"
+fi
+
+archive_url="$maven_base_url/$version/camel-launcher-$version-bin.tar.gz"
+archive_file="$staging_dir/camel-launcher-$version-bin.tar.gz"
+fetch "$archive_url" "$archive_file" || fail "failed to download archive from
$archive_url"
+
+actual_tar_sha256="$(sha256 "$archive_file")"
+[ "$actual_tar_sha256" = "$p_tar" ] || fail "checksum mismatch for downloaded
archive"
+
+validate_tar "$archive_file" "$version"
+
+extract_dir="$staging_dir/extract"
+mkdir -p "$extract_dir"
+tar -xzf "$archive_file" -C "$extract_dir"
+
+staged_root_dir="$extract_dir/camel-launcher-$version"
+verify_staged "$staged_root_dir/bin/camel.sh"
+
+activate "$version" "$staged_root_dir"
+
+case ":$PATH:" in
+ *":$bin_dir:"*) ;;
+ *) echo "Add $bin_dir to your PATH to use the 'camel' command." ;;
+esac
+
+echo "Installed Camel CLI $version to $data_root/$version"
diff --git a/update-camel-cli-manifest.sh b/update-camel-cli-manifest.sh
new file mode 100755
index 00000000..5afe5ad8
--- /dev/null
+++ b/update-camel-cli-manifest.sh
@@ -0,0 +1,131 @@
+#!/bin/sh
+#
+# update-camel-cli-manifest.sh - publish Camel CLI Launcher installers +
manifests.
+#
+# Usage: ./update-camel-cli-manifest.sh <version> [--latest]
+#
+# 1. Fetches install.sh and install.ps1 from apache/camel main and writes them
+# byte-identical to static/install.sh and static/install.ps1.
+# 2. Downloads camel-launcher-<version>-bin.{tar.gz,zip} from Maven Central,
+# computes SHA-256, and writes
static/camel-cli/releases/<version>.properties.
+# 3. With --latest, updates latest.properties, but only if <version> is newer.
+#
+# The manifest format (format=1, version, tar_sha256, zip_sha256) mirrors
+# WebsiteManifestGenerator.java in apache/camel and is parsed by the
installers.
+set -eu
+
+MAIN_RAW="https://raw.githubusercontent.com/apache/camel/main/dsl/camel-jbang/camel-launcher/src/install"
+MAVEN_BASE="https://repo1.maven.org/maven2/org/apache/camel/camel-launcher"
+
+repo_root=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
+static_dir="$repo_root/static"
+releases_dir="$static_dir/camel-cli/releases"
+
+die() { echo "error: $*" >&2; exit 1; }
+usage() { echo "Usage: $0 <version> [--latest]" >&2; exit 2; }
+
+[ $# -ge 1 ] || usage
+version=$1
+shift
+update_latest=0
+while [ $# -gt 0 ]; do
+ case $1 in
+ --latest) update_latest=1 ;;
+ *) usage ;;
+ esac
+ shift
+done
+
+echo "$version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' || die "version must be
X.Y.Z: $version"
+
+sha256() {
+ if command -v sha256sum >/dev/null 2>&1; then
+ sha256sum "$1" | awk '{print $1}'
+ elif command -v shasum >/dev/null 2>&1; then
+ shasum -a 256 "$1" | awk '{print $1}'
+ else
+ die "need sha256sum or shasum"
+ fi
+}
+
+# 0 (true) if $1 is strictly newer than $2 (both X.Y.Z numeric).
+version_gt() {
+ i=1
+ while [ "$i" -le 3 ]; do
+ fa=$(echo "$1" | cut -d. -f"$i")
+ fb=$(echo "$2" | cut -d. -f"$i")
+ [ "$fa" -gt "$fb" ] && return 0
+ [ "$fa" -lt "$fb" ] && return 1
+ i=$((i + 1))
+ done
+ return 1
+}
+
+tmp=$(mktemp -d)
+trap 'rm -rf "$tmp"' EXIT INT TERM
+
+# --- 1. Vendor the installer scripts from apache/camel main ------------------
+mkdir -p "$static_dir"
+for script in install.sh install.ps1; do
+ echo "Fetching $script from apache/camel main"
+ curl -fsSL "$MAIN_RAW/$script" -o "$tmp/$script" \
+ || die "could not download $script from $MAIN_RAW/$script"
+ mv "$tmp/$script" "$static_dir/$script"
+ echo " wrote static/$script"
+done
+
+# --- 2. Compute checksums from the Maven Central -bin archives ---------------
+tar_url="$MAVEN_BASE/$version/camel-launcher-$version-bin.tar.gz"
+zip_url="$MAVEN_BASE/$version/camel-launcher-$version-bin.zip"
+echo "Downloading $tar_url"
+curl -fsSL "$tar_url" -o "$tmp/archive.tar.gz" || die "could not download
$tar_url"
+echo "Downloading $zip_url"
+curl -fsSL "$zip_url" -o "$tmp/archive.zip" || die "could not download
$zip_url"
+tar_sha256=$(sha256 "$tmp/archive.tar.gz")
+zip_sha256=$(sha256 "$tmp/archive.zip")
+
+manifest_body() {
+ printf 'format=1\nversion=%s\ntar_sha256=%s\nzip_sha256=%s\n' \
+ "$version" "$tar_sha256" "$zip_sha256"
+}
+
+write_manifest() {
+ dest=$1
+ tmpf=$(mktemp "$tmp/manifest.XXXXXX")
+ manifest_body >"$tmpf"
+ mv "$tmpf" "$dest"
+ echo " wrote ${dest#"$repo_root"/}"
+}
+
+mkdir -p "$releases_dir"
+version_file="$releases_dir/$version.properties"
+
+# --- 3. Per-version manifest (immutable) ------------------------------------
+if [ -f "$version_file" ]; then
+ if manifest_body | cmp -s - "$version_file"; then
+ echo "$version.properties already up to date"
+ else
+ die "$version.properties exists with different checksums; per-version
manifests are immutable"
+ fi
+else
+ write_manifest "$version_file"
+fi
+
+# --- 4. latest.properties (forward-only) ------------------------------------
+if [ "$update_latest" -eq 1 ]; then
+ latest_file="$releases_dir/latest.properties"
+ if [ -f "$latest_file" ]; then
+ current=$(sed -n 's/^version=//p' "$latest_file")
+ if [ "$current" = "$version" ]; then
+ echo "latest.properties already at $version"
+ elif version_gt "$version" "$current"; then
+ write_manifest "$latest_file"
+ else
+ die "latest.properties already at $current (newer than $version);
refusing to move backward"
+ fi
+ else
+ write_manifest "$latest_file"
+ fi
+fi
+
+echo "Done."