diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2e95015..28f46ce 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,6 +24,13 @@ on: description: Mark the VSIX as a pre-release. type: boolean default: false + extension-targets: + # Every VSIX bundles a platform-specific JRE, so a release has to build + # one package per platform. That is ~125 MB each, which is why anything + # that is not a release asks for the host alone. + description: 'Platforms to package the extension for: host, all, or a comma-separated list.' + type: string + default: host test-extension: description: Run the VS Code extension test suites. type: boolean @@ -42,6 +49,9 @@ on: maven-cache-key: description: Cache key holding the Maven dependencies, for pruning. value: ${{ jobs.build.outputs.maven-cache-key }} + temurin-cache-key: + description: Cache key holding the bundled Java runtimes, for pruning. + value: ${{ jobs.build.outputs.temurin-cache-key }} cli-version: description: The osate-cli version from the build provenance. value: ${{ jobs.build.outputs.cli-version }} @@ -65,6 +75,7 @@ jobs: osate-sha: ${{ steps.pin.outputs.sha }} osate-cache-key: ${{ steps.keys.outputs.osate }} maven-cache-key: ${{ steps.keys.outputs.maven }} + temurin-cache-key: ${{ steps.keys.outputs.temurin }} cli-version: ${{ steps.versions.outputs.cli }} extension-version: ${{ steps.versions.outputs.extension }} @@ -102,9 +113,11 @@ jobs: env: OSATE_KEY: osate-${{ runner.os }}-${{ steps.pin.outputs.sha }}-v1 MAVEN_KEY: maven-${{ runner.os }}-${{ steps.pin.outputs.sha }}-${{ hashFiles('**/pom.xml') }} + TEMURIN_KEY: temurin-${{ runner.os }}-${{ hashFiles('scripts/lib/temurin.sh') }} run: | echo "osate=$OSATE_KEY" >> "$GITHUB_OUTPUT" echo "maven=$MAVEN_KEY" >> "$GITHUB_OUTPUT" + echo "temurin=$TEMURIN_KEY" >> "$GITHUB_OUTPUT" # Third-party downloads, above all Tycho's resolution of the Eclipse target # platform. Safe to reuse partially, so prefix fallback is enabled. org/osate @@ -203,6 +216,21 @@ jobs: fi echo "osate2 pin: \`${{ steps.pin.outputs.sha }}\`" >> "$GITHUB_STEP_SUMMARY" + # The bundled JREs the extension packages: ~50 MB per platform, so a release + # that builds all six downloads ~300 MB from Adoptium. Keyed on the helper + # that owns the feature version. Safe to reuse loosely — the helper verifies + # every archive against Adoptium's published checksum and re-downloads one + # that a floating GA release has superseded. + - name: Restore the bundled Java runtimes + id: temurin + if: inputs.build-extension + uses: actions/cache/restore@v5 + with: + path: target/temurin-downloads + key: temurin-${{ runner.os }}-${{ hashFiles('scripts/lib/temurin.sh') }} + restore-keys: | + temurin-${{ runner.os }}- + # Build only what the caller asked for. A release of one component has no # reason to build the other two. - name: Build and test @@ -219,6 +247,9 @@ jobs: if [ "${{ inputs.extension-pre-release }}" = "true" ]; then args+=(--extension-pre-release) fi + if [ "${{ inputs.build-extension }}" = "true" ]; then + args+=(--extension-targets "${{ inputs.extension-targets }}") + fi printf 'build-test-release %s\n' "${args[*]}" ./scripts/build-test-release "${args[@]}" @@ -260,12 +291,16 @@ jobs: aadl-language-server/releng/org.osate.aadl.ls.repository/target/*.zip target/build-provenance.properties + # One VSIX per platform, each ~125 MB because of its bundled runtime, so + # these are not kept for the default 90 days. A release publishes from them + # within the same workflow run. - name: Upload the VS Code extension if: inputs.upload-artifacts && inputs.build-extension uses: actions/upload-artifact@v6 with: name: vscode-extension if-no-files-found: error + retention-days: 7 path: vscode-extension/aadl2-*.vsix - name: Upload the CLI distribution @@ -298,3 +333,10 @@ jobs: ~/.m2/repository !~/.m2/repository/org/osate key: maven-${{ runner.os }}-${{ steps.pin.outputs.sha }}-${{ hashFiles('**/pom.xml') }} + + - name: Save the bundled Java runtimes + if: always() && inputs.build-extension && steps.temurin.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: target/temurin-downloads + key: temurin-${{ runner.os }}-${{ hashFiles('scripts/lib/temurin.sh') }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7fe32fd..6aded0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,6 +67,7 @@ jobs: GH_TOKEN: ${{ github.token }} OSATE_KEEP: ${{ needs.build.outputs.osate-cache-key }} MAVEN_KEEP: ${{ needs.build.outputs.maven-cache-key }} + TEMURIN_KEEP: ${{ needs.build.outputs.temurin-cache-key }} steps: - name: Delete superseded cache entries run: | @@ -95,6 +96,7 @@ jobs: prune "osate-${{ runner.os }}-" "$OSATE_KEEP" prune "maven-${{ runner.os }}-" "$MAVEN_KEEP" + prune "temurin-${{ runner.os }}-" "$TEMURIN_KEEP" # Pull-request caches live on their own refs, which the prune above # deliberately does not touch, and they are the bulk of the usage: one diff --git a/.github/workflows/release-osate-cli.yml b/.github/workflows/release-osate-cli.yml index a08b2d6..e7f1ccf 100644 --- a/.github/workflows/release-osate-cli.yml +++ b/.github/workflows/release-osate-cli.yml @@ -119,6 +119,19 @@ jobs: -Djavadoc=false -DfailIfNoTests=false \ clean install + # The four bundled JREs, ~190 MB from Adoptium on every release otherwise. + # Keyed on the helper that owns the feature version and shared in spirit with + # build.yml's cache; the paths differ because each packaging path keeps its + # own download directory. Loose reuse is safe: the helper verifies every + # archive against Adoptium's published checksum. + - name: Cache the bundled Java runtimes + uses: actions/cache@v5 + with: + path: osate-cli/packaging/target/downloads + key: temurin-cli-${{ runner.os }}-${{ hashFiles('scripts/lib/temurin.sh') }} + restore-keys: | + temurin-cli-${{ runner.os }}- + - name: Build the tooling and CLI run: ./scripts/build-test-release --skip-osate --skip-extension diff --git a/.github/workflows/release-vscode.yml b/.github/workflows/release-vscode.yml index b0f4d48..5ffea40 100644 --- a/.github/workflows/release-vscode.yml +++ b/.github/workflows/release-vscode.yml @@ -76,12 +76,18 @@ jobs: # Decided at build time: vsce refuses to publish a package as a pre-release # unless the marker is already in its manifest. extension-pre-release: ${{ needs.verify-version.outputs.pre-release == 'true' }} + # Every package carries its own platform's Java runtime, so a release has to + # build all of them. Clients are offered the package matching their platform + # and nothing at all on a platform we do not build. + extension-targets: all publish: name: Publish needs: [verify-version, build] runs-on: ubuntu-latest - timeout-minutes: 20 + # Six platform packages of ~125 MB each are uploaded twice over, to the + # Marketplace and to Open VSX. + timeout-minutes: 45 permissions: contents: write env: @@ -104,38 +110,69 @@ jobs: name: vscode-extension path: dist - - name: Check the VSIX + - name: Check the VSIX set run: | - vsix="dist/aadl2-${VERSION}.vsix" - if [ ! -f "$vsix" ]; then - echo "Expected $vsix; got:" >&2 - ls -l dist >&2 - exit 1 - fi - # The server plug-ins reach the VSIX through a symlink that vsce - # dereferences. An empty server/aadl/lib means the package is useless. - count=$(unzip -l "$vsix" | grep -c 'extension/server/aadl/lib/.*\.jar') - echo "$count server plug-in jars in the VSIX" - if [ "$count" -lt 100 ]; then - echo "Too few server plug-ins; the symlink did not resolve." >&2 - exit 1 - fi + # Every supported platform must be present before anything is published. + # Marketplace versions are immutable, so publishing some targets and + # failing on the rest would leave platforms permanently on different + # versions of the extension. + targets=$(vscode-extension/packaging/scripts/stage-runtime --all-targets) + paths=() + for target in $targets; do + vsix="dist/aadl2-${target}-${VERSION}.vsix" + if [ ! -f "$vsix" ]; then + echo "Expected $vsix; got:" >&2 + ls -l dist >&2 + exit 1 + fi - # The marker is baked in at package time, so confirm the package matches - # what the tag asked for. vsce would reject a mismatch in one direction - # and silently publish a pre-release as stable in the other. - if unzip -p "$vsix" extension.vsixmanifest | grep -q 'Microsoft.VisualStudio.Code.PreRelease'; then - marked=true - else - marked=false - fi - echo "packaged as pre-release: $marked (tag asked for $PRE_RELEASE)" - if [ "$marked" != "$PRE_RELEASE" ]; then - echo "VSIX pre-release marker does not match the tag." >&2 - exit 1 - fi + # The server plug-ins and the bundled runtime both reach the VSIX + # through symlinks that vsce dereferences. Either one missing makes the + # package useless, and neither shows up as a packaging failure. + jars=$(unzip -l "$vsix" | grep -c 'extension/server/aadl/lib/.*\.jar' || true) + if [ "$jars" -lt 100 ]; then + echo "$vsix holds only $jars server plug-ins; the symlink did not resolve." >&2 + exit 1 + fi + + case "$target" in + win32-*) java_entry='extension/runtime/bin/java\.exe$' ;; + *) java_entry='extension/runtime/bin/java$' ;; + esac + # grep -c, not -q: -q closes the pipe and the resulting SIGPIPE fails + # unzip, which under pipefail fails the step. + if [ "$(unzip -l "$vsix" | grep -cE "$java_entry" || true)" -eq 0 ]; then + echo "$vsix has no bundled Java runtime." >&2 + exit 1 + fi + + manifest=$(unzip -p "$vsix" extension.vsixmanifest) + + # A package built for the wrong platform would install on clients that + # cannot run its runtime. + if ! printf '%s' "$manifest" | grep -q "TargetPlatform=\"$target\""; then + echo "$vsix is not marked for $target." >&2 + exit 1 + fi + + # The marker is baked in at package time, so confirm the package matches + # what the tag asked for. vsce would reject a mismatch in one direction + # and silently publish a pre-release as stable in the other. + if printf '%s' "$manifest" | grep -q 'Microsoft.VisualStudio.Code.PreRelease'; then + marked=true + else + marked=false + fi + if [ "$marked" != "$PRE_RELEASE" ]; then + echo "$vsix pre-release marker is $marked; the tag asked for $PRE_RELEASE." >&2 + exit 1 + fi + + echo "$vsix: $jars server plug-ins, bundled runtime, pre-release=$marked" + paths+=("$vsix") + done - echo "VSIX=$vsix" >> "$GITHUB_ENV" + echo "VSIX_PATHS=${paths[*]}" >> "$GITHUB_ENV" - name: Create the GitHub release if: github.ref_type == 'tag' @@ -146,12 +183,13 @@ jobs: if [ "$PRE_RELEASE" = "true" ]; then args+=(--prerelease) fi + # shellcheck disable=SC2086 # the paths are a deliberate word list gh release create "$GITHUB_REF_NAME" \ --repo "$GITHUB_REPOSITORY" \ --title "AADL2 VS Code extension $VERSION" \ --generate-notes \ "${args[@]+"${args[@]}"}" \ - "$VSIX" + $VSIX_PATHS - name: Install publishing tools if: github.ref_type == 'tag' && (env.VSCE_PAT != '' || env.OVSX_PAT != '') @@ -168,7 +206,15 @@ jobs: if [ "$PRE_RELEASE" = "true" ]; then args+=(--pre-release) fi - npx --no-install vsce publish --packagePath "../$VSIX" "${args[@]+"${args[@]}"}" + paths=() + for vsix in $VSIX_PATHS; do + paths+=("../$vsix") + done + # One invocation with every package: --packagePath is variadic, and + # publishing the platforms together is the closest thing to an atomic + # release the Marketplace offers. --pre-release comes first because the + # variadic option would otherwise swallow it. + npx --no-install vsce publish "${args[@]+"${args[@]}"}" --packagePath "${paths[@]}" # Requires the "osate" namespace claimed at open-vsx.org and its token # stored as OVSX_PAT. @@ -182,7 +228,26 @@ jobs: if [ "$PRE_RELEASE" = "true" ]; then args+=(--pre-release) fi - npx --yes ovsx publish "../$VSIX" --pat "$OVSX_PAT" "${args[@]+"${args[@]}"}" + paths=() + for vsix in $VSIX_PATHS; do + paths+=("../$vsix") + done + # Retried per package: six ~125 MB uploads is the flakiest step in this + # workflow, and a re-run of the whole job cannot republish what already + # landed, because a published version is permanent. + for path in "${paths[@]}"; do + for attempt in 1 2 3; do + if npx --yes ovsx publish "$path" --pat "$OVSX_PAT" "${args[@]+"${args[@]}"}"; then + break + fi + if [ "$attempt" = 3 ]; then + echo "Open VSX publish failed for $path." >&2 + exit 1 + fi + echo "Retrying $path in 30s" + sleep 30 + done + done - name: Note skipped publications if: github.ref_type == 'tag' diff --git a/AGENTS.md b/AGENTS.md index 84fa922..9e954db 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,7 +64,8 @@ the VSIX before those plug-ins exist. Important outputs: - `aadl-language-server/releng/org.osate.aadl.ls.repository/target/repository/` -- `vscode-extension/aadl2-*.vsix` +- `vscode-extension/aadl2--.vsix` (host platform by default; + `--extension-targets all` builds every published platform) - `osate-cli/dist/target/dist/` Run Maven/Tycho builds, CLI integration tests, VS Code integration tests, and @@ -126,6 +127,12 @@ owns the OSATE cache and delegates the rest to `scripts/build-test-release`; build and a VSIX that silently ships no server or a stale one. - Keep the plug-in exclusion lists in `osate-cli/dist/pom.xml` and `vscode-extension/.vscodeignore` synchronized. +- Both deliverables bundle an Eclipse Temurin JRE through + `scripts/lib/temurin.sh`. The VS Code extension runs that runtime and nothing + else, so every VSIX is platform-specific: `vsce package --target` produces one + package per platform and no universal fallback is published. A build that does + not stage a runtime cannot produce a working package, which is why the + packaging script, not Maven, drives the per-target loop. - The CLI workspace server loads language-server plug-ins from sibling JARs using an isolated `URLClassLoader`. Do not shade it or nest the plug-in JARs. - Protocol-visible command changes may require coordinated updates to the diff --git a/RELEASING.md b/RELEASING.md index 29c5aa5..ec33320 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -79,10 +79,23 @@ so a missing packager cannot silently drop the four Linux packages. **`vscode-v*`** — [`release-vscode.yml`](.github/workflows/release-vscode.yml) -`aadl2-.vsix` attached to a GitHub Release, then published to the VS -Code Marketplace and Open VSX. The VSIX is checked for a plausible number of -bundled server plug-ins first, because the server reaches it through a symlink -that would otherwise fail silently. +Six platform packages — `aadl2--.vsix` for darwin-x64, +darwin-arm64, linux-x64, linux-arm64, win32-x64 and win32-arm64 — attached to a +GitHub Release, then published to the VS Code Marketplace and Open VSX. Each one +embeds an Eclipse Temurin 21 JRE for its platform, which is why there is a package +per platform and no universal one. + +Every package is checked before anything is published: a plausible number of +server plug-ins, the bundled runtime, the declared target platform, and the +pre-release marker. The plug-ins and the runtime both reach the VSIX through +symlinks that would otherwise fail silently, and Marketplace versions are +immutable — publishing three targets and then failing would leave platforms +stranded on different versions for good. For the same reason all six go to the +Marketplace in a single `vsce publish` invocation. + +Clients on a platform we do not build for — Alpine Linux, 32-bit ARM, and the web +— are offered nothing at all, because no untargeted fallback package is +published. ### Stable or pre-release @@ -129,6 +142,10 @@ To produce a pre-release VSIX locally: ./scripts/build-test-release --skip-osate --extension-pre-release ``` +That packages the host platform only. Add `--extension-targets all` to reproduce +the full release set; each package downloads its platform's JRE once and is then +cached under `target/temurin-downloads`. + **`ls-v*`** — [`release-server.yml`](.github/workflows/release-server.yml) The p2 repository archive plus `build-provenance.properties`, attached to a @@ -179,8 +196,13 @@ gh workflow run release-osate-cli.yml --repo osate/aadl-tooling --ref main - The `osate2` submodule pin is part of every release. `build-provenance.properties` records the tooling commit, the OSATE commit and gitlink, and all three versions, so a released artifact can always be traced back to its exact inputs. -- macOS tarballs are unsigned and not notarized. Downloads through a browser will - be quarantined by Gatekeeper; the Homebrew path is not affected. The `.deb` and - `.rpm` packages are unsigned too. +- macOS **tarballs** are unsigned and not notarized. Downloads through a browser + will be quarantined by Gatekeeper; the Homebrew path is not affected. The `.deb` + and `.rpm` packages are unsigned too. This does not apply to the VS Code + packages: Adoptium's own per-binary signatures travel inside the JRE, and VS + Code does not quarantine the files it extracts from a VSIX. +- The bundled JREs are verified against Adoptium's published checksums at build + time, and `build-provenance.properties` records the vendor, feature version and + resolved runtime version that shipped. - Release runs reuse the cached OSATE build when the submodule pin has not moved. A release right after a submodule bump pays for a full OSATE build. diff --git a/osate-cli/packaging/README.md b/osate-cli/packaging/README.md index d4a612f..ed430ce 100644 --- a/osate-cli/packaging/README.md +++ b/osate-cli/packaging/README.md @@ -101,6 +101,11 @@ writes tarballs under: osate-cli/packaging/target/artifacts/ ``` +The download, checksum verification against Adoptium's published digest, and +unpacking live in `scripts/lib/temurin.sh`, shared with VS Code extension +packaging, which bundles the same runtimes. `TEMURIN_FEATURE_VERSION` in +`metadata.env` still pins the version used here. + If `nfpm` is on `PATH`, Linux `.deb` and `.rpm` packages are also built. Use `--nfpm` to require nFPM, or `--no-nfpm` to skip native Linux packages. diff --git a/osate-cli/packaging/scripts/build-release-artifacts.sh b/osate-cli/packaging/scripts/build-release-artifacts.sh index 43855ae..a08f004 100755 --- a/osate-cli/packaging/scripts/build-release-artifacts.sh +++ b/osate-cli/packaging/scripts/build-release-artifacts.sh @@ -25,6 +25,9 @@ set -euo pipefail script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) # shellcheck source=common.sh source "$script_dir/common.sh" +# Shared with VS Code extension packaging, which bundles the same runtimes. +# shellcheck source=../../../scripts/lib/temurin.sh +source "$repo_root/scripts/lib/temurin.sh" default_targets=(macos-x64 macos-arm64 linux-x64 linux-arm64) targets=() @@ -152,32 +155,15 @@ mkdir -p "$downloads_dir" "$staging_dir" "$artifacts_dir" "$generated_dir" # needing the dist tree. printf '%s\n' "$OSATE_CLI_VERSION" > "$artifacts_dir/VERSION" -temurin_download_url() { - local target=$1 - local adoptium_os adoptium_arch - adoptium_os=$(target_adoptium_os "$target") - adoptium_arch=$(target_adoptium_arch "$target") - printf 'https://api.adoptium.net/v3/binary/latest/%s/ga/%s/%s/jre/hotspot/normal/eclipse?project=jdk\n' \ - "$java_feature_version" "$adoptium_os" "$adoptium_arch" -} - download_temurin() { local target=$1 local ext=$2 - local archive="$downloads_dir/temurin-${java_feature_version}-${target}.${ext}" - local tmp="$archive.tmp" - local url - if [ -f "$archive" ]; then - printf '%s\n' "$archive" - return - fi - - url=$(temurin_download_url "$target") - echo "Downloading Eclipse Temurin $java_feature_version JRE for $target" >&2 - curl -fL --retry 3 --retry-delay 2 -o "$tmp" "$url" - mv "$tmp" "$archive" - printf '%s\n' "$archive" + temurin_download "$java_feature_version" \ + "$(target_adoptium_os "$target")" \ + "$(target_adoptium_arch "$target")" \ + "$ext" \ + "$downloads_dir" } extract_runtime() { @@ -185,18 +171,12 @@ extract_runtime() { local archive=$2 local extract_dir=$3 local runtime_dir=$4 - local java_name="java" - local java_bin runtime_home - - rm -rf "$extract_dir" "$runtime_dir" - mkdir -p "$extract_dir" "$runtime_dir" + local runtime_home - require_command tar - tar -xzf "$archive" -C "$extract_dir" + runtime_home=$(temurin_unpack "$archive" "$extract_dir" java) - java_bin=$(find "$extract_dir" -path "*/bin/$java_name" -print -quit) - [ -n "$java_bin" ] || die "could not find $java_name in extracted runtime for $target" - runtime_home=$(cd "$(dirname "$java_bin")/.." && pwd) + rm -rf "$runtime_dir" + mkdir -p "$runtime_dir" cp -R "$runtime_home/." "$runtime_dir/" } diff --git a/scripts/build-test-release b/scripts/build-test-release index dc2ab90..98f6a5c 100755 --- a/scripts/build-test-release +++ b/scripts/build-test-release @@ -32,11 +32,17 @@ plugins_dir="${repo_root}/aadl-language-server/releng/org.osate.aadl.ls.reposito extension_dir="${repo_root}/vscode-extension" provenance_dir="${repo_root}/target" +# For TEMURIN_FEATURE_VERSION, so the bundled runtime recorded in the provenance +# file comes from the same place the packaging scripts read it from. +# shellcheck source=lib/temurin.sh +source "${repo_root}/scripts/lib/temurin.sh" + skip_osate=false skip_extension=false skip_extension_tests=false skip_cli=false extension_pre_release=false +extension_targets=host usage() { cat <<'EOF' @@ -62,6 +68,12 @@ Options: the package manifest and refuses to publish a package as a pre-release unless it was built as one, so this has to be decided at build time. + --extension-targets Which platforms to package the extension for: "host" + (default), "all", or a comma-separated list of VS Code + platform ids. Each VSIX bundles a platform-specific + Java runtime, so there is no universal package; a + release needs "all", and everything else wants the + host alone. -h, --help Show this help. EOF } @@ -84,6 +96,14 @@ while [[ $# -gt 0 ]]; do --extension-pre-release) extension_pre_release=true ;; + --extension-targets) + if [[ $# -lt 2 ]]; then + echo "--extension-targets requires a value" >&2 + exit 2 + fi + extension_targets=$2 + shift + ;; -h | --help) usage exit 0 @@ -194,12 +214,12 @@ mvn \ if [[ "${skip_extension}" == true ]]; then echo "Skipping VS Code extension" else - extension_args=() + extension_args=(-Dvsce.package.targets="${extension_targets}") if [[ "${extension_pre_release}" == true ]]; then - echo "Building VS Code extension (pre-release)" + echo "Building VS Code extension (pre-release) for ${extension_targets}" extension_args+=(-Dvsce.package.script=package:pre-release) else - echo "Building VS Code extension" + echo "Building VS Code extension for ${extension_targets}" fi mvn \ @@ -285,6 +305,71 @@ if [[ "${antlr_osgi_count}" != "1" || "${antlr3_count}" != "1" ]]; then exit 1 fi +# Every VSIX must carry a Java runtime for its own platform, and the host's must +# actually run. The runtime reaches the VSIX through the same kind of symlink as +# the server plug-ins, so nothing but looking inside the package proves it landed. +if [[ "${skip_extension}" == false ]]; then + host_target=$("${extension_dir}/packaging/scripts/stage-runtime" --host-target) + shopt -s nullglob + vsix_files=("${extension_dir}"/aadl2-*.vsix) + shopt -u nullglob + + if [[ ${#vsix_files[@]} -eq 0 ]]; then + echo "No VSIX was produced in ${extension_dir}." >&2 + exit 1 + fi + + for vsix in "${vsix_files[@]}"; do + vsix_name=$(basename "${vsix}") + vsix_target=${vsix_name#aadl2-} + vsix_target=${vsix_target%-*} + + java_entry='extension/runtime/bin/java' + java_pattern='extension/runtime/bin/java$' + if [[ "${vsix_target}" == win32-* ]]; then + java_entry='extension/runtime/bin/java.exe' + java_pattern='extension/runtime/bin/java\.exe$' + fi + + # grep -c rather than -q: -q closes the pipe on the first match, and the + # SIGPIPE that gives unzip fails the whole pipeline under pipefail. + java_count=$(unzip -l "${vsix}" | grep -cE "${java_pattern}" || true) + if [[ "${java_count}" -eq 0 ]]; then + echo "${vsix_name} has no bundled Java runtime at ${java_entry}." >&2 + exit 1 + fi + + jar_count=$(unzip -l "${vsix}" | grep -c 'extension/server/aadl/lib/.*\.jar' || true) + if [[ "${jar_count}" -lt 100 ]]; then + echo "${vsix_name} holds only ${jar_count} server plug-ins; the symlink did not resolve." >&2 + exit 1 + fi + + echo " ${vsix_name}: ${jar_count} server plug-ins, bundled ${java_entry##*/}" + done + + # The staged runtime is the one the extension tests just ran against and the + # one the host's VSIX contains, so running it here is the end of that chain. + staged_java="${extension_dir}/runtime/bin/java" + if [[ ! -x "${staged_java}" ]]; then + echo "No executable runtime staged at ${staged_java} for ${host_target}." >&2 + exit 1 + fi + # Cleared the same three variables the extension clears before launching the + # server, so this reports the runtime rather than whatever the machine injects. + bundled_java_version=$( + env -u JAVA_TOOL_OPTIONS -u _JAVA_OPTIONS -u JDK_JAVA_OPTIONS \ + "${staged_java}" -version 2>&1 | + sed -n 's/.*version "\([^"]*\)".*/\1/p' | + head -n 1 + ) + if [[ -z "${bundled_java_version}" ]]; then + echo "The staged runtime at ${staged_java} did not report a version." >&2 + exit 1 + fi + echo " staged runtime for ${host_target}: Java ${bundled_java_version}" +fi + tooling_version=$(mvn -q -f "${repo_root}/pom.xml" help:evaluate \ -Dexpression=project.version -DforceStdout) cli_version=$(mvn -q -f "${repo_root}/osate-cli/pom.xml" help:evaluate \ @@ -298,13 +383,25 @@ mkdir -p "${provenance_dir}" printf 'osate.gitlink=%s\n' "${expected_osate_sha}" printf 'osate.version=%s\n' "${osate_version}" printf 'cli.version=%s\n' "${cli_version}" + # Which platforms were packaged, and the runtime they carry. The bundled JRE is + # part of what shipped, so it belongs in the record of what a release was built + # from; "latest GA" floats, which is exactly why the feature version alone is + # not enough to reconstruct a build. + if [[ "${skip_extension}" == true ]]; then + printf 'extension.targets=none\n' + else + printf 'extension.targets=%s\n' "${extension_targets}" + printf 'java.vendor=Eclipse Temurin\n' + printf 'java.feature.version=%s\n' "${TEMURIN_FEATURE_VERSION}" + printf 'java.runtime.version=%s\n' "${bundled_java_version}" + fi printf 'build.timestamp=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" } > "${provenance_dir}/build-provenance.properties" echo "Language-server repository:" echo " ${repo_root}/aadl-language-server/releng/org.osate.aadl.ls.repository/target/repository" -echo "VS Code extension:" -echo " ${repo_root}/vscode-extension/aadl2-*.vsix" +echo "VS Code extension (one platform-specific package per target):" +echo " ${repo_root}/vscode-extension/aadl2--.vsix" echo "CLI distribution:" echo " ${repo_root}/osate-cli/dist/target/dist" echo "Build provenance:" diff --git a/scripts/lib/temurin.sh b/scripts/lib/temurin.sh new file mode 100644 index 0000000..90ef84a --- /dev/null +++ b/scripts/lib/temurin.sh @@ -0,0 +1,187 @@ +#!/usr/bin/env bash + +# Copyright (c) 2004-2026 Carnegie Mellon University and others. (see Contributors file). +# All Rights Reserved. +# +# NO WARRANTY. ALL MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY +# KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE +# OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT +# MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. +# +# This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0/ +# SPDX-License-Identifier: EPL-2.0 +# +# Created, in part, with funding and support from the United States Government. (see Acknowledgments file). +# +# This program includes and/or can make use of certain third party source code, object code, documentation and other +# files ("Third Party Software"). The Third Party Software that is used by this program is dependent upon your system +# configuration. By using this program, You agree to comply with any and all relevant Third Party Software terms and +# conditions contained in any such Third Party Software or separate license file distributed with such Third Party +# Software. The parties who own the Third Party Software ("Third Party Licensors") are intended third party beneficiaries +# to this license with respect to the terms applicable to their Third Party Software. Third Party Software licenses +# only apply to the Third Party Software and not any other portion of this program or this program as a whole. + +# Downloads and unpacks Eclipse Temurin JREs from Adoptium. +# +# Sourced by both bundling paths: osate-cli release packaging and VS Code +# extension packaging. It takes Adoptium coordinates (os, architecture, archive +# extension, java executable name) rather than either caller's target ids, so +# neither naming scheme leaks in here. Every function is prefixed and defines +# its own diagnostics, so sourcing it cannot collide with a caller's helpers. + +# The feature version both paths bundle. osate-cli pins its own value in +# osate-cli/packaging/metadata.env, which is sourced before this file and is +# therefore left alone; this default serves callers that have no metadata of +# their own. It must stay >= minimumJavaMajorVersion in +# vscode-extension/src/javaRuntime.ts. +: "${TEMURIN_FEATURE_VERSION:=21}" + +temurin_die() { + echo "error: $*" >&2 + exit 1 +} + +temurin_warn() { + echo "warning: $*" >&2 +} + +temurin_require_command() { + command -v "$1" >/dev/null 2>&1 || temurin_die "required command not found: $1" +} + +temurin_sha256() { + local 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 }' + else + temurin_die "neither sha256sum nor shasum is available" + fi +} + +temurin_download_url() { + local feature=$1 os=$2 arch=$3 + printf 'https://api.adoptium.net/v3/binary/latest/%s/ga/%s/%s/jre/hotspot/normal/eclipse?project=jdk\n' \ + "$feature" "$os" "$arch" +} + +# The checksum Adoptium publishes for the archive we are about to fetch. Prints +# nothing when the API cannot be reached or the response has no archive entry, +# which is what lets an offline rebuild proceed from a cached archive. +# +# The assets response holds several binaries per platform (a .tar.gz or .zip +# archive plus a .pkg or .msi installer), each with its own checksum, so the one +# we want is selected by the archive's file name. Collapsing the whitespace and +# splitting on '}' puts each binary's checksum and name on one line, which is +# what makes this a sed job rather than a JSON parse. +temurin_expected_checksum() { + local feature=$1 os=$2 arch=$3 ext=$4 + local url="https://api.adoptium.net/v3/assets/latest/${feature}/hotspot?os=${os}&architecture=${arch}&image_type=jre&vendor=eclipse" + + curl -fsSL --retry 2 --retry-delay 2 "$url" 2>/dev/null | + tr -d ' \t\n' | + tr '}' '\n' | + grep "\"name\":\"[^\"]*\.${ext}\"" | + sed -n 's/.*"checksum":"\([0-9a-f]\{64\}\)".*/\1/p' | + head -n 1 +} + +# Prints the path to a verified archive, downloading it only when the cache does +# not already hold it. "latest GA" floats, so a cached archive whose digest no +# longer matches the published one is a superseded build rather than corruption: +# fetch once more, then insist. +temurin_download() { + local feature=$1 os=$2 arch=$3 ext=$4 cache_dir=$5 + local archive="$cache_dir/temurin-${feature}-${os}-${arch}.${ext}" + local expected actual url attempt + + temurin_require_command curl + temurin_require_command awk + mkdir -p "$cache_dir" + + expected=$(temurin_expected_checksum "$feature" "$os" "$arch" "$ext") + url=$(temurin_download_url "$feature" "$os" "$arch") + + for attempt in 1 2; do + if [ ! -f "$archive" ]; then + echo "Downloading Eclipse Temurin $feature JRE for $os/$arch" >&2 + curl -fL --retry 3 --retry-delay 2 -o "$archive.tmp" "$url" + mv "$archive.tmp" "$archive" + fi + + if [ -z "$expected" ]; then + temurin_warn "could not read the published checksum for $os/$arch; using $archive unverified" + printf '%s\n' "$archive" + return + fi + + actual=$(temurin_sha256 "$archive") + if [ "$actual" = "$expected" ]; then + printf '%s\n' "$archive" + return + fi + + if [ "$attempt" = 1 ]; then + temurin_warn "cached $archive does not match the published checksum; downloading it again" + rm -f "$archive" + fi + done + + temurin_die "checksum mismatch for $archive: expected $expected, got $actual" +} + +# Unpacks an archive and prints the Java home inside it. +# +# The macOS archives nest the runtime under Contents/Home while the others do +# not, so the home is located by finding the java executable instead of being +# assumed: every caller then sees the same bin/ layout. Extraction is +# skipped when the directory already holds this exact archive, which keeps +# repeated staging in a local build loop cheap. +temurin_unpack() { + local archive=$1 extract_dir=$2 java_exe=$3 + local digest marker java_bin dangling + + digest=$(temurin_sha256 "$archive") + marker="$extract_dir/.temurin-unpacked" + java_bin="" + + # The marker records which archive was unpacked here, but something else may + # have disturbed the tree since — a clean that followed a symlink into it, for + # instance. Re-extract unless the executable is actually still there, so a + # damaged cache heals itself instead of failing every later build. + if [ -f "$marker" ] && [ "$(cat "$marker")" = "$digest" ]; then + java_bin=$(find "$extract_dir" -path "*/bin/$java_exe" -print -quit 2>/dev/null || true) + fi + + if [ -z "$java_bin" ]; then + rm -rf "$extract_dir" + mkdir -p "$extract_dir" + case "$archive" in + *.zip) + temurin_require_command unzip + unzip -q "$archive" -d "$extract_dir" + ;; + *.tar.gz) + temurin_require_command tar + tar -xzf "$archive" -C "$extract_dir" + ;; + *) + temurin_die "unsupported archive type: $archive" + ;; + esac + printf '%s\n' "$digest" > "$marker" + fi + + java_bin=$(find "$extract_dir" -path "*/bin/$java_exe" -print -quit) + [ -n "$java_bin" ] || temurin_die "could not find bin/$java_exe in $archive" + + # The Linux runtimes symlink most of legal/ into java.base. Packaging tools + # that dereference symlinks fail on a broken one, so refuse a runtime that + # would only fall over later. + dangling=$(find "$(dirname "$java_bin")/.." -type l ! -exec test -e {} \; -print) + [ -z "$dangling" ] || temurin_die "dangling symlinks in the unpacked runtime: $dangling" + + (cd "$(dirname "$java_bin")/.." && pwd) +} diff --git a/vscode-extension/.gitignore b/vscode-extension/.gitignore index d0f44d1..090bcec 100644 --- a/vscode-extension/.gitignore +++ b/vscode-extension/.gitignore @@ -3,6 +3,8 @@ dist node_modules node target +# Symlink to the staged Eclipse Temurin JRE; see packaging/scripts/stage-runtime. +runtime .vscode-test/ *.vsix tsconfig.tsbuildinfo diff --git a/vscode-extension/.vscodeignore b/vscode-extension/.vscodeignore index afbff4a..9dab0c1 100644 --- a/vscode-extension/.vscodeignore +++ b/vscode-extension/.vscodeignore @@ -7,7 +7,12 @@ pom.xml .npmrc node/** target/** +packaging/** src/** +# Gradle start scripts that search JAVA_HOME and PATH for a JVM. The extension +# launches the server itself with the bundled runtime and never uses these, and +# shipping them would offer a second, unsupported way to start it. +server/aadl/bin/** .gitignore .yarnrc vsc-extension-quickstart.md diff --git a/vscode-extension/AGENTS.md b/vscode-extension/AGENTS.md index 8b882b8..59e942b 100644 --- a/vscode-extension/AGENTS.md +++ b/vscode-extension/AGENTS.md @@ -27,16 +27,24 @@ Guidance for the TypeScript VS Code extension. ## Runtime contract -- The extension requires `redhat.java` and launches the server with that - extension's tooling JRE. Java 21 or newer is required; do not add a fallback - to another Java installation. +- The extension runs the Eclipse Temurin JRE bundled at `runtime/` and nothing + else. There is no discovery, no `JAVA_HOME` or `PATH` fallback, no dependency + on `redhat.java`, and no setting that points elsewhere: one supported runtime + means a user's failure is reproducible. Do not add a fallback. +- `JAVA_TOOL_OPTIONS`, `_JAVA_OPTIONS` and `JDK_JAVA_OPTIONS` are removed from + the server's environment. They would let a machine inject agents or repoint the + trust store into the runtime we bundle precisely to control. +- The **JRE** image is enough. It ships `libjdwp`, so the `-agentlib:jdwp` debug + launch works; do not switch to the ~40 MB larger JDK for it. +- `runtime/` is a symlink that `packaging/scripts/stage-runtime` creates, like + `server/aadl/lib`. Both reach the VSIX only because packaging passes + `--follow-symlinks`; keep it. - The bundled server is launched over stdio using `org.osate.aadl.ls.RunAadl2Server`. -- `server/aadl/lib` is a symlink to the generated language-server p2 plug-ins. - VSIX packaging must continue to use `--follow-symlinks`. -- `serverClasspath` builds an explicit classpath, excludes the incompatible - standalone ANTLR runtime bundle, and fails if `antlr-runtime-4.4.jar` is - absent. Keep its tests with any classpath change. +- `serverClasspath` builds an explicit classpath from every jar in + `server/aadl/lib`. The ANTLR 4 runtime bundle, which would collide with the + ANTLR 3 runtime Xtext parsers link against, is excluded at packaging time by + `.vscodeignore`. Keep its tests with any classpath change. - Reuse the existing file watcher and language-client lifecycle when implementing restart behavior. @@ -51,17 +59,33 @@ npm run lint npm run test:unit ``` -`npm run test:integration` downloads/launches VS Code and may install -`redhat.java`, so it needs network access. `npm test` runs unit and integration -tests. +Unit tests run with no runtime staged, so keep the bundled-runtime helpers pure. + +`npm run test:integration` stages the host runtime, then downloads and launches +VS Code, so it needs network access the first time. It runs with +`--disable-extensions`; the suite must never skip itself. + +```bash +npm run stage-runtime # host platform, into runtime/ +npm run stage-runtime linux-x64 +``` Package the extension with: ```bash -npm run package # stable +npm run package # stable, host platform only npm run package:pre-release # marked as a pre-release +AADL_VSIX_TARGETS=all npm run package ``` +Each VSIX bundles a platform-specific JRE, so there is no universal package: +`vsce package --target` produces `aadl2--.vsix` for every +supported platform (`packaging/scripts/stage-runtime --all-targets`). A client on +any other platform is offered nothing, because no untargeted fallback is +published. Maven selects the set through `-Dvsce.package.targets` (`host`, +`all`, or a list), which reaches `packaging/scripts/package-vsix` as +`AADL_VSIX_TARGETS`. + The pre-release marker is written into the VSIX manifest at package time, and `vsce publish` refuses to publish a package as a pre-release unless it was built as one, so it cannot be added later. `vscode-extension/pom.xml` selects the script @@ -82,15 +106,21 @@ no dependency edge from this module to the server and a single reactor build wit `validate` if they are missing. The Maven build installs pinned Node, runs `npm install`, compiles the -extension, and packages `aadl2-*.vsix`. If the server changed, rebuild its p2 -repository before packaging. +extension, stages the bundled runtime, and packages `aadl2-*.vsix`. If the server +changed, rebuild its p2 repository before packaging. + +`mvn clean` deliberately leaves `runtime/` alone: it is a symlink into a JRE +cache under the repository-root `target/`, and maven-clean deletes the contents +of the link's target even with `followSymlinks=false`. Staging recreates the link +on every build anyway. ## Tests -- Unit tests cover helpers, Java selection, lifecycle behavior, server - classpath construction, command arguments, symbols, and syntax grammars. +- Unit tests cover helpers, bundled-runtime resolution, lifecycle behavior, + server classpath construction, command arguments, symbols, and syntax grammars. - Integration tests cover manifest contributions, extension discovery, and - activation/language features when `redhat.java` is available. + activation/language features. They are unconditional: the runtime ships in the + extension, so there is nothing left to be absent and nothing to skip for. - Test output is compiled through `tsconfig.test.json` into `out/test/` and is excluded from the VSIX. @@ -119,6 +149,9 @@ When changing commands or settings: - Record user-facing changes in `CHANGELOG.md`. - Keep `.vscodeignore` synchronized with `osate-cli/dist/pom.xml` when changing bundled plug-in exclusions. +- The bundled runtime is downloaded through `scripts/lib/temurin.sh`, shared with + osate-cli packaging. Change the Temurin feature version there, and keep it at or + above `minimumJavaMajorVersion` in `src/javaRuntime.ts`. - Verify the generated VSIX rather than assuming a successful TypeScript compile proves packaging. diff --git a/vscode-extension/CHANGELOG.md b/vscode-extension/CHANGELOG.md index 256bcf9..444244b 100644 --- a/vscode-extension/CHANGELOG.md +++ b/vscode-extension/CHANGELOG.md @@ -29,6 +29,14 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how ## [Unreleased] +- Bundle an Eclipse Temurin 21 JRE and run the language server with it. The Red + Hat Java extension is no longer required or used, and no Java installation on + the machine is consulted. +- Publish one package per platform: macOS x64/arm64, Linux x64/arm64, and Windows + x64/arm64. The Marketplace installs the matching one automatically; an + installation from a downloaded VSIX has to match the platform. Alpine Linux and + 32-bit ARM are not supported. + ## [0.1.0] - 2026-09-02 - Align the extension version with the language server and osate-cli at 0.1.0. diff --git a/vscode-extension/README.md b/vscode-extension/README.md index 82c37f7..16a4fbe 100644 --- a/vscode-extension/README.md +++ b/vscode-extension/README.md @@ -32,11 +32,17 @@ Code. In Visual Studio Code, open the Extensions view, search for `AADL2` from the `osate` publisher, and select **Install**. +Installing from the Marketplace gets the package built for your platform, which +is what you want: each one contains its own Java runtime. + To install a downloaded release instead, open the Extensions view menu, select -**Install from VSIX...**, and choose the `aadl2-.vsix` file. +**Install from VSIX...**, and choose the `aadl2--.vsix` file +matching your operating system and processor, such as +`aadl2-darwin-arm64-.vsix`. A package for another platform installs but +cannot start the language server. -The extension automatically installs its required Red Hat Java extension -dependency. +Nothing else has to be installed. The extension carries the Java runtime it +needs, and no longer requires the Red Hat Java extension. ## Getting Started @@ -76,12 +82,16 @@ diagnostics are written to the **AADL2 Language Server** output channel. The extension requires: - Visual Studio Code 1.110 or newer -- The Red Hat Java extension (`redhat.java`), installed automatically -- Java 21 or newer in the tooling JRE provided by the Red Hat Java extension +- One of the supported platforms: macOS (Intel or Apple silicon), Linux (x64 or + arm64), or Windows (x64 or arm64) + +No Java installation is required. Every package bundles an Eclipse Temurin 21 +JRE, and the extension runs the language server with that runtime only — it never +uses `JAVA_HOME`, a Java installation on the `PATH`, or another extension's +runtime, so the server behaves the same on every machine. -The AADL extension always uses that tooling JRE to run its bundled language -server. If the JRE is unavailable or outdated, update or reinstall the Red Hat -Java extension. +Platforms outside that list, including Alpine Linux and 32-bit ARM, have no +package and cannot install the extension. ## Extension Settings @@ -132,10 +142,12 @@ restart. Open **View: Toggle Output**, select **AADL2 Language Server**, and inspect the startup message. The channel identifies the Java executable and version used to -launch the server. +launch the server, which is always the runtime inside the extension. -If the Red Hat Java extension does not provide a tooling JRE, or provides a Java -version older than 21, update or reinstall that extension. +If the error says the bundled runtime could not be run, the installed package was +almost certainly built for a different platform — most often because it was +installed from a downloaded VSIX. Reinstall from the Marketplace, which selects +the right package automatically. ### Editing results appear stale @@ -157,3 +169,12 @@ steps, and relevant output-channel messages. ## Release Notes See the [changelog](CHANGELOG.md) for release notes. + +## Third-party software + +Each package embeds an unmodified Eclipse Temurin 21 JRE from +[Adoptium](https://adoptium.net), distributed under the GNU General Public +License, version 2, with the Classpath Exception. Its own license and notice files +travel with it under `runtime/legal/` and `runtime/NOTICE` inside the installed +extension. The AADL extension itself, and the language server it runs, are covered +by [LICENSE.txt](LICENSE.txt). diff --git a/vscode-extension/package.json b/vscode-extension/package.json index 48b7d58..62e5a07 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -32,9 +32,6 @@ "activationEvents": [ "onLanguage:aadl2" ], - "extensionDependencies": [ - "redhat.java" - ], "main": "./out/extension.js", "contributes": { "languages": [ @@ -196,12 +193,13 @@ "compile": "node esbuild.js", "watch": "node esbuild.js --watch", "package-bundle": "node esbuild.js --production", - "package": "npm run package-bundle && vsce package --no-dependencies --follow-symlinks", - "package:pre-release": "npm run package-bundle && vsce package --no-dependencies --follow-symlinks --pre-release", + "stage-runtime": "packaging/scripts/stage-runtime", + "package": "npm run package-bundle && packaging/scripts/package-vsix", + "package:pre-release": "npm run package-bundle && packaging/scripts/package-vsix --pre-release", "check-types": "tsc -b ./ --noEmit", "compile-tests": "tsc -p tsconfig.test.json", "test:unit": "npm run compile-tests && mocha --ui tdd \"out/test/unit/**/*.test.js\"", - "test:integration": "npm run compile-tests && node out/test/runIntegrationTests.js", + "test:integration": "npm run compile-tests && npm run stage-runtime && node out/test/runIntegrationTests.js", "test": "npm run test:unit && npm run test:integration", "lint": "eslint src --ext ts" }, diff --git a/vscode-extension/packaging/README.md b/vscode-extension/packaging/README.md new file mode 100644 index 0000000..6b93fc9 --- /dev/null +++ b/vscode-extension/packaging/README.md @@ -0,0 +1,81 @@ + + +# Extension packaging + +The extension runs an Eclipse Temurin JRE that ships inside it, so a package is +only valid for one platform. These two scripts are what make that work; Maven +calls them through the `package` and `package:pre-release` npm scripts. + +## `scripts/stage-runtime` + +Downloads the Temurin JRE for a target and points `../runtime` at it. + +```bash +packaging/scripts/stage-runtime # host platform +packaging/scripts/stage-runtime linux-arm64 +packaging/scripts/stage-runtime --host-target # print the host's target id +packaging/scripts/stage-runtime --all-targets # print every supported target id +``` + +`runtime` is a symlink, like `server/aadl/lib`, and packaging dereferences both +with `vsce package --follow-symlinks`. Archives are cached in +`target/temurin-downloads` and unpacked under `target/temurin-runtimes/`, +both at the repository root and outside anything `mvn clean` deletes. Override +them with `TEMURIN_DOWNLOAD_DIR` and `TEMURIN_RUNTIME_DIR`. + +The download, checksum verification and unpacking are in +[`scripts/lib/temurin.sh`](../../scripts/lib/temurin.sh), shared with osate-cli +release packaging. macOS archives nest the runtime under `Contents/Home`; the +helper locates `bin/java` and stages the home it finds, so `runtime/bin/java` is +the same path on every platform. + +## `scripts/package-vsix` + +Packages one VSIX per target, staging each runtime first. + +```bash +packaging/scripts/package-vsix # host platform +packaging/scripts/package-vsix --target linux-x64 --target win32-x64 +AADL_VSIX_TARGETS=all packaging/scripts/package-vsix # every platform +packaging/scripts/package-vsix --pre-release +``` + +Two details are deliberate: + +- the host target is packaged first, because packaging is the step most likely to + fail and the host is the cheapest failure to diagnose; +- the host runtime is re-staged at the end, because the test phase runs straight + after packaging and execs `runtime/bin/java`. A foreign-architecture runtime + left behind would fail there, a long way from the cause. + +`AADL_VSIX_TARGETS` accepts `host`, `all`, or a comma- or space-separated list; +Maven passes `-Dvsce.package.targets` through it. + +## Platforms + +`darwin-x64`, `darwin-arm64`, `linux-x64`, `linux-arm64`, `win32-x64`, +`win32-arm64`. No untargeted package is published, so clients on any other +platform — Alpine Linux, 32-bit ARM, the web — are offered nothing. Adding one +means adding its Adoptium coordinates in `stage-runtime` and its expected +artifact name to the release workflow's check. diff --git a/vscode-extension/packaging/scripts/package-vsix b/vscode-extension/packaging/scripts/package-vsix new file mode 100755 index 0000000..bc95a95 --- /dev/null +++ b/vscode-extension/packaging/scripts/package-vsix @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# VSCode extension for AADL +# +# Copyright 2026 Carnegie Mellon University. +# +# NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE ENGINEERING INSTITUTE MATERIAL IS +# FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, +# EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF +# FITNESS FOR PURPOSE OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE +# MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO +# FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. +# +# Licensed under a BSD (SEI)-style license, please see LICENSE.txt +# or contact permission@sei.cmu.edu for full terms. +# +# [DISTRIBUTION STATEMENT A] This material has been approved for public release and unlimited +# distribution. Please see Copyright notice for non-US Government use and distribution. +# +# This Software includes and/or makes use of Third-Party Software each subject to its own license. +# +# DM26-0821 + +set -euo pipefail + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +extension_dir=$(cd "$script_dir/../.." && pwd) +stage_runtime="$script_dir/stage-runtime" + +die() { + echo "error: $*" >&2 + exit 1 +} + +usage() { + cat <]... + +Packages one VSIX per target, each carrying that target's bundled Java runtime. +Because the runtime is platform-specific there is no universal package: every +VSIX is built with 'vsce package --target' and named aadl2--.vsix. + +With no --target, packages for the host alone, which is what a local build and a +pull-request build want. Targets may also be given in AADL_VSIX_TARGETS as a +comma- or space-separated list, or the word 'all'; Maven passes the value of +-Dvsce.package.targets that way. + +Options: + --pre-release Mark the package as a pre-release. vsce writes this into the + manifest and refuses to publish a package as a pre-release + unless it was built as one, so it cannot be added later. + --target Target to package. Repeatable. + -h, --help Show this help. +EOF +} + +pre_release=() +targets=() +while [ $# -gt 0 ]; do + case "$1" in + --pre-release) + pre_release=(--pre-release) + ;; + --target) + [ $# -ge 2 ] || die "--target requires a value" + targets+=("$2") + shift + ;; + -h | --help) + usage + exit 0 + ;; + *) + echo "unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + esac + shift +done + +host_target=$("$stage_runtime" --host-target) + +if [ ${#targets[@]} -eq 0 ] && [ -n "${AADL_VSIX_TARGETS:-}" ]; then + case "$AADL_VSIX_TARGETS" in + all) + while IFS= read -r line; do targets+=("$line"); done < <("$stage_runtime" --all-targets) + ;; + host) + targets=("$host_target") + ;; + *) + IFS=', ' read -r -a targets <<<"$AADL_VSIX_TARGETS" + ;; + esac +fi + +[ ${#targets[@]} -gt 0 ] || targets=("$host_target") + +command -v vsce >/dev/null 2>&1 || die "vsce not found on PATH; run this through 'npm run package'" + +# Host first when it is in the list: it is the target whose VSIX gets executed by +# the test phase, and packaging is the step most likely to fail, so fail on the +# one that costs least to diagnose. +ordered=() +for target in "${targets[@]}"; do + [ "$target" = "$host_target" ] && ordered+=("$target") +done +for target in "${targets[@]}"; do + [ "$target" != "$host_target" ] && ordered+=("$target") +done + +cd "$extension_dir" +for target in "${ordered[@]}"; do + "$stage_runtime" "$target" + vsce package --no-dependencies --follow-symlinks --target "$target" "${pre_release[@]+"${pre_release[@]}"}" +done + +# Leave the host runtime staged whatever the last target was. The test phase runs +# straight after packaging and execs this runtime; a foreign-arch one left behind +# would fail there, a long way from the cause. +if [ "${ordered[${#ordered[@]} - 1]}" != "$host_target" ]; then + "$stage_runtime" "$host_target" +fi + +echo "Packaged:" +for target in "${ordered[@]}"; do + echo " $(ls "$extension_dir"/aadl2-"$target"-*.vsix)" +done diff --git a/vscode-extension/packaging/scripts/stage-runtime b/vscode-extension/packaging/scripts/stage-runtime new file mode 100755 index 0000000..ad5544e --- /dev/null +++ b/vscode-extension/packaging/scripts/stage-runtime @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +# VSCode extension for AADL +# +# Copyright 2026 Carnegie Mellon University. +# +# NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE ENGINEERING INSTITUTE MATERIAL IS +# FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, +# EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF +# FITNESS FOR PURPOSE OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE +# MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO +# FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. +# +# Licensed under a BSD (SEI)-style license, please see LICENSE.txt +# or contact permission@sei.cmu.edu for full terms. +# +# [DISTRIBUTION STATEMENT A] This material has been approved for public release and unlimited +# distribution. Please see Copyright notice for non-US Government use and distribution. +# +# This Software includes and/or makes use of Third-Party Software each subject to its own license. +# +# DM26-0821 + +set -euo pipefail + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +extension_dir=$(cd "$script_dir/../.." && pwd) +repo_root=$(cd "$extension_dir/.." && pwd) +# shellcheck source=../../../scripts/lib/temurin.sh +source "$repo_root/scripts/lib/temurin.sh" + +# Kept outside vscode-extension: its `mvn clean` deletes the module's target +# directory as well as its listed filesets, and re-downloading ~50 MB per target +# per build is not a tolerable price for a clean build. +downloads_dir="${TEMURIN_DOWNLOAD_DIR:-$repo_root/target/temurin-downloads}" +runtimes_dir="${TEMURIN_RUNTIME_DIR:-$repo_root/target/temurin-runtimes}" +runtime_link="$extension_dir/runtime" + +# The VS Code platform ids that `vsce package --target` accepts, restricted to +# the ones we bundle a runtime for. A client on any other platform is offered no +# package at all, because we publish no untargeted fallback. +all_targets=(darwin-x64 darwin-arm64 linux-x64 linux-arm64 win32-x64 win32-arm64) + +usage() { + cat <] + +Stages an Eclipse Temurin $TEMURIN_FEATURE_VERSION JRE for and points +vscode-extension/runtime at it, which is where the extension looks for its Java +runtime at both package time and run time. Defaults to the host target. + +Targets: ${all_targets[*]} + +Options: + --host-target Print the host's target id and exit without staging. + --all-targets Print every supported target id and exit without staging. + -h, --help Show this help. +EOF +} + +host_target() { + local os arch + + case "$(uname -s)" in + Darwin) os=darwin ;; + Linux) os=linux ;; + MINGW* | MSYS* | CYGWIN*) os=win32 ;; + *) temurin_die "unsupported host operating system: $(uname -s)" ;; + esac + + case "$(uname -m)" in + x86_64 | amd64) arch=x64 ;; + arm64 | aarch64) arch=arm64 ;; + *) temurin_die "unsupported host architecture: $(uname -m)" ;; + esac + + printf '%s-%s\n' "$os" "$arch" +} + +# Adoptium's own os/architecture names, plus the archive format and executable +# name that go with them. +adoptium_coordinates() { + case "$1" in + darwin-x64) printf 'mac x64 tar.gz java\n' ;; + darwin-arm64) printf 'mac aarch64 tar.gz java\n' ;; + linux-x64) printf 'linux x64 tar.gz java\n' ;; + linux-arm64) printf 'linux aarch64 tar.gz java\n' ;; + win32-x64) printf 'windows x64 zip java.exe\n' ;; + win32-arm64) printf 'windows aarch64 zip java.exe\n' ;; + *) temurin_die "unsupported target: $1 (expected one of: ${all_targets[*]})" ;; + esac +} + +target="" +while [ $# -gt 0 ]; do + case "$1" in + --host-target) + host_target + exit 0 + ;; + --all-targets) + printf '%s\n' "${all_targets[@]}" + exit 0 + ;; + -h | --help) + usage + exit 0 + ;; + -*) + echo "unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + *) + [ -z "$target" ] || temurin_die "only one target may be given" + target=$1 + ;; + esac + shift +done + +[ -n "$target" ] || target=$(host_target) + +# Two statements, not `read <<<"$(...)"`: a failure inside a command +# substitution only exits the subshell, and there it would be masked by read's +# own exit status, so an unsupported target would sail on with empty +# coordinates. A plain assignment propagates the failure under `set -e`. +coordinates=$(adoptium_coordinates "$target") +read -r adoptium_os adoptium_arch archive_ext java_exe <<<"$coordinates" + +archive=$(temurin_download "$TEMURIN_FEATURE_VERSION" "$adoptium_os" "$adoptium_arch" "$archive_ext" "$downloads_dir") +runtime_home=$(temurin_unpack "$archive" "$runtimes_dir/$target" "$java_exe") + +# A symlink rather than a copy, exactly like server/aadl/lib: vsce packages with +# --follow-symlinks, so the runtime is dereferenced into the VSIX, and switching +# targets costs nothing. Relative so the tree stays relocatable. +rm -rf "$runtime_link" +ln -s "../${runtime_home#"$repo_root"/}" "$runtime_link" + +[ -x "$runtime_link/bin/$java_exe" ] || temurin_die "staged runtime has no executable bin/$java_exe" +echo "Staged Eclipse Temurin $TEMURIN_FEATURE_VERSION for $target at $runtime_link" diff --git a/vscode-extension/pom.xml b/vscode-extension/pom.xml index 00fefd9..9a00ed6 100644 --- a/vscode-extension/pom.xml +++ b/vscode-extension/pom.xml @@ -45,6 +45,13 @@ as a pre-release unless it was built as one, so the choice has to be made here rather than at publish time. --> package + + host ${project.basedir}/../aadl-language-server/releng/org.osate.aadl.ls.repository/target/repository/plugins @@ -130,6 +137,12 @@ package run ${vsce.package.script} + + + ${vsce.package.targets} + @@ -151,6 +164,14 @@ ${project.basedir}/out + ${project.basedir} diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index 4aa31de..892070e 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -22,7 +22,6 @@ ******************************************************************************/ import { commands, - extensions, window, workspace, ExtensionContext, @@ -38,6 +37,7 @@ import { } from 'vscode-languageclient/node'; import * as path from 'path'; +import * as fs from 'fs'; import { execFile } from 'child_process'; import { findSymbol } from './symbols'; @@ -49,6 +49,8 @@ import { } from './contributedAadl'; import { AnalysisCommandResult, presentAnalysisResult } from './analysisResult'; import { + bundledRuntimeHome, + executableRuntimeFiles, javaExecutableIn, JavaRuntime, minimumJavaMajorVersion, @@ -58,39 +60,14 @@ import { serverClasspath } from './serverClasspath'; let client: LanguageClient; -interface RedHatJavaApi { - javaRequirement?: { - // eslint-disable-next-line @typescript-eslint/naming-convention - tooling_jre?: string; - }; -} - export interface AadlExtensionApi { - javaRuntime: JavaRuntime; + javaRuntime?: JavaRuntime; } function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } -async function redHatJavaHome(): Promise { - const ext = extensions.getExtension('redhat.java'); - if (!ext) { - throw new Error('The Red Hat Java extension (redhat.java) is required to run the AADL language server.'); - } - let api: RedHatJavaApi | undefined; - try { - api = ext.isActive ? ext.exports : await ext.activate(); - } catch (error) { - throw new Error(`Could not activate the Red Hat Java extension: ${errorMessage(error)}`); - } - const javaHome = api?.javaRequirement?.tooling_jre; - if (!javaHome) { - throw new Error('The Red Hat Java extension did not provide a tooling JRE. Update or reinstall redhat.java.'); - } - return javaHome; -} - async function javaMajorVersion(executable: string): Promise { return new Promise(resolve => { execFile(executable, ['-version'], { windowsHide: true }, (error, stdout, stderr) => { @@ -103,34 +80,65 @@ async function javaMajorVersion(executable: string): Promise }); } -async function resolveJavaRuntime(): Promise { - const toolingJre = await redHatJavaHome(); - const executable = javaExecutableIn(toolingJre); - const majorVersion = await javaMajorVersion(executable); +/** + * Makes the bundled runtime executable again after a packaging or installation + * step dropped the mode bits. vsce stores them and VS Code restores them, so + * this is a repair attempted only once the runtime has already failed to run — + * not something to do on every activation. + */ +function repairRuntimePermissions(javaHome: string): void { + for (const file of executableRuntimeFiles(javaHome)) { + try { + fs.chmodSync(file, 0o755); + } catch { + // Nothing to do: the probe below reports the runtime as unusable. + } + } +} + +async function resolveJavaRuntime(context: ExtensionContext): Promise { + const home = bundledRuntimeHome(context.extensionPath); + const executable = javaExecutableIn(home); + + let majorVersion = await javaMajorVersion(executable); + if (majorVersion === undefined) { + repairRuntimePermissions(home); + majorVersion = await javaMajorVersion(executable); + } + if (majorVersion === undefined) { throw new Error( - `Could not run the Red Hat Java extension's tooling JRE at ${executable}. ` - + 'Update or reinstall redhat.java.' + `Could not run the bundled Java runtime at ${executable}. ` + + 'This usually means the installed package was built for a different platform; ' + + 'reinstall the AADL extension from the Marketplace so the right one is chosen.' ); } if (majorVersion < minimumJavaMajorVersion) { throw new Error( - `The Red Hat Java extension's tooling JRE is Java ${majorVersion}; ` - + `Java ${minimumJavaMajorVersion} or newer is required. Update redhat.java.` + `The bundled Java runtime at ${executable} is Java ${majorVersion}; ` + + `Java ${minimumJavaMajorVersion} or newer is required. This is a packaging error.` ); } - return { executable, home: toolingJre, source: 'Red Hat Java extension', majorVersion }; + return { executable, home, source: 'bundled Java runtime', majorVersion }; } async function startLanguageServer( context: ExtensionContext, aadlFileWatcher: FileSystemWatcher ): Promise { - const java = await resolveJavaRuntime(); + const java = await resolveJavaRuntime(context); const classpath = serverClasspath(context.asAbsolutePath(path.join('server', 'aadl', 'lib'))); const mainClass = 'org.osate.aadl.ls.RunAadl2Server'; + // The point of bundling a runtime is that every user runs the same JVM the + // same way. These three variables are picked up by any JVM launch and can + // inject agents or repoint the trust store, so they are dropped rather than + // inherited; JAVA_HOME is set so anything the server spawns finds the + // bundled runtime instead of searching the system. // eslint-disable-next-line @typescript-eslint/naming-convention - const baseEnv = { ...process.env, ...(java.home ? { JAVA_HOME: java.home } : {}) }; + const baseEnv: NodeJS.ProcessEnv = { ...process.env, JAVA_HOME: java.home }; + delete baseEnv.JAVA_TOOL_OPTIONS; + delete baseEnv._JAVA_OPTIONS; + delete baseEnv.JDK_JAVA_OPTIONS; const serverOptions: ServerOptions = { run: { @@ -190,8 +198,19 @@ function showAnalysisResult(result: AnalysisCommandResult): void { export async function activate(context: ExtensionContext): Promise { const aadlFileWatcher = workspace.createFileSystemWatcher('**/*.aadl'); context.subscriptions.push(aadlFileWatcher); - // Start the language server - const javaRuntime = await startLanguageServer(context, aadlFileWatcher); + + // Start the language server. Letting this reject would surface only as a + // generic activation failure in the Extensions view, which is no way to learn + // that the installed package was built for another platform. Report it, then + // carry on registering commands so aadl2.restart can retry. + let javaRuntime: JavaRuntime | undefined; + try { + javaRuntime = await startLanguageServer(context, aadlFileWatcher); + } catch (error) { + const message = `The AADL language server did not start: ${errorMessage(error)}`; + console.error(message, error); + void window.showErrorMessage(message); + } // Make plugin-contributed AADL sources available as read-only virtual documents. // Resolve the client lazily because the restart command replaces the client instance. diff --git a/vscode-extension/src/javaRuntime.ts b/vscode-extension/src/javaRuntime.ts index fdbf7a1..38074bb 100644 --- a/vscode-extension/src/javaRuntime.ts +++ b/vscode-extension/src/javaRuntime.ts @@ -35,6 +35,29 @@ export function javaExecutableIn(javaHome: string, platform: NodeJS.Platform = p return path.join(javaHome, 'bin', platform === 'win32' ? 'java.exe' : 'java'); } +/** + * The Java home bundled in the extension. Packaging stages a platform-specific + * Eclipse Temurin JRE here, so this is the only Java the extension ever uses: + * there is no discovery, no JAVA_HOME or PATH fallback, and no setting to point + * somewhere else. Keep it in step with packaging/scripts/stage-runtime. + */ +export function bundledRuntimeHome(extensionPath: string): string { + return path.join(extensionPath, 'runtime'); +} + +/** + * Files that must be executable for the bundled runtime to work. `java` is + * obvious; `jspawnhelper` is what the JVM itself execs to spawn a process, and a + * packaging step that loses the mode bit turns that into a runtime failure far + * from its cause. + */ +export function executableRuntimeFiles(javaHome: string, platform: NodeJS.Platform = process.platform): string[] { + if (platform === 'win32') { + return []; + } + return [javaExecutableIn(javaHome, platform), path.join(javaHome, 'lib', 'jspawnhelper')]; +} + export function parseJavaMajorVersion(versionOutput: string): number | undefined { const match = /version\s+"([^"]+)"/i.exec(versionOutput); if (!match) { diff --git a/vscode-extension/src/test/integration/activation.test.ts b/vscode-extension/src/test/integration/activation.test.ts index 22a6fcc..660232c 100644 --- a/vscode-extension/src/test/integration/activation.test.ts +++ b/vscode-extension/src/test/integration/activation.test.ts @@ -26,15 +26,8 @@ import * as vscode from 'vscode'; const EXTENSION_ID = 'osate.aadl2'; -interface RedHatJavaApi { - javaRequirement?: { - // eslint-disable-next-line @typescript-eslint/naming-convention - tooling_jre?: string; - }; -} - interface AadlExtensionApi { - javaRuntime: { + javaRuntime?: { executable: string; home: string; source: string; @@ -42,10 +35,6 @@ interface AadlExtensionApi { }; } -function redHatJavaAvailable(): boolean { - return !!vscode.extensions.getExtension('redhat.java'); -} - async function waitForActivation(timeoutMs: number): Promise { const ext = vscode.extensions.getExtension(EXTENSION_ID); assert.ok(ext, `extension ${EXTENSION_ID} not found`); @@ -150,11 +139,10 @@ suite('extension auto-activation on aadl files', function () { let aadlFile: string; + // Nothing gates this suite any more. It used to skip itself when redhat.java + // was absent, which meant a silent pass; the runtime now ships in the + // extension, so a missing or unusable one is a failure. suiteSetup(function () { - if (!redHatJavaAvailable()) { - console.log('redhat.java not installed in this VS Code profile — skipping activation tests'); - this.skip(); - } const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; assert.ok(workspaceFolder, 'integration test workspace is not open'); aadlFile = path.join(workspaceFolder.uri.fsPath, 'TestProject', 'sample.aadl'); @@ -175,28 +163,28 @@ suite('extension auto-activation on aadl files', function () { assert.strictEqual(activated, true, 'extension did not activate within 60s'); }); - test('uses the Red Hat tooling JRE and requires Java 21 or newer', async () => { - const redHatExtension = vscode.extensions.getExtension('redhat.java'); - assert.ok(redHatExtension, 'redhat.java not found'); - const redHatApi = redHatExtension!.isActive - ? redHatExtension!.exports - : await redHatExtension!.activate(); - const toolingJre = redHatApi?.javaRequirement?.tooling_jre; - assert.ok(toolingJre, 'redhat.java did not provide a tooling JRE'); - + test('runs the bundled Java runtime and nothing installed on the machine', async () => { const aadlExtension = vscode.extensions.getExtension(EXTENSION_ID); assert.ok(aadlExtension?.isActive, 'AADL extension is not active'); const runtime = aadlExtension!.exports.javaRuntime; + assert.ok(runtime, 'the language server did not start, so no runtime was resolved'); + + const expectedHome = path.join(aadlExtension!.extensionPath, 'runtime'); const expectedExecutable = path.join( - toolingJre!, + expectedHome, 'bin', process.platform === 'win32' ? 'java.exe' : 'java' ); - assert.strictEqual(runtime.source, 'Red Hat Java extension'); - assert.strictEqual(runtime.home, toolingJre); - assert.strictEqual(runtime.executable, expectedExecutable); - assert.ok(runtime.majorVersion >= 21, `expected Java 21+, got Java ${runtime.majorVersion}`); + assert.strictEqual(runtime!.source, 'bundled Java runtime'); + assert.strictEqual(runtime!.home, expectedHome); + assert.strictEqual(runtime!.executable, expectedExecutable); + assert.ok(runtime!.majorVersion >= 21, `expected Java 21+, got Java ${runtime!.majorVersion}`); + }); + + test('no Java extension is involved', () => { + assert.strictEqual(vscode.extensions.getExtension('redhat.java'), undefined, + 'the suite runs with --disable-extensions; a redhat.java here means the isolation broke'); }); test('custom commands are registered after activation', async () => { diff --git a/vscode-extension/src/test/runIntegrationTests.ts b/vscode-extension/src/test/runIntegrationTests.ts index 29b2084..ad4ed4e 100644 --- a/vscode-extension/src/test/runIntegrationTests.ts +++ b/vscode-extension/src/test/runIntegrationTests.ts @@ -23,11 +23,9 @@ import * as path from 'path'; import * as os from 'os'; import * as fs from 'fs'; -import * as cp from 'child_process'; import { runTests, downloadAndUnzipVSCode, - resolveCliArgsFromVSCodeExecutablePath, } from '@vscode/test-electron'; function resolveDownloadedExecutable(downloadedExecutable: string): string { @@ -43,45 +41,23 @@ function resolveDownloadedExecutable(downloadedExecutable: string): string { return downloadedExecutable; } -function locateInstalledRedHatJava(extensionsDir: string): string | undefined { - if (!fs.existsSync(extensionsDir)) { - return undefined; - } - const entries = fs.readdirSync(extensionsDir, { withFileTypes: true }); - const match = entries - .filter(e => e.isDirectory() && /^redhat\.java-/i.test(e.name)) - .map(e => e.name) - .sort() - .pop(); - return match ? path.join(extensionsDir, match) : undefined; -} - -async function tryDownloadRedHatJava(extensionsDir: string, userDataDir: string): Promise { - let vscodeExe: string; - try { - vscodeExe = await downloadAndUnzipVSCode(); - } catch { - return false; - } - const [cli, ...cliArgs] = resolveCliArgsFromVSCodeExecutablePath(vscodeExe); - // NODE_USE_SYSTEM_CA=1 makes Node consult the OS trust store, which on - // macOS picks up corp roots from the keychain. Falls back gracefully - // on environments where the flag is unrecognized. - const result = cp.spawnSync( - cli, - [ - ...cliArgs, - '--user-data-dir', userDataDir, - '--extensions-dir', extensionsDir, - '--install-extension', 'redhat.java', - '--force', - ], - { - stdio: 'inherit', - env: { ...process.env, NODE_USE_SYSTEM_CA: '1' }, - }, +/** + * Fails early, with the fix in the message, when the extension has no runtime + * staged. Without this the suite would launch VS Code, wait for an activation + * that cannot happen, and report a timeout. + */ +function requireStagedRuntime(extensionDevelopmentPath: string): void { + const executable = path.join( + extensionDevelopmentPath, + 'runtime', + 'bin', + process.platform === 'win32' ? 'java.exe' : 'java', ); - return result.status === 0; + if (!fs.existsSync(executable)) { + throw new Error( + `No bundled Java runtime at ${executable}. Stage one first: npm run stage-runtime`, + ); + } } async function main() { @@ -93,6 +69,7 @@ async function main() { const extensionDevelopmentPath = path.resolve(__dirname, '..', '..'); const extensionTestsPath = path.resolve(__dirname, 'integration', 'index'); const workspacePath = path.resolve(extensionDevelopmentPath, 'src', 'test', 'fixtures', 'workspace'); + requireStagedRuntime(extensionDevelopmentPath); const vscodeExecutablePath = resolveDownloadedExecutable(await downloadAndUnzipVSCode()); // Keep the user-data-dir and extensions-dir short — VS Code's IPC // socket path has a hard 103-char limit on macOS, which we blow past @@ -100,30 +77,17 @@ async function main() { const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aadl-vsc-')); const extensionsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aadl-ext-')); - // The AADL extension's activate() resolves a tooling JRE through - // redhat.java. To install it we, in order: - // 1. copy from ~/.vscode/extensions if present; - // 2. download from the marketplace via the VS Code CLI, with - // NODE_USE_SYSTEM_CA=1 so corp-MITM TLS chains validate - // against the OS keychain. - // If both fail the activation suite skips itself. - const userExtDir = path.join(os.homedir(), '.vscode', 'extensions'); - const localCopy = locateInstalledRedHatJava(userExtDir); - if (localCopy) { - const dest = path.join(extensionsDir, path.basename(localCopy)); - fs.cpSync(localCopy, dest, { recursive: true }); - } else if (!(await tryDownloadRedHatJava(extensionsDir, userDataDir))) { - console.warn('redhat.java not available locally and download failed; activation tests will skip.'); - } - - // Don't pass --disable-extensions: the AADL extension's activate() - // requires redhat.java's tooling JRE. + // --disable-extensions still loads the extension under development, so the + // suite exercises the bundled runtime in isolation. It used to install + // redhat.java here to supply a JVM, which made the tests need the + // marketplace and let them skip themselves when it was unreachable. await runTests({ extensionDevelopmentPath, extensionTestsPath, vscodeExecutablePath, launchArgs: [ workspacePath, + '--disable-extensions', '--user-data-dir', userDataDir, '--extensions-dir', extensionsDir, ], diff --git a/vscode-extension/src/test/unit/javaRuntime.test.ts b/vscode-extension/src/test/unit/javaRuntime.test.ts index 791ab3f..8719c05 100644 --- a/vscode-extension/src/test/unit/javaRuntime.test.ts +++ b/vscode-extension/src/test/unit/javaRuntime.test.ts @@ -21,7 +21,10 @@ * DM26-0821 ******************************************************************************/ import * as assert from 'assert'; +import * as path from 'path'; import { + bundledRuntimeHome, + executableRuntimeFiles, javaExecutableIn, minimumJavaMajorVersion, parseJavaMajorVersion @@ -49,4 +52,18 @@ suite('Java runtime selection helpers', () => { assert.strictEqual(javaExecutableIn('/opt/jdk', 'linux'), '/opt/jdk/bin/java'); assert.strictEqual(javaExecutableIn('C:\\Java\\jdk', 'win32'), 'C:\\Java\\jdk/bin/java.exe'); }); + + // The staging script writes this exact directory, so the two must not drift: + // a mismatch produces an extension that packages a runtime it cannot find. + test('looks for the bundled runtime beside the extension', () => { + assert.strictEqual(bundledRuntimeHome('/ext/aadl2'), path.join('/ext/aadl2', 'runtime')); + }); + + test('knows which bundled files must stay executable', () => { + assert.deepStrictEqual(executableRuntimeFiles('/ext/aadl2/runtime', 'linux'), [ + '/ext/aadl2/runtime/bin/java', + '/ext/aadl2/runtime/lib/jspawnhelper' + ]); + assert.deepStrictEqual(executableRuntimeFiles('C:\\ext\\aadl2\\runtime', 'win32'), []); + }); }); diff --git a/vscode-extension/src/test/unit/lifecycle.test.ts b/vscode-extension/src/test/unit/lifecycle.test.ts index decf0d0..34c8097 100644 --- a/vscode-extension/src/test/unit/lifecycle.test.ts +++ b/vscode-extension/src/test/unit/lifecycle.test.ts @@ -47,9 +47,25 @@ suite('language server lifecycle', () => { assert.ok(!source.includes("path.join('server', 'aadl', 'bin'")); }); - test('does not fall back from the Red Hat tooling JRE', () => { - assert.ok(source.includes("source: 'Red Hat Java extension'")); + test('runs the bundled runtime and nothing else', () => { + assert.ok(source.includes("source: 'bundled Java runtime'")); + assert.ok(source.includes('bundledRuntimeHome(context.extensionPath)')); + assert.ok(!source.includes('redhat.java'), + 'the Red Hat Java extension is no longer a dependency'); assert.ok(!source.includes("source: 'JAVA_HOME'")); assert.ok(!source.includes("source: 'PATH'")); }); + + test('does not let the environment reconfigure the bundled runtime', () => { + for (const variable of ['JAVA_TOOL_OPTIONS', '_JAVA_OPTIONS', 'JDK_JAVA_OPTIONS']) { + assert.ok(source.includes(`delete baseEnv.${variable}`), + `${variable} must not be inherited by the server process`); + } + }); + + test('reports a startup failure instead of rejecting activation', () => { + assert.ok(/activate\([\s\S]*?try \{\s*javaRuntime = await startLanguageServer/.test(source), + 'a runtime that cannot start must be reported, not surfaced as a bare activation failure'); + assert.ok(source.includes('showErrorMessage(message)')); + }); });