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/,
);