From 7256c31305e00dcbb90d88f2f30cc82477e87469 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 17 Sep 2026 22:07:27 +0000 Subject: [PATCH 1/7] Initial plan From e8d7cec825bbbc059c39140123edbe8fe8b9f784 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 17 Sep 2026 22:18:07 +0000 Subject: [PATCH 2/7] feat(java): add linuxmusl-x64 native classifier Co-authored-by: edburns <75821+edburns@users.noreply.github.com> --- .github/workflows/java-publish-maven.yml | 89 +++++++++++++ .github/workflows/java-publish-snapshot.yml | 102 +++++++++++++++ .github/workflows/java-sdk-tests.yml | 95 ++++++++++++++ java/copilot-native/pom.xml | 120 ++++++++++++++++++ .../scripts/fetch-native.test.mjs | 2 +- .../scripts/validate-local-publication.mjs | 1 + .../scripts/validate-native-artifact.test.mjs | 1 + .../scripts/validate-native-host.mjs | 14 ++ .../scripts/validate-native-host.test.mjs | 39 +++++- 9 files changed, 460 insertions(+), 3 deletions(-) diff --git a/.github/workflows/java-publish-maven.yml b/.github/workflows/java-publish-maven.yml index cf7af2c3fc..61f1e71a2c 100644 --- a/.github/workflows/java-publish-maven.yml +++ b/.github/workflows/java-publish-maven.yml @@ -208,6 +208,66 @@ jobs: if-no-files-found: error retention-days: 1 + build-linuxmusl-x64-classifier: + name: Build Linux musl x64 native classifier + needs: resolve-source + runs-on: ubuntu-latest + permissions: + contents: read + defaults: + run: + shell: bash + working-directory: ./java + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ needs.resolve-source.outputs.validated_source }} + fetch-depth: 1 + persist-credentials: false + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version: 22 + package-manager-cache: false + + - name: Build and validate linuxmusl-x64 classifier + run: | + set -euo pipefail + SOURCE_COMMIT=$(git rev-parse HEAD) + if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.validated_source }}" ]; then + echo "::error::Checked out $SOURCE_COMMIT instead of the resolved release source." + exit 1 + fi + VERSION="${{ needs.resolve-source.outputs.release_version }}" + docker run --rm \ + --volume "$GITHUB_WORKSPACE:/workspace" \ + --workdir /workspace/java \ + "eclipse-temurin:25-jdk-alpine" \ + sh -c "apk add --no-cache git maven nodejs npm && + git config --global --add safe.directory /workspace && + node copilot-native/scripts/validate-native-host.mjs linuxmusl-x64 && + mvn -B -pl copilot-native package -DskipTests -Dcopilot.native.libc=musl -Drevision=$VERSION" + JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION-linuxmusl-x64.jar" + PRIMARY_JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION.jar" + test -f "$JAR" + node copilot-native/scripts/validate-native-artifact.mjs \ + classifier linuxmusl-x64 "$JAR" "$(basename "$JAR")" .. + node copilot-native/scripts/validate-native-artifact.mjs placeholder "$PRIMARY_JAR" + MANIFEST="copilot-native/target/linuxmusl-x64-$VERSION.sha256" + HASH=$(sha256sum "$JAR" | cut -d ' ' -f 1) + printf '%s %s' "$HASH" "$(basename "$JAR")" > "$MANIFEST" + node copilot-native/scripts/validate-native-artifact.mjs \ + checksum "$JAR" "$MANIFEST" "$(basename "$JAR")" + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: java-native-linuxmusl-x64-release-${{ github.run_id }}-${{ github.run_attempt }} + path: | + java/copilot-native/target/copilot-sdk-java-runtime-${{ needs.resolve-source.outputs.release_version }}-linuxmusl-x64.jar + java/copilot-native/target/linuxmusl-x64-${{ needs.resolve-source.outputs.release_version }}.sha256 + if-no-files-found: error + retention-days: 1 + build-windows-classifier: name: Build Windows native classifier needs: resolve-source @@ -446,6 +506,7 @@ jobs: [ resolve-source, build-linux-arm64-classifier, + build-linuxmusl-x64-classifier, build-windows-classifier, build-windows-arm64-classifier, build-darwin-classifier, @@ -488,6 +549,11 @@ jobs: name: java-native-linux-arm64-release-${{ github.run_id }}-${{ github.run_attempt }} path: ${{ runner.temp }}/java-native-linux-arm64 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: java-native-linuxmusl-x64-release-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/java-native-linuxmusl-x64 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: java-native-win32-x64-release-${{ github.run_id }}-${{ github.run_attempt }} @@ -550,6 +616,27 @@ jobs: echo "windows_jar=$JAR" >> "$GITHUB_OUTPUT" echo "windows_sha=$(cut -d ' ' -f 1 "$MANIFEST")" >> "$GITHUB_OUTPUT" + - name: Verify immutable source and Linux musl x64 classifier + id: linuxmusl-x64-artifact + run: | + SOURCE_COMMIT=$(git rev-parse HEAD) + if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.validated_source }}" ]; then + echo "::error::Checked out $SOURCE_COMMIT instead of the resolved release source." + exit 1 + fi + VERSION="${{ needs.resolve-source.outputs.release_version }}" + ARTIFACT_DIRECTORY="${{ runner.temp }}/java-native-linuxmusl-x64" + JAR="$ARTIFACT_DIRECTORY/copilot-sdk-java-runtime-$VERSION-linuxmusl-x64.jar" + MANIFEST="$ARTIFACT_DIRECTORY/linuxmusl-x64-$VERSION.sha256" + test -f "$JAR" + test -f "$MANIFEST" + node "$GITHUB_WORKSPACE/java/copilot-native/scripts/validate-native-artifact.mjs" \ + checksum "$JAR" "$MANIFEST" "$(basename "$JAR")" + node "$GITHUB_WORKSPACE/java/copilot-native/scripts/validate-native-artifact.mjs" \ + classifier linuxmusl-x64 "$JAR" "$(basename "$JAR")" "$GITHUB_WORKSPACE" + echo "linuxmusl_x64_jar=$JAR" >> "$GITHUB_OUTPUT" + echo "linuxmusl_x64_sha=$(cut -d ' ' -f 1 "$MANIFEST")" >> "$GITHUB_OUTPUT" + - name: Verify immutable source and Darwin classifier id: darwin-artifact run: | @@ -619,6 +706,7 @@ jobs: VERSION="${{ needs.resolve-source.outputs.release_version }}" mvn -B deploy -DskipTests -DskipITs -Prelease -Drevision="$VERSION" -Dcopilot.native.libc=glibc \ "-Dcopilot.native.external.linux.arm64.classifier.path=${{ steps.linux-arm64-artifact.outputs.linux_arm64_jar }}" \ + "-Dcopilot.native.external.linuxmusl.x64.classifier.path=${{ steps.linuxmusl-x64-artifact.outputs.linuxmusl_x64_jar }}" \ "-Dcopilot.native.external.win32.classifier.path=${{ steps.windows-artifact.outputs.windows_jar }}" \ "-Dcopilot.native.external.win32.arm64.classifier.path=${{ steps.windows-arm64-artifact.outputs.windows_arm64_jar }}" \ "-Dcopilot.native.external.darwin.classifier.path=${{ steps.darwin-artifact.outputs.darwin_jar }}" \ @@ -657,6 +745,7 @@ jobs: echo "| --- | --- | --- | --- | --- |" echo "| \`linux-x64\` | \`ubuntu-latest\` | \`$(basename "$LINUX_JAR")\` | \`$LINUX_SHA\` | Published |" echo "| \`linux-arm64\` | \`ubuntu-24.04-arm\` | \`$(basename "${{ steps.linux-arm64-artifact.outputs.linux_arm64_jar }}")\` | \`${{ steps.linux-arm64-artifact.outputs.linux_arm64_sha }}\` | Published |" + echo "| \`linuxmusl-x64\` | \`Alpine x64\` | \`$(basename "${{ steps.linuxmusl-x64-artifact.outputs.linuxmusl_x64_jar }}")\` | \`${{ steps.linuxmusl-x64-artifact.outputs.linuxmusl_x64_sha }}\` | Published |" echo "| \`win32-x64\` | \`windows-latest\` | \`$(basename "${{ steps.windows-artifact.outputs.windows_jar }}")\` | \`${{ steps.windows-artifact.outputs.windows_sha }}\` | Published |" echo "| \`win32-arm64\` | \`windows-11-arm\` | \`$(basename "${{ steps.windows-arm64-artifact.outputs.windows_arm64_jar }}")\` | \`${{ steps.windows-arm64-artifact.outputs.windows_arm64_sha }}\` | Published |" echo "| \`darwin-x64\` | \`macos-15-intel\` | \`$(basename "${{ steps.darwin-x64-artifact.outputs.darwin_x64_jar }}")\` | \`${{ steps.darwin-x64-artifact.outputs.darwin_x64_sha }}\` | Published |" diff --git a/.github/workflows/java-publish-snapshot.yml b/.github/workflows/java-publish-snapshot.yml index fbaf4a0d6d..683d23d9d9 100644 --- a/.github/workflows/java-publish-snapshot.yml +++ b/.github/workflows/java-publish-snapshot.yml @@ -97,6 +97,75 @@ jobs: if-no-files-found: error retention-days: 1 + build-linuxmusl-x64-classifier: + name: Build Linux musl x64 snapshot classifier + needs: resolve-source + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + version: ${{ steps.build.outputs.version }} + defaults: + run: + shell: bash + working-directory: ./java + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.sha }} + fetch-depth: 1 + persist-credentials: false + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version: 22 + + - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: "25" + distribution: "microsoft" + cache: "maven" + + - name: Build and validate linuxmusl-x64 classifier + id: build + run: | + set -euo pipefail + SOURCE_COMMIT=$(git rev-parse HEAD) + if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.source_sha }}" ]; then + echo "::error::Checked out $SOURCE_COMMIT instead of the resolved snapshot source." + exit 1 + fi + docker run --rm \ + --volume "$GITHUB_WORKSPACE:/workspace" \ + --workdir /workspace/java \ + "eclipse-temurin:25-jdk-alpine" \ + sh -c "apk add --no-cache git maven nodejs npm && + git config --global --add safe.directory /workspace && + node copilot-native/scripts/validate-native-host.mjs linuxmusl-x64 && + mvn -B -pl copilot-native package -DskipTests -Dcopilot.native.libc=musl" + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION-linuxmusl-x64.jar" + PRIMARY_JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION.jar" + test -f "$JAR" + node copilot-native/scripts/validate-native-artifact.mjs \ + classifier linuxmusl-x64 "$JAR" "$(basename "$JAR")" .. + node copilot-native/scripts/validate-native-artifact.mjs placeholder "$PRIMARY_JAR" + MANIFEST="copilot-native/target/linuxmusl-x64-$VERSION.sha256" + HASH=$(sha256sum "$JAR" | cut -d ' ' -f 1) + printf '%s %s' "$HASH" "$(basename "$JAR")" > "$MANIFEST" + node copilot-native/scripts/validate-native-artifact.mjs \ + checksum "$JAR" "$MANIFEST" "$(basename "$JAR")" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: java-native-linuxmusl-x64-snapshot-${{ github.run_id }}-${{ github.run_attempt }} + path: | + java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-linuxmusl-x64.jar + java/copilot-native/target/linuxmusl-x64-${{ steps.build.outputs.version }}.sha256 + if-no-files-found: error + retention-days: 1 + build-windows-classifier: name: Build Windows snapshot classifier needs: resolve-source @@ -351,6 +420,7 @@ jobs: [ resolve-source, build-linux-arm64-classifier, + build-linuxmusl-x64-classifier, build-windows-classifier, build-windows-arm64-classifier, build-darwin-classifier, @@ -388,6 +458,11 @@ jobs: name: java-native-linux-arm64-snapshot-${{ github.run_id }}-${{ github.run_attempt }} path: ${{ runner.temp }}/java-native-linux-arm64 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: java-native-linuxmusl-x64-snapshot-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/java-native-linuxmusl-x64 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: java-native-win32-x64-snapshot-${{ github.run_id }}-${{ github.run_attempt }} @@ -464,6 +539,31 @@ jobs: echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "windows_sha=$(cut -d ' ' -f 1 "$MANIFEST")" >> "$GITHUB_OUTPUT" + - name: Verify version, source, and Linux musl x64 classifier + id: linuxmusl-x64-artifact + run: | + SOURCE_COMMIT=$(git rev-parse HEAD) + if [ "$SOURCE_COMMIT" != "${{ needs.resolve-source.outputs.source_sha }}" ]; then + echo "::error::Checked out $SOURCE_COMMIT instead of the resolved snapshot source." + exit 1 + fi + VERSION="${{ steps.linux-arm64-artifact.outputs.version }}" + if [ "$VERSION" != "${{ needs.build-linuxmusl-x64-classifier.outputs.version }}" ]; then + echo "::error::Linux musl x64 classifier version does not match deploy version." + exit 1 + fi + ARTIFACT_DIRECTORY="${{ runner.temp }}/java-native-linuxmusl-x64" + JAR="$ARTIFACT_DIRECTORY/copilot-sdk-java-runtime-$VERSION-linuxmusl-x64.jar" + MANIFEST="$ARTIFACT_DIRECTORY/linuxmusl-x64-$VERSION.sha256" + test -f "$JAR" + test -f "$MANIFEST" + node "$GITHUB_WORKSPACE/java/copilot-native/scripts/validate-native-artifact.mjs" \ + checksum "$JAR" "$MANIFEST" "$(basename "$JAR")" + node "$GITHUB_WORKSPACE/java/copilot-native/scripts/validate-native-artifact.mjs" \ + classifier linuxmusl-x64 "$JAR" "$(basename "$JAR")" "$GITHUB_WORKSPACE" + echo "linuxmusl_x64_jar=$JAR" >> "$GITHUB_OUTPUT" + echo "linuxmusl_x64_sha=$(cut -d ' ' -f 1 "$MANIFEST")" >> "$GITHUB_OUTPUT" + - name: Verify version, source, and Darwin classifier id: darwin-artifact run: | @@ -544,6 +644,7 @@ jobs: VERSION="${{ steps.windows-artifact.outputs.version }}" mvn -B deploy -DskipTests -DskipITs -Dcopilot.native.libc=glibc \ "-Dcopilot.native.external.linux.arm64.classifier.path=${{ steps.linux-arm64-artifact.outputs.linux_arm64_jar }}" \ + "-Dcopilot.native.external.linuxmusl.x64.classifier.path=${{ steps.linuxmusl-x64-artifact.outputs.linuxmusl_x64_jar }}" \ "-Dcopilot.native.external.win32.classifier.path=${{ steps.windows-artifact.outputs.windows_jar }}" \ "-Dcopilot.native.external.win32.arm64.classifier.path=${{ steps.windows-arm64-artifact.outputs.windows_arm64_jar }}" \ "-Dcopilot.native.external.darwin.classifier.path=${{ steps.darwin-artifact.outputs.darwin_jar }}" \ @@ -582,6 +683,7 @@ jobs: echo "| --- | --- | --- | --- | --- |" echo "| \`linux-x64\` | \`ubuntu-latest\` | \`$(basename "$LINUX_JAR")\` | \`$LINUX_SHA\` | Published |" echo "| \`linux-arm64\` | \`ubuntu-24.04-arm\` | \`$(basename "${{ steps.linux-arm64-artifact.outputs.linux_arm64_jar }}")\` | \`${{ steps.linux-arm64-artifact.outputs.linux_arm64_sha }}\` | Published |" + echo "| \`linuxmusl-x64\` | \`Alpine x64\` | \`$(basename "${{ steps.linuxmusl-x64-artifact.outputs.linuxmusl_x64_jar }}")\` | \`${{ steps.linuxmusl-x64-artifact.outputs.linuxmusl_x64_sha }}\` | Published |" echo "| \`win32-x64\` | \`windows-latest\` | \`$(basename "${{ steps.windows-artifact.outputs.windows_jar }}")\` | \`${{ steps.windows-artifact.outputs.windows_sha }}\` | Published |" echo "| \`win32-arm64\` | \`windows-11-arm\` | \`$(basename "${{ steps.windows-arm64-artifact.outputs.windows_arm64_jar }}")\` | \`${{ steps.windows-arm64-artifact.outputs.windows_arm64_sha }}\` | Published |" echo "| \`darwin-x64\` | \`macos-15-intel\` | \`$(basename "${{ steps.darwin-x64-artifact.outputs.darwin_x64_jar }}")\` | \`${{ steps.darwin-x64-artifact.outputs.darwin_x64_sha }}\` | Published |" diff --git a/.github/workflows/java-sdk-tests.yml b/.github/workflows/java-sdk-tests.yml index c51d56cc1f..a292bc3273 100644 --- a/.github/workflows/java-sdk-tests.yml +++ b/.github/workflows/java-sdk-tests.yml @@ -26,6 +26,9 @@ jobs: - os: ubuntu-24.04-arm classifier: linux-arm64 maven-args: -Dcopilot.native.libc=glibc + - os: ubuntu-latest + classifier: linuxmusl-x64 + alpine-image: eclipse-temurin:25-jdk-alpine - os: windows-latest classifier: win32-x64 - os: windows-11-arm @@ -54,13 +57,30 @@ jobs: node-version: 22 - name: Validate native host + if: matrix.alpine-image == '' run: node copilot-native/scripts/validate-native-host.mjs ${{ matrix.classifier }} - name: Run Java SDK tests (InProcess) + if: matrix.alpine-image == '' env: CI: "true" run: mvn clean verify -Pinprocess ${{ matrix.maven-args }} + - name: Run Java SDK tests (InProcess, musl) + if: matrix.alpine-image != '' + env: + CI: "true" + run: | + docker run --rm \ + --volume "$GITHUB_WORKSPACE:/workspace" \ + --workdir /workspace/java \ + --env CI \ + "${{ matrix.alpine-image }}" \ + sh -c 'apk add --no-cache git maven nodejs npm && + git config --global --add safe.directory /workspace && + node copilot-native/scripts/validate-native-host.mjs linuxmusl-x64 && + mvn clean verify -Pinprocess -Dcopilot.native.libc=musl' + - name: Generate Test Report Summary if: failure() uses: ./.github/actions/java-test-report @@ -137,6 +157,65 @@ jobs: overwrite: true retention-days: 1 + java-native-publication-linuxmusl-x64: + name: "Java Native Publication Input (linuxmusl-x64)" + if: github.event.repository.fork == false + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + source_sha: ${{ steps.build.outputs.source_sha }} + version: ${{ steps.build.outputs.version }} + defaults: + run: + shell: bash + working-directory: ./java + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version: 22 + + - name: Build and validate linuxmusl-x64 classifier + id: build + run: | + set -euo pipefail + VERSION="$JAVA_PUBLICATION_REVISION" + docker run --rm \ + --volume "$GITHUB_WORKSPACE:/workspace" \ + --workdir /workspace/java \ + "eclipse-temurin:25-jdk-alpine" \ + sh -c "apk add --no-cache git maven nodejs npm && + git config --global --add safe.directory /workspace && + node copilot-native/scripts/validate-native-host.mjs linuxmusl-x64 && + mvn -B -pl copilot-native package -DskipTests -Dcopilot.native.libc=musl -Drevision=$VERSION" + JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION-linuxmusl-x64.jar" + PRIMARY_JAR="copilot-native/target/copilot-sdk-java-runtime-$VERSION.jar" + test -f "$JAR" + node copilot-native/scripts/validate-native-artifact.mjs \ + classifier linuxmusl-x64 "$JAR" "$(basename "$JAR")" .. + node copilot-native/scripts/validate-native-artifact.mjs placeholder "$PRIMARY_JAR" + MANIFEST="copilot-native/target/linuxmusl-x64-$VERSION.sha256" + HASH=$(sha256sum "$JAR" | cut -d ' ' -f 1) + printf '%s %s' "$HASH" "$(basename "$JAR")" > "$MANIFEST" + node copilot-native/scripts/validate-native-artifact.mjs \ + checksum "$JAR" "$MANIFEST" "$(basename "$JAR")" + echo "source_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: java-native-publication-linuxmusl-x64-${{ github.run_id }} + path: | + java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-linuxmusl-x64.jar + java/copilot-native/target/linuxmusl-x64-${{ steps.build.outputs.version }}.sha256 + if-no-files-found: error + overwrite: true + retention-days: 1 + java-native-publication-windows: name: "Java Native Publication Input (win32-x64)" if: github.event.repository.fork == false @@ -376,6 +455,7 @@ jobs: needs: [ java-native-publication-linux-arm64, + java-native-publication-linuxmusl-x64, java-native-publication-windows, java-native-publication-windows-arm64, java-native-publication-darwin, @@ -407,6 +487,11 @@ jobs: name: java-native-publication-linux-arm64-${{ github.run_id }} path: ${{ github.workspace }}/java/native-publication-input/linux-arm64 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: java-native-publication-linuxmusl-x64-${{ github.run_id }} + path: ${{ github.workspace }}/java/native-publication-input/linuxmusl-x64 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: java-native-publication-win32-x64-${{ github.run_id }} @@ -431,6 +516,7 @@ jobs: run: | set -euo pipefail test "$(git rev-parse HEAD)" = "${{ needs.java-native-publication-linux-arm64.outputs.source_sha }}" + test "$(git rev-parse HEAD)" = "${{ needs.java-native-publication-linuxmusl-x64.outputs.source_sha }}" test "$(git rev-parse HEAD)" = "${{ needs.java-native-publication-windows.outputs.source_sha }}" test "$(git rev-parse HEAD)" = "${{ needs.java-native-publication-windows-arm64.outputs.source_sha }}" test "$(git rev-parse HEAD)" = "${{ needs.java-native-publication-darwin.outputs.source_sha }}" @@ -438,16 +524,20 @@ jobs: VERSION="${{ needs.java-native-publication-windows.outputs.version }}" test "$VERSION" = "$JAVA_PUBLICATION_REVISION" test "$VERSION" = "${{ needs.java-native-publication-linux-arm64.outputs.version }}" + test "$VERSION" = "${{ needs.java-native-publication-linuxmusl-x64.outputs.version }}" test "$VERSION" = "${{ needs.java-native-publication-windows-arm64.outputs.version }}" test "$VERSION" = "${{ needs.java-native-publication-darwin.outputs.version }}" test "$VERSION" = "${{ needs.java-native-publication-darwin-x64.outputs.version }}" LINUX_ARM64_DIRECTORY="$GITHUB_WORKSPACE/java/native-publication-input/linux-arm64" + LINUXMUSL_X64_DIRECTORY="$GITHUB_WORKSPACE/java/native-publication-input/linuxmusl-x64" WINDOWS_DIRECTORY="$GITHUB_WORKSPACE/java/native-publication-input/windows" WINDOWS_ARM64_DIRECTORY="$GITHUB_WORKSPACE/java/native-publication-input/windows-arm64" DARWIN_DIRECTORY="$GITHUB_WORKSPACE/java/native-publication-input/darwin" DARWIN_X64_DIRECTORY="$GITHUB_WORKSPACE/java/native-publication-input/darwin-x64" LINUX_ARM64_JAR="$LINUX_ARM64_DIRECTORY/copilot-sdk-java-runtime-$VERSION-linux-arm64.jar" LINUX_ARM64_MANIFEST="$LINUX_ARM64_DIRECTORY/linux-arm64-$VERSION.sha256" + LINUXMUSL_X64_JAR="$LINUXMUSL_X64_DIRECTORY/copilot-sdk-java-runtime-$VERSION-linuxmusl-x64.jar" + LINUXMUSL_X64_MANIFEST="$LINUXMUSL_X64_DIRECTORY/linuxmusl-x64-$VERSION.sha256" WINDOWS_JAR="$WINDOWS_DIRECTORY/copilot-sdk-java-runtime-$VERSION-win32-x64.jar" WINDOWS_MANIFEST="$WINDOWS_DIRECTORY/win32-x64-$VERSION.sha256" WINDOWS_ARM64_JAR="$WINDOWS_ARM64_DIRECTORY/copilot-sdk-java-runtime-$VERSION-win32-arm64.jar" @@ -460,6 +550,10 @@ jobs: checksum "$LINUX_ARM64_JAR" "$LINUX_ARM64_MANIFEST" "$(basename "$LINUX_ARM64_JAR")" node copilot-native/scripts/validate-native-artifact.mjs \ classifier linux-arm64 "$LINUX_ARM64_JAR" "$(basename "$LINUX_ARM64_JAR")" .. + node copilot-native/scripts/validate-native-artifact.mjs \ + checksum "$LINUXMUSL_X64_JAR" "$LINUXMUSL_X64_MANIFEST" "$(basename "$LINUXMUSL_X64_JAR")" + node copilot-native/scripts/validate-native-artifact.mjs \ + classifier linuxmusl-x64 "$LINUXMUSL_X64_JAR" "$(basename "$LINUXMUSL_X64_JAR")" .. node copilot-native/scripts/validate-native-artifact.mjs \ checksum "$WINDOWS_JAR" "$WINDOWS_MANIFEST" "$(basename "$WINDOWS_JAR")" node copilot-native/scripts/validate-native-artifact.mjs \ @@ -488,6 +582,7 @@ jobs: "-Drevision=$VERSION" \ -Dcopilot.native.libc=glibc \ "-Dcopilot.native.external.linux.arm64.classifier.path=$LINUX_ARM64_JAR" \ + "-Dcopilot.native.external.linuxmusl.x64.classifier.path=$LINUXMUSL_X64_JAR" \ "-Dcopilot.native.external.win32.classifier.path=$WINDOWS_JAR" \ "-Dcopilot.native.external.win32.arm64.classifier.path=$WINDOWS_ARM64_JAR" \ "-Dcopilot.native.external.darwin.classifier.path=$DARWIN_JAR" \ diff --git a/java/copilot-native/pom.xml b/java/copilot-native/pom.xml index f4ac485862..2c750e4e47 100644 --- a/java/copilot-native/pom.xml +++ b/java/copilot-native/pom.xml @@ -409,6 +409,64 @@ + + native-linuxmusl-x64 + + + Linux + amd64 + + + copilot.native.libc + musl + + + + linuxmusl-x64 + + + + + org.codehaus.mojo + exec-maven-plugin + + + validate-native-host + validate + + + fetch-native + generate-resources + + + test-fetch-native + test + + + + + org.apache.maven.plugins + maven-jar-plugin + + + jar-native + package + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + verify-native-jars + package + + + + + + native-win32-x64 @@ -689,6 +747,68 @@ + + + attach-external-linuxmusl-x64-classifier + + + copilot.native.external.linuxmusl.x64.classifier.path + + + + + + org.codehaus.mojo + exec-maven-plugin + + + validate-external-linuxmusl-x64-classifier + validate + + exec + + + node + + ${project.basedir}/scripts/validate-native-artifact.mjs + classifier + linuxmusl-x64 + ${copilot.native.external.linuxmusl.x64.classifier.path} + ${project.build.finalName}-linuxmusl-x64.jar + ${copilot.sdk.root} + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + attach-external-linuxmusl-x64-classifier + package + + attach-artifact + + + + + ${copilot.native.external.linuxmusl.x64.classifier.path} + jar + linuxmusl-x64 + + + + + + + + + + net.java.dev.jna @@ -644,7 +644,7 @@ CI enforces both checks. Spotless runs explicitly in CI; `mvn verify` alone does Run native-runtime Maven commands from the `java` directory. Native packaging requires Node.js in addition to JDK 25 and Maven because `copilot-native/scripts/fetch-native.mjs` retrieves the pinned runtime package from the corresponding GitHub release. -On a native Linux glibc host, Maven activates `native-linux-x64` or `native-linux-arm64` for the matching architecture when `copilot.native.libc=glibc` is set. On Windows x64, Windows ARM64, Intel macOS, and Apple Silicon macOS, Maven activates `native-win32-x64`, `native-win32-arm64`, `native-darwin-x64`, or `native-darwin-arm64` automatically. The matching profile validates the host, runs the native script tests, fetches the pinned platform package from the corresponding `github/copilot-cli` release during `generate-resources`, packages the classifier JAR during `package`, and verifies its native contents. +On a native Linux glibc host, Maven activates `native-linux-x64` or `native-linux-arm64` for the matching architecture when `copilot.native.libc=glibc` is set. On a Linux musl x64 host, Maven activates `native-linuxmusl-x64` when `copilot.native.libc=musl` is set. On Windows x64, Windows ARM64, Intel macOS, and Apple Silicon macOS, Maven activates `native-win32-x64`, `native-win32-arm64`, `native-darwin-x64`, or `native-darwin-arm64` automatically. The matching profile validates the host, runs the native script tests, fetches the pinned platform package from the corresponding `github/copilot-cli` release during `generate-resources`, packages the classifier JAR during `package`, and verifies its native contents. Before opting in, validate that Node.js reports glibc for the build host: @@ -679,7 +679,14 @@ node copilot-native/scripts/validate-native-host.mjs linux-arm64 mvn -Pinprocess clean verify -Dcopilot.native.libc=glibc ``` -On Linux musl and other unsupported hosts, do not set `copilot.native.libc=glibc`. A normal build produces only the OS-neutral primary, sources, and Javadoc JARs; it does not run native script tests, download or stage native files, or produce a platform classifier JAR. +The same command validates in-process mode on Linux musl x64: + +```bash +node copilot-native/scripts/validate-native-host.mjs linuxmusl-x64 +mvn -Pinprocess clean verify -Dcopilot.native.libc=musl +``` + +On Linux musl ARM64 and other unsupported hosts, do not set `copilot.native.libc`. A normal build produces only the OS-neutral primary, sources, and Javadoc JARs; it does not run native script tests, download or stage native files, or produce a platform classifier JAR. To build only the OS-neutral artifacts on any host, or override the glibc opt-in, disable native download and packaging: diff --git a/java/docs/adr/adr-007-native-bundling-strategy.md b/java/docs/adr/adr-007-native-bundling-strategy.md index a1c4ac6b6c..1187a4ae1b 100644 --- a/java/docs/adr/adr-007-native-bundling-strategy.md +++ b/java/docs/adr/adr-007-native-bundling-strategy.md @@ -362,7 +362,7 @@ The pattern follows DJL's `LibUtils.loadLibrary()` approach: detect the platform 3. Extracts `runtime.node` and the transitional CLI entrypoint into `~/.copilot/runtime-cache/` if valid cached files are not already present. 4. Loads it via [JNA](#references) using the C ABI entry points, per the [binding technology decision](#binding-technology-jna-over-panama-ffm) above. The JNA-specific code is confined behind an internal binding interface to preserve a future FFM migration path. * A validated supported-host profile fetches the matching platform tarball from the pinned `github/copilot-cli` release, verifies its release SHA-256, and packages the version-matched runtime files. -* The current release work publishes the `linux-x64`, `linux-arm64`, `win32-x64`, `win32-arm64`, `darwin-x64`, and `darwin-arm64` classifiers. The planned classifier set expands to the other detected platforms. +* The current release work publishes the `linux-x64`, `linux-arm64`, `linuxmusl-x64`, `win32-x64`, `win32-arm64`, `darwin-x64`, and `darwin-arm64` classifiers. The planned classifier set expands to the other detected platforms. * Adding an implemented platform requires validated host activation, a profile that supplies the classifier and platform CLI filename, and lifecycle bindings for the shared host validation, fetch, script test, package, and verification executions. * `cli-native.node` is not bundled. It provides terminal UI features that are irrelevant to the Java SDK's programmatic API surface. diff --git a/java/sdk/pom.xml b/java/sdk/pom.xml index b5bbe7fd1e..8d527a99ba 100644 --- a/java/sdk/pom.xml +++ b/java/sdk/pom.xml @@ -707,6 +707,10 @@ did not produce the multi-release output. Re-build on JDK 25+ and verify the native-linuxmusl-x64 + + Linux + amd64 + copilot.native.libc musl From a9bad356b4f7bd3c1baeab1edbb7c8e3c8204508 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 17 Sep 2026 23:18:09 +0000 Subject: [PATCH 5/7] ci(java): rename windows classifier jobs to build-windows-x64-classifier Co-authored-by: edburns <75821+edburns@users.noreply.github.com> --- .github/workflows/java-publish-maven.yml | 6 +++--- .github/workflows/java-publish-snapshot.yml | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/java-publish-maven.yml b/.github/workflows/java-publish-maven.yml index 932d571622..d8ca560ab7 100644 --- a/.github/workflows/java-publish-maven.yml +++ b/.github/workflows/java-publish-maven.yml @@ -279,8 +279,8 @@ jobs: if-no-files-found: error retention-days: 1 - build-windows-classifier: - name: Build Windows native classifier + build-windows-x64-classifier: + name: Build Windows x64 native classifier needs: resolve-source runs-on: windows-latest permissions: @@ -518,7 +518,7 @@ jobs: resolve-source, build-linux-arm64-classifier, build-linuxmusl-x64-classifier, - build-windows-classifier, + build-windows-x64-classifier, build-windows-arm64-classifier, build-darwin-classifier, build-darwin-x64-classifier, diff --git a/.github/workflows/java-publish-snapshot.yml b/.github/workflows/java-publish-snapshot.yml index aa7ff58bde..ce5b8c5f4e 100644 --- a/.github/workflows/java-publish-snapshot.yml +++ b/.github/workflows/java-publish-snapshot.yml @@ -171,8 +171,8 @@ jobs: if-no-files-found: error retention-days: 1 - build-windows-classifier: - name: Build Windows snapshot classifier + build-windows-x64-classifier: + name: Build Windows x64 snapshot classifier needs: resolve-source runs-on: windows-latest permissions: @@ -426,7 +426,7 @@ jobs: resolve-source, build-linux-arm64-classifier, build-linuxmusl-x64-classifier, - build-windows-classifier, + build-windows-x64-classifier, build-windows-arm64-classifier, build-darwin-classifier, build-darwin-x64-classifier, @@ -527,7 +527,7 @@ jobs: exit 1 fi VERSION="${{ steps.linux-arm64-artifact.outputs.version }}" - if [ "$VERSION" != "${{ needs.build-windows-classifier.outputs.version }}" ]; then + if [ "$VERSION" != "${{ needs.build-windows-x64-classifier.outputs.version }}" ]; then echo "::error::Windows classifier version does not match deploy version." exit 1 fi From 648d39f00d793dff40673a34ba00c9ba0e41c6fd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Sep 2026 00:08:33 +0000 Subject: [PATCH 6/7] ci(java): clarify native publication job IDs Co-authored-by: edburns <75821+edburns@users.noreply.github.com> --- .github/workflows/java-sdk-tests.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/java-sdk-tests.yml b/.github/workflows/java-sdk-tests.yml index c625894ab8..b491717be6 100644 --- a/.github/workflows/java-sdk-tests.yml +++ b/.github/workflows/java-sdk-tests.yml @@ -233,7 +233,7 @@ jobs: overwrite: true retention-days: 1 - java-native-publication-windows: + java-native-publication-windows-x64: name: "Java Native Publication Input (win32-x64)" if: github.event.repository.fork == false runs-on: windows-latest @@ -349,7 +349,7 @@ jobs: overwrite: true retention-days: 1 - java-native-publication-darwin: + java-native-publication-darwin-arm64: name: "Java Native Publication Input (darwin-arm64)" if: github.event.repository.fork == false runs-on: macos-26 @@ -473,9 +473,9 @@ jobs: [ java-native-publication-linux-arm64, java-native-publication-linuxmusl-x64, - java-native-publication-windows, + java-native-publication-windows-x64, java-native-publication-windows-arm64, - java-native-publication-darwin, + java-native-publication-darwin-arm64, java-native-publication-darwin-x64, ] runs-on: ubuntu-latest @@ -534,16 +534,16 @@ jobs: set -euo pipefail test "$(git rev-parse HEAD)" = "${{ needs.java-native-publication-linux-arm64.outputs.source_sha }}" test "$(git rev-parse HEAD)" = "${{ needs.java-native-publication-linuxmusl-x64.outputs.source_sha }}" - test "$(git rev-parse HEAD)" = "${{ needs.java-native-publication-windows.outputs.source_sha }}" + test "$(git rev-parse HEAD)" = "${{ needs.java-native-publication-windows-x64.outputs.source_sha }}" test "$(git rev-parse HEAD)" = "${{ needs.java-native-publication-windows-arm64.outputs.source_sha }}" - test "$(git rev-parse HEAD)" = "${{ needs.java-native-publication-darwin.outputs.source_sha }}" + test "$(git rev-parse HEAD)" = "${{ needs.java-native-publication-darwin-arm64.outputs.source_sha }}" test "$(git rev-parse HEAD)" = "${{ needs.java-native-publication-darwin-x64.outputs.source_sha }}" - VERSION="${{ needs.java-native-publication-windows.outputs.version }}" + VERSION="${{ needs.java-native-publication-windows-x64.outputs.version }}" test "$VERSION" = "$JAVA_PUBLICATION_REVISION" test "$VERSION" = "${{ needs.java-native-publication-linux-arm64.outputs.version }}" test "$VERSION" = "${{ needs.java-native-publication-linuxmusl-x64.outputs.version }}" test "$VERSION" = "${{ needs.java-native-publication-windows-arm64.outputs.version }}" - test "$VERSION" = "${{ needs.java-native-publication-darwin.outputs.version }}" + test "$VERSION" = "${{ needs.java-native-publication-darwin-arm64.outputs.version }}" test "$VERSION" = "${{ needs.java-native-publication-darwin-x64.outputs.version }}" LINUX_ARM64_DIRECTORY="$GITHUB_WORKSPACE/java/native-publication-input/linux-arm64" LINUXMUSL_X64_DIRECTORY="$GITHUB_WORKSPACE/java/native-publication-input/linuxmusl-x64" From 1eb7948a085e297ebd2bbf2ea523130d3d0a1b37 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Fri, 18 Sep 2026 18:51:04 +0000 Subject: [PATCH 7/7] fix(java): positively detect musl native hosts Parse the running Node executable's ELF interpreter and require the architecture-specific musl loader before packaging a musl classifier. Reject glibc, static, malformed, and otherwise unknown ELF hosts even when Node omits its glibc runtime report. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b30bdaec-2ebd-4d51-ab1e-4de5e43772cb --- .../scripts/validate-native-host.mjs | 159 +++++++++++++++++- .../scripts/validate-native-host.test.mjs | 159 +++++++++++++++++- 2 files changed, 314 insertions(+), 4 deletions(-) diff --git a/java/copilot-native/scripts/validate-native-host.mjs b/java/copilot-native/scripts/validate-native-host.mjs index 43a1878f35..20b261f4b1 100644 --- a/java/copilot-native/scripts/validate-native-host.mjs +++ b/java/copilot-native/scripts/validate-native-host.mjs @@ -2,13 +2,142 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ +import { readFileSync } from "node:fs"; +import { posix as path } from "node:path"; import { pathToFileURL } from "node:url"; +const ELF64_HEADER_SIZE = 64; +const ELF64_PROGRAM_HEADER_SIZE = 56; +const ELFCLASS64 = 2; +const ELFDATA2LSB = 1; +const EV_CURRENT = 1; +const EM_X86_64 = 62; +const EM_AARCH64 = 183; +const PT_INTERP = 3; + +const MUSL_ARCHITECTURES = { + x64: { + arch: "x64", + displayArch: "x64", + elfMachine: EM_X86_64, + interpreter: "ld-musl-x86_64.so.1", + }, + arm64: { + arch: "arm64", + displayArch: "ARM64", + elfMachine: EM_AARCH64, + interpreter: "ld-musl-aarch64.so.1", + }, +}; + +const MUSL_CLASSIFIERS = { + "linuxmusl-x64": MUSL_ARCHITECTURES.x64, +}; + +function readSafeInteger(buffer, offset, label) { + const value = buffer.readBigUInt64LE(offset); + if (value > BigInt(Number.MAX_SAFE_INTEGER)) { + throw new Error(`${label} exceeds the supported size`); + } + return Number(value); +} + +export function readElfInterpreter(buffer) { + if (buffer.length < ELF64_HEADER_SIZE) { + throw new Error("Node executable is too small to contain an ELF64 header"); + } + if ( + buffer[0] !== 0x7f || + buffer[1] !== 0x45 || + buffer[2] !== 0x4c || + buffer[3] !== 0x46 + ) { + throw new Error("Node executable is not an ELF file"); + } + if (buffer[4] !== ELFCLASS64 || buffer[5] !== ELFDATA2LSB) { + throw new Error("Node executable is not a little-endian ELF64 file"); + } + if (buffer[6] !== EV_CURRENT || buffer.readUInt32LE(20) !== EV_CURRENT) { + throw new Error("Node executable uses an unsupported ELF version"); + } + + const machine = buffer.readUInt16LE(18); + const programHeaderOffset = readSafeInteger( + buffer, + 32, + "ELF program header offset", + ); + const programHeaderEntrySize = buffer.readUInt16LE(54); + const programHeaderCount = buffer.readUInt16LE(56); + + if (programHeaderCount === 0xffff) { + throw new Error("Extended ELF program header counts are unsupported"); + } + if ( + programHeaderCount > 0 && + programHeaderEntrySize < ELF64_PROGRAM_HEADER_SIZE + ) { + throw new Error("ELF program header entries are too small"); + } + + const programHeaderEnd = + BigInt(programHeaderOffset) + + BigInt(programHeaderEntrySize) * BigInt(programHeaderCount); + if (programHeaderEnd > BigInt(buffer.length)) { + throw new Error("ELF program header table extends beyond the executable"); + } + + let interpreter; + for (let index = 0; index < programHeaderCount; index += 1) { + const entryOffset = programHeaderOffset + index * programHeaderEntrySize; + if (buffer.readUInt32LE(entryOffset) !== PT_INTERP) { + continue; + } + if (interpreter !== undefined) { + throw new Error("Node executable contains multiple ELF interpreters"); + } + + const interpreterOffset = readSafeInteger( + buffer, + entryOffset + 8, + "ELF interpreter offset", + ); + const interpreterSize = readSafeInteger( + buffer, + entryOffset + 32, + "ELF interpreter size", + ); + const interpreterEnd = BigInt(interpreterOffset) + BigInt(interpreterSize); + if (interpreterSize < 2 || interpreterEnd > BigInt(buffer.length)) { + throw new Error("ELF interpreter extends beyond the executable"); + } + + const bytes = buffer.subarray( + interpreterOffset, + interpreterOffset + interpreterSize, + ); + if ( + bytes[bytes.length - 1] !== 0 || + bytes.subarray(0, bytes.length - 1).includes(0) + ) { + throw new Error("ELF interpreter is not a valid null-terminated path"); + } + + interpreter = bytes.subarray(0, bytes.length - 1).toString("utf8"); + if (!interpreter.startsWith("/")) { + throw new Error("ELF interpreter path is not absolute"); + } + } + + return { machine, interpreter }; +} + export function validateNativeHost(classifier, host) { - if (classifier === "linuxmusl-x64") { - if (host.platform !== "linux" || host.arch !== "x64") { + const muslHost = MUSL_CLASSIFIERS[classifier]; + if (muslHost) { + if (host.platform !== "linux" || host.arch !== muslHost.arch) { throw new Error( - `Native ${classifier} packaging requires Linux x64; detected ${host.platform}-${host.arch}`, + `Native ${classifier} packaging requires Linux ${muslHost.displayArch}; detected ${host.platform}-${host.arch}`, ); } if (host.glibcVersionRuntime) { @@ -16,6 +145,17 @@ export function validateNativeHost(classifier, host) { `Native ${classifier} packaging requires musl; detected glibc ${host.glibcVersionRuntime}`, ); } + if ( + host.elfMachine !== muslHost.elfMachine || + path.basename(host.elfInterpreter ?? "") !== muslHost.interpreter + ) { + const detected = host.elfDetectionError + ? `unable to inspect the Node executable: ${host.elfDetectionError}` + : `${host.elfInterpreter ?? "no dynamic ELF interpreter"} (ELF machine ${host.elfMachine ?? "unknown"})`; + throw new Error( + `Native ${classifier} packaging requires musl; detected ${detected}`, + ); + } return `Validated native build host: ${classifier} (musl)`; } @@ -62,10 +202,23 @@ export function validateNativeHost(classifier, host) { export function detectNativeHost() { const report = process.report?.getReport(); + let elf; + let elfDetectionError; + if (process.platform === "linux") { + try { + elf = readElfInterpreter(readFileSync("/proc/self/exe")); + } catch (error) { + elfDetectionError = + error instanceof Error ? error.message : String(error); + } + } return { platform: process.platform, arch: process.arch, glibcVersionRuntime: report?.header?.glibcVersionRuntime, + elfMachine: elf?.machine, + elfInterpreter: elf?.interpreter, + elfDetectionError, }; } diff --git a/java/copilot-native/scripts/validate-native-host.test.mjs b/java/copilot-native/scripts/validate-native-host.test.mjs index 8ec724a09e..89f68734e2 100644 --- a/java/copilot-native/scripts/validate-native-host.test.mjs +++ b/java/copilot-native/scripts/validate-native-host.test.mjs @@ -5,7 +5,103 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { validateNativeHost } from "./validate-native-host.mjs"; +import { + readElfInterpreter, + validateNativeHost, +} from "./validate-native-host.mjs"; + +function createElf64({ machine = 62, interpreter } = {}) { + const interpreterBytes = + interpreter === undefined + ? undefined + : Buffer.from(`${interpreter}\0`, "utf8"); + const programHeaderCount = interpreterBytes === undefined ? 0 : 1; + const interpreterOffset = ELF64_HEADER_SIZE + ELF64_PROGRAM_HEADER_SIZE; + const buffer = Buffer.alloc( + interpreterOffset + (interpreterBytes?.length ?? 0), + ); + + buffer.set([0x7f, 0x45, 0x4c, 0x46], 0); + buffer[4] = 2; + buffer[5] = 1; + buffer[6] = 1; + buffer.writeUInt16LE(machine, 18); + buffer.writeUInt32LE(1, 20); + buffer.writeBigUInt64LE( + BigInt(programHeaderCount === 0 ? 0 : ELF64_HEADER_SIZE), + 32, + ); + buffer.writeUInt16LE(ELF64_HEADER_SIZE, 52); + buffer.writeUInt16LE(ELF64_PROGRAM_HEADER_SIZE, 54); + buffer.writeUInt16LE(programHeaderCount, 56); + + if (interpreterBytes !== undefined) { + buffer.writeUInt32LE(3, ELF64_HEADER_SIZE); + buffer.writeBigUInt64LE(BigInt(interpreterOffset), ELF64_HEADER_SIZE + 8); + buffer.writeBigUInt64LE( + BigInt(interpreterBytes.length), + ELF64_HEADER_SIZE + 32, + ); + interpreterBytes.copy(buffer, interpreterOffset); + } + + return buffer; +} + +const ELF64_HEADER_SIZE = 64; +const ELF64_PROGRAM_HEADER_SIZE = 56; + +test("reads the x64 musl interpreter from an ELF executable", () => { + assert.deepEqual( + readElfInterpreter( + createElf64({ + interpreter: "/lib/ld-musl-x86_64.so.1", + }), + ), + { + machine: 62, + interpreter: "/lib/ld-musl-x86_64.so.1", + }, + ); +}); + +test("reads the ARM64 musl interpreter from an ELF executable", () => { + assert.deepEqual( + readElfInterpreter( + createElf64({ + machine: 183, + interpreter: "/lib/ld-musl-aarch64.so.1", + }), + ), + { + machine: 183, + interpreter: "/lib/ld-musl-aarch64.so.1", + }, + ); +}); + +test("reports a static ELF executable without an interpreter", () => { + assert.deepEqual(readElfInterpreter(createElf64()), { + machine: 62, + interpreter: undefined, + }); +}); + +test("rejects a truncated ELF executable", () => { + assert.throws( + () => readElfInterpreter(Buffer.from([0x7f, 0x45, 0x4c, 0x46])), + /too small/, + ); +}); + +test("rejects an ELF interpreter outside the executable", () => { + const buffer = createElf64({ + interpreter: "/lib/ld-musl-x86_64.so.1", + }); + buffer.writeBigUInt64LE(BigInt(buffer.length + 1), ELF64_HEADER_SIZE + 8); + + assert.throws(() => readElfInterpreter(buffer), /interpreter extends beyond/); +}); test("accepts Linux x64 with glibc", () => { assert.equal( @@ -35,6 +131,8 @@ test("accepts Linux musl x64", () => { platform: "linux", arch: "x64", glibcVersionRuntime: undefined, + elfMachine: 62, + elfInterpreter: "/lib/ld-musl-x86_64.so.1", }), "Validated native build host: linuxmusl-x64 (musl)", ); @@ -115,11 +213,68 @@ test("rejects Linux musl x64 with glibc", () => { platform: "linux", arch: "x64", glibcVersionRuntime: "2.39", + elfMachine: 62, + elfInterpreter: "/lib64/ld-linux-x86-64.so.2", }), /requires musl/, ); }); +test("rejects Linux musl x64 when the glibc report is unavailable", () => { + assert.throws( + () => + validateNativeHost("linuxmusl-x64", { + platform: "linux", + arch: "x64", + glibcVersionRuntime: undefined, + elfMachine: 62, + elfInterpreter: "/lib64/ld-linux-x86-64.so.2", + }), + /ld-linux-x86-64/, + ); +}); + +test("rejects Linux musl x64 with unknown libc", () => { + assert.throws( + () => + validateNativeHost("linuxmusl-x64", { + platform: "linux", + arch: "x64", + glibcVersionRuntime: undefined, + elfMachine: 62, + elfInterpreter: undefined, + }), + /no dynamic ELF interpreter/, + ); +}); + +test("rejects Linux musl x64 with the ARM64 musl interpreter", () => { + assert.throws( + () => + validateNativeHost("linuxmusl-x64", { + platform: "linux", + arch: "x64", + glibcVersionRuntime: undefined, + elfMachine: 183, + elfInterpreter: "/lib/ld-musl-aarch64.so.1", + }), + /ld-musl-aarch64/, + ); +}); + +test("rejects Linux musl x64 when ELF detection fails", () => { + assert.throws( + () => + validateNativeHost("linuxmusl-x64", { + platform: "linux", + arch: "x64", + glibcVersionRuntime: undefined, + elfDetectionError: "permission denied", + }), + /unable to inspect the Node executable: permission denied/, + ); +}); + test("rejects a non-x64 host for Linux musl x64", () => { assert.throws( () => @@ -247,6 +402,8 @@ test("rejects an unimplemented classifier", () => { platform: "linux", arch: "arm64", glibcVersionRuntime: undefined, + elfMachine: 183, + elfInterpreter: "/lib/ld-musl-aarch64.so.1", }), /Unsupported native build classifier/, );