Skip to content

Build installers

Build installers #304

name: Build installers
on:
workflow_dispatch:
inputs:
platforms:
description: 'Platforms to build: "all", "none" (only the shared native package), or a comma-separated subset of windows-x86-64,macosx-x86-64,macosx-aarch64,linux-x86-64,linux-aarch64'
required: false
default: 'all'
# Least privilege: this pipeline only builds and uploads artifacts; it never writes to the repo.
permissions:
contents: read
jobs:
setup:
runs-on: ubuntu-latest
outputs:
jpackage_matrix: ${{ steps.set.outputs.jpackage_matrix }}
linux_matrix: ${{ steps.set.outputs.linux_matrix }}
steps:
- name: Compute build matrices from the platforms input
id: set
shell: bash
env:
PLATFORMS_INPUT: ${{ inputs.platforms }}
run: |
sel="$PLATFORMS_INPUT"
[ -z "$sel" ] && sel="all"
jpkg=$(jq -cn --arg sel "$sel" '
[
{key:"macosx-x86-64", os:"macos-15-intel", platform:"x64", wsystem:"macosx", warc:"x86-64"},
{key:"macosx-aarch64", os:"macos-latest", platform:"arm64", wsystem:"macosx", warc:"aarch64"},
{key:"windows-x86-64", os:"windows-latest", platform:"x64", wsystem:"windows", warc:"x86-64"}
]
| map(select($sel == "all" or (.key as $k | ($sel | split(",")) | index($k) != null)))
| map(del(.key))')
linux=$(jq -cn --arg sel "$sel" '
[
{key:"linux-x86-64", arch:"linux/amd64"},
{key:"linux-aarch64", arch:"linux/arm64"}
]
| map(select($sel == "all" or (.key as $k | ($sel | split(",")) | index($k) != null)))
| map(.arch)')
{
echo "jpackage_matrix=$jpkg"
echo "linux_matrix=$linux"
} >> "$GITHUB_OUTPUT"
echo "Selected platforms: $sel"
echo "jpackage matrix: $jpkg"
echo "linux matrix: $linux"
build:
runs-on: ubuntu-latest
outputs:
version: ${{ env.WEASIS_VERSION }}
cleanversion: ${{ env.WEASIS_CLEAN_VERSION }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up JDK for compilation
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with:
distribution: 'temurin'
java-version: '26'
cache: 'maven'
- name: Build with Maven
shell: bash
run: |
mvn -B clean install
mvn -B -P compressXZ -f weasis-distributions clean package
file="weasis-distributions/target/native-dist/weasis-native.zip"
sha256sum "$file" >> "$file.sha256"
- name: Retrieve version
shell: bash
run: |
FVERSION=$(awk -F'[<>]' '/<revision>/{print $3}' weasis-parent/pom.xml)$(awk -F'[<>]' '/<changelist>/{print $3}' weasis-parent/pom.xml)
echo "WEASIS_VERSION=${FVERSION}" >> $GITHUB_ENV
- name: Set compatible version
shell: bash
run: |
CVERSION=$(echo $WEASIS_VERSION | sed -e 's/"//g' -e 's/-.*//' -e 's/\(\([0-9]\+\.\)\{2\}[0-9]\+\)\.[0-9]\+/\1/')
echo "WEASIS_CLEAN_VERSION=${CVERSION}" >> $GITHUB_ENV
- name: Upload the weasis package for building the native distribution
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: weasis-native-${{ env.WEASIS_VERSION }}
path: weasis-distributions/target/native-dist/weasis-native.*
jpackage:
env:
NAME: "Weasis"
IDENTIFIER: "org.weasis.launcher"
WEASIS_INPUT: "native-dist/weasis-native/bin-dist"
WEASIS_OUTPUT: "output-dist"
WEASIS_INPUT_BIN: "native-dist/weasis-native/bin-dist/weasis"
WEASIS_SCRIPT: "native-dist/weasis-native/build/script"
runs-on: ${{ matrix.os }}
needs: [setup, build]
if: ${{ needs.setup.outputs.jpackage_matrix != '[]' }}
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(needs.setup.outputs.jpackage_matrix) }}
steps:
- name: Set output version from build
shell: bash
run: |
echo ${{needs.build.outputs.version}}
echo ${{needs.build.outputs.cleanversion}}
echo "WEASIS_VERSION=${{needs.build.outputs.version}}" >> $GITHUB_ENV
echo "WEASIS_CLEAN_VERSION=${{needs.build.outputs.cleanversion}}" >> $GITHUB_ENV
- name: Set up JDK
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with:
distribution: 'temurin'
java-version: '26'
- name: Download weasis-native
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: weasis-native-${{ env.WEASIS_VERSION }}
path: native-dist
- name: Unzip the native package version
shell: bash
run: |
unzip -o "native-dist/weasis-native.zip" -d "native-dist/weasis-native"
- name: Remove binaries not related to the current architecture
shell: bash
run: |
# Remove pack jar for launcher
rm -f ${{ env.WEASIS_INPUT_BIN }}/*.jar.pack.gz
# Remove unrelated weasis-opencv-core native packages
find ${{ env.WEASIS_INPUT_BIN }}/bundle/weasis-opencv-core-* -type f ! -name "*-${{ matrix.wsystem }}-${{ matrix.warc }}-*" -exec rm -f {} \;
# Remove unrelated jogamp native packages
find ${{ env.WEASIS_INPUT_BIN }}/bundle/jogamp-* -type f ! -name "*-${{ matrix.wsystem }}-${{ matrix.warc }}-*" ! -name "jogamp-[0-9]*" -exec rm -f {} \;
echo "$(ls ${{ env.WEASIS_INPUT_BIN }}/bundle/)"
- name: Apply the deployment specific preparation
shell: bash
run: |
siteHook="${{ env.WEASIS_SCRIPT }}/prepare-input-site.sh"
if [[ -f "$siteHook" ]] ; then
bash "$siteHook" "${{ env.WEASIS_INPUT_BIN }}"
else
echo "No deployment specific preparation"
fi
- name: Import Developer Certificate
uses: apple-actions/import-codesign-certs@5142e029c445c10ffc7149d172e540235a065466 # v7
if: matrix.wsystem == 'macosx'
id: cert
with:
p12-file-base64: ${{ secrets.MACOS_CERTIFICATE_DEVELOPMENT }}
p12-password: ${{ secrets.MACOS_CERTIFICATE_PWD }}
- name: Import Installer Certificate
uses: apple-actions/import-codesign-certs@5142e029c445c10ffc7149d172e540235a065466 # v7
if: matrix.wsystem == 'macosx'
with:
create-keychain: "false"
keychain-password: ${{ steps.cert.outputs.keychain-password }}
p12-file-base64: ${{ secrets.MACOS_CERTIFICATE_INSTALLER }}
p12-password: ${{ secrets.MACOS_CERTIFICATE_PWD }}
- name: Install WiX Toolset (Windows only)
if: matrix.wsystem == 'windows'
shell: pwsh
run: |
dotnet tool install --global wix --version "5.*"
echo "$env:USERPROFILE\.dotnet\tools" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
$WIX_VERSION = & "$env:USERPROFILE\.dotnet\tools\wix.exe" --version
Write-Host "Installed WiX version: $WIX_VERSION"
& "$env:USERPROFILE\.dotnet\tools\wix.exe" extension add --global WixToolset.UI.wixext/$WIX_VERSION
& "$env:USERPROFILE\.dotnet\tools\wix.exe" extension add --global WixToolset.Util.wixext/$WIX_VERSION
- name: Build app binary with jpackage
shell: bash
env:
RES: "${{ env.WEASIS_SCRIPT }}/resources/${{ matrix.wsystem }}"
run: |
machine=${{ matrix.wsystem }}
# JDK_MODULES, customOptions and commonOptions are shared with package-weasis.sh
source "${{ env.WEASIS_SCRIPT }}/launch-options.sh" "$machine"
declare -a signArgs=()
if [ "$machine" = "macosx" ] ; then
KEYCHAIN="$HOME/Library/Keychains/signing_temp.keychain-db"
# Enumerate all identities in the keychain for diagnostics.
echo "=== Identities found in signing keychain ==="
security find-identity -v "$KEYCHAIN"
# Resolve the Developer ID Application identity (without -p codesigning
# to avoid trust-policy filtering on non-default keychains).
SIGN_ID=$(security find-identity -v "$KEYCHAIN" \
| grep "Developer ID Application" | head -1 | awk -F'"' '{print $2}')
echo "Developer ID Application identity: $SIGN_ID"
if [ -z "$SIGN_ID" ]; then
echo "ERROR: No 'Developer ID Application' certificate found in $KEYCHAIN"
exit 1
fi
# Extract just the user-name portion required by jpackage
# e.g. "Developer ID Application: Nicolas Roduit (TEAMID)" -> "Nicolas Roduit"
MAC_SIGNING_USER=$(echo "$SIGN_ID" \
| sed 's/^Developer ID Application: //' | sed 's/ (.*//')
echo "jpackage signing user name: $MAC_SIGNING_USER"
# Pre-sign ALL native libraries embedded in JARs.
# jpackage --mac-sign will sign the extracted content of the app image,
# but dylibs INSIDE jars must be signed before jpackage repackages them.
for jar_file in "${{ env.WEASIS_INPUT_BIN }}"/*.jar "${{ env.WEASIS_INPUT_BIN }}"/bundle/*.jar; do
[ -f "$jar_file" ] || continue
if unzip -l "$jar_file" 2>/dev/null | grep -qE '\.(dylib|jnilib)$'; then
echo "--- Signing native libs in: $jar_file"
tmpdir=$(mktemp -d)
unzip -q "$jar_file" -d "$tmpdir"
while IFS= read -r -d '' lib; do
echo " Signing: $lib"
codesign --force --options runtime --timestamp \
--sign "$SIGN_ID" --keychain "$KEYCHAIN" "$lib"
done < <(find "$tmpdir" \( -name "*.dylib" -o -name "*.jnilib" \) -print0)
jar_name=$(basename "$jar_file")
(cd "$tmpdir" && jar cf "/tmp/$jar_name" .)
mv -f "/tmp/$jar_name" "$jar_file"
rm -rf "$tmpdir"
fi
done
# Use jpackage's built-in signing (--mac-sign) which applies --options runtime
# and --timestamp correctly in inside-out order, exactly like package-weasis.sh.
signArgs=("--mac-package-identifier" "${{ env.IDENTIFIER }}" \
"--mac-signing-key-user-name" "$MAC_SIGNING_USER" \
"--mac-sign")
fi
jpackage --type app-image --input "${{ env.WEASIS_INPUT_BIN }}" --dest "${{ env.WEASIS_OUTPUT }}" --name "${{ env.NAME }}" \
--main-jar weasis-launcher.jar --main-class org.weasis.launcher.AppLauncher --add-modules "$JDK_MODULES" \
--resource-dir "${{ env.RES }}" --app-version "${{ env.WEASIS_CLEAN_VERSION }}" --add-launcher "Dicomizer=${{ env.RES }}/dicomizer-launcher.properties" \
"${signArgs[@]}" "${customOptions[@]}" "${commonOptions[@]}"
if [ "$machine" = "macosx" ] ; then
APP_PATH="${{ env.WEASIS_OUTPUT }}/${{ env.NAME }}.app"
echo "--- Verifying app bundle signature (deep + strict)"
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
echo "--- Checking notarization requirements on all Mach-O binaries"
fail=0
while IFS= read -r -d '' bin; do
# codesign -dvvv writes to stderr
info=$(codesign -dvvv "$bin" 2>&1)
cert=$(echo "$info" | grep "Authority=Developer ID Application" || true)
ts=$(echo "$info" | grep "^Timestamp=" || true)
hr=$(echo "$info" | grep "flags=0x10000(runtime)" || true)
if [ -z "$cert" ] || [ -z "$ts" ] || [ -z "$hr" ]; then
echo "FAIL: $bin"
[ -z "$cert" ] && echo " ✗ Developer ID Application authority not found"
[ -z "$ts" ] && echo " ✗ Secure timestamp missing"
[ -z "$hr" ] && echo " ✗ Hardened runtime flag (0x10000) missing"
fail=1
fi
done < <(find "$APP_PATH/Contents/MacOS" "$APP_PATH/Contents/runtime" \
-type f \( -name "*.dylib" -o -name "*.so" -o -name "jspawnhelper" \) -print0)
# Also check the main launcher executables explicitly
for bin in "$APP_PATH/Contents/MacOS/Weasis" "$APP_PATH/Contents/MacOS/Dicomizer"; do
[ -f "$bin" ] || continue
info=$(codesign -dvvv "$bin" 2>&1)
cert=$(echo "$info" | grep "Authority=Developer ID Application" || true)
ts=$(echo "$info" | grep "^Timestamp=" || true)
hr=$(echo "$info" | grep "flags=0x10000(runtime)" || true)
if [ -z "$cert" ] || [ -z "$ts" ] || [ -z "$hr" ]; then
echo "FAIL: $bin"
[ -z "$cert" ] && echo " ✗ Developer ID Application authority not found"
[ -z "$ts" ] && echo " ✗ Secure timestamp missing"
[ -z "$hr" ] && echo " ✗ Hardened runtime flag (0x10000) missing"
fail=1
fi
done
if [ "$fail" -ne 0 ]; then
echo "ERROR: One or more binaries do not meet notarization requirements."
exit 1
fi
echo "All checked binaries meet notarization requirements."
fi
echo "$(ls -la ${{ env.WEASIS_OUTPUT }})"
- name: Build package with jpackage
shell: bash
env:
RES: "${{ env.WEASIS_SCRIPT }}/resources/${{ matrix.wsystem }}"
FILE_ASSOC: "${{ env.WEASIS_SCRIPT }}/file-associations.properties"
VENDOR: "Weasis Team"
COPYRIGHT: "© 2009-2026 Weasis Team"
run: |
mkdir -p final-dist
machine=${{ matrix.wsystem }}
ARC_NAME=${{ matrix.warc }}
if [ "$machine" = "windows" ] ; then
[ "$ARC_NAME" = "aarch64" ] && UPGRADE_UID="3aedc24e-48a8-4623-ab39-0c3c01c7383c" || UPGRADE_UID="3aedc24e-48a8-4623-ab39-0c3c01c7383a"
jpackage --type "msi" --app-image "${{ env.WEASIS_OUTPUT }}/${{ env.NAME }}" --dest "${{ env.WEASIS_OUTPUT }}" --name "${{ env.NAME }}" --resource-dir "${{ env.RES }}/msi/${ARC_NAME}" \
--license-file "${{ env.WEASIS_INPUT }}/Licence.txt" --description "Weasis DICOM viewer" --win-upgrade-uuid "$UPGRADE_UID" \
--win-menu --win-menu-group "${{ env.NAME }}" --copyright "${{ env.COPYRIGHT }}" --app-version "${{ env.WEASIS_CLEAN_VERSION }}" \
--vendor "${{ env.VENDOR }}" --file-associations "${{ env.FILE_ASSOC }}" --verbose
cp ${{ env.WEASIS_OUTPUT }}/*.msi final-dist/${{ env.NAME }}-${{ env.WEASIS_CLEAN_VERSION }}-${ARC_NAME}.msi
elif [ "$machine" = "macosx" ] ; then
# Do not set verbose for public build.
# Do NOT pass --mac-sign here: the app bundle was already signed with proper
# --options runtime + --timestamp in the app-image step above.
# Using --mac-sign at the pkg stage causes jpackage to re-sign every dylib
# without --timestamp, which breaks Apple notarization.
jpackage --type "pkg" --app-image "${{ env.WEASIS_OUTPUT }}/${{ env.NAME }}.app" --dest "${{ env.WEASIS_OUTPUT }}" --name "${{ env.NAME }}" --resource-dir "${{ env.RES }}" \
--license-file "${{ env.WEASIS_INPUT }}/Licence.txt" --copyright "${{ env.COPYRIGHT }}" --app-version "${{ env.WEASIS_CLEAN_VERSION }}" --mac-package-identifier "${{ env.IDENTIFIER }}"
# Sign the unsigned .pkg with the Developer ID Installer certificate.
# productsign only wraps the outer package; it does not touch the app contents.
UNSIGNED_PKG="${{ env.WEASIS_OUTPUT }}/${{ env.NAME }}-${{ env.WEASIS_CLEAN_VERSION }}.pkg"
SIGNED_PKG="final-dist/${{ env.NAME }}-${{ env.WEASIS_CLEAN_VERSION }}-${ARC_NAME}.pkg"
KEYCHAIN="$HOME/Library/Keychains/signing_temp.keychain-db"
INSTALLER_ID=$(security find-identity -v -p basic "$KEYCHAIN" \
| grep "Developer ID Installer" | head -1 | awk -F'"' '{print $2}')
echo "Signing pkg with installer identity: $INSTALLER_ID"
productsign --sign "$INSTALLER_ID" --keychain "$KEYCHAIN" --timestamp \
"$UNSIGNED_PKG" "$SIGNED_PKG"
fi
echo "APP_ARTIFACT=weasis-${machine}-${ARC_NAME}-${{ env.WEASIS_VERSION }}" >> $GITHUB_ENV
- name: Notarize Release Build
if: matrix.wsystem == 'macosx'
shell: bash
run: |
SUBMISSION=$(xcrun notarytool submit \
"final-dist/${{ env.NAME }}-${{ env.WEASIS_CLEAN_VERSION }}-${{ matrix.warc }}.pkg" \
--apple-id "${{ secrets.APPLE_DEVELOPER_AC_USERNAME }}" \
--team-id "${{ secrets.APPLE_TEAM_ID }}" \
--password "${{ secrets.WEASIS_PWD }}" \
--wait --output-format json)
echo "$SUBMISSION"
STATUS=$(echo "$SUBMISSION" | python3 -c "import sys,json; print(json.load(sys.stdin)['status'])")
ID=$(echo "$SUBMISSION" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
if [ "$STATUS" != "Accepted" ]; then
echo "Notarization failed with status: $STATUS. Fetching log..."
xcrun notarytool log "$ID" \
--apple-id "${{ secrets.APPLE_DEVELOPER_AC_USERNAME }}" \
--team-id "${{ secrets.APPLE_TEAM_ID }}" \
--password "${{ secrets.WEASIS_PWD }}"
exit 1
fi
- name: Staple Release Build
if: matrix.wsystem == 'macosx'
shell: bash
run: |
xcrun stapler staple \
"final-dist/${{ env.NAME }}-${{ env.WEASIS_CLEAN_VERSION }}-${{ matrix.warc }}.pkg"
- name: Add checksum sha256 file
shell: bash
run: |
for file in final-dist/*; do
if [[ -f "$file" ]]; then
if [[ "$(uname -s)" == "Darwin" ]]; then
shasum -a 256 "$file" >> "$file.sha256"
else
sha256sum "$file" >> "$file.sha256"
fi
fi
done
- name: Upload the installer
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ env.APP_ARTIFACT }}
path: final-dist
linux-multiarch:
needs: [setup, build]
runs-on: ubuntu-latest
if: ${{ needs.setup.outputs.linux_matrix != '[]' }}
strategy:
matrix:
arch: ${{ fromJSON(needs.setup.outputs.linux_matrix) }}
steps:
- name: Download weasis-native
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: weasis-native-${{ needs.build.outputs.version }}
path: native-dist
- name: Unzip the native package version
shell: bash
run: |
unzip -o "native-dist/weasis-native.zip" -d "native-dist/weasis-native"
- name: Set up QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4
- name: Set up Docker Buildx for multi-arch
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
- name: Run Docker container to package Weasis for ${{ matrix.arch }}
shell: bash
run: |
ARCH="${{ matrix.arch }}"
# Build Docker image for the current architecture
echo "Building Docker image for ${ARCH}..."
docker buildx build --load --platform "${ARCH}" -t weasis/builder:latest native-dist/weasis-native/build/docker
# Run Docker container to package weasis
echo "Running Docker container to package Weasis for ${ARCH}..."
docker run --platform "${ARCH}" --rm \
-v "$(pwd)/native-dist/weasis-native:/work" \
weasis/builder:latest \
bash -c "export JAVA_TOOL_OPTIONS=-Djdk.lang.Process.launchMechanism=vfork; mkdir -p /work/installer; \
cd /work/installer; /work/build/script/package-weasis.sh --jdk /opt/java/openjdk --temp /work/temp"
echo "--- Contents of installer directory:"
ls -la native-dist/weasis-native/installer/ || echo "[WARN] No installer directory found"
PKG_ARTIFACT=$(ls -d native-dist/weasis-native/installer/*/ 2>/dev/null | head -n 1 | xargs -n 1 basename)
if [ -z "$PKG_ARTIFACT" ]; then
echo "[ERROR] No installer subdirectory found under native-dist/weasis-native/installer/"
exit 1
fi
echo "PKG_ARTIFACT=${PKG_ARTIFACT}" >> $GITHUB_ENV
echo "Found package artifact: ${PKG_ARTIFACT}"
echo "--- Contents of installer/${PKG_ARTIFACT}:"
ls -la "native-dist/weasis-native/installer/${PKG_ARTIFACT}/"
mkdir -p final-dist
# Copy .deb and .rpm files to the final-dist directory
DEB_COUNT=$(find "native-dist/weasis-native/installer/${PKG_ARTIFACT}" -maxdepth 1 -name "*.deb" | wc -l)
RPM_COUNT=$(find "native-dist/weasis-native/installer/${PKG_ARTIFACT}" -maxdepth 1 -name "*.rpm" | wc -l)
if [ "$DEB_COUNT" -gt 0 ]; then
cp native-dist/weasis-native/installer/${PKG_ARTIFACT}/*.deb final-dist/
else
echo "[INFO] No .deb files found to copy."
fi
if [ "$RPM_COUNT" -gt 0 ]; then
cp native-dist/weasis-native/installer/${PKG_ARTIFACT}/*.rpm final-dist/
else
echo "[INFO] No .rpm files found to copy."
fi
echo "--- Contents of final-dist:"
ls -la final-dist/
if [ -z "$(ls -A final-dist/)" ]; then
echo "[ERROR] final-dist is empty — no .deb or .rpm files were produced."
exit 1
fi
for file in final-dist/*; do
if [[ -f "$file" ]]; then
sha256sum "$file" >> "$file.sha256"
fi
done
timeout-minutes: 45
continue-on-error: false
- name: Upload Linux multi-arch installers
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ env.PKG_ARTIFACT }}
path: final-dist
if-no-files-found: error