Skip to content

Package Tomviz Installers #498

Package Tomviz Installers

Package Tomviz Installers #498

Workflow file for this run

name: Package Tomviz Installers
on:
# Rebuild on push to master when packaging code changes
push:
branches:
- main
- master
paths:
- 'packaging/**'
- '.github/workflows/package.yml'
# Poll for new conda-forge releases every 6 hours
schedule:
- cron: '0 */6 * * *'
env:
PYTHON_VERSION: '3.13'
jobs:
check-version:
runs-on: ubuntu-latest
outputs:
tomviz_version: ${{ steps.check.outputs.tomviz_version }}
build_string: ${{ steps.check.outputs.build_string }}
should_build: ${{ steps.check.outputs.should_build }}
steps:
- uses: actions/checkout@v4
- name: Check for new version or build
id: check
env:
GH_TOKEN: ${{ github.token }}
PYTHON_VERSION: ${{ env.PYTHON_VERSION }}
run: |
# Get the latest tomviz version and build string from conda-forge.
# If the script errors, set -e aborts the step before we use the values.
LATEST=$(python3 .github/scripts/latest_conda_forge_tomviz.py "$PYTHON_VERSION")
read -r VERSION BUILD_STRING <<< "$LATEST"
echo "Latest conda-forge: $VERSION ($BUILD_STRING)"
echo "tomviz_version=$VERSION" >> "$GITHUB_OUTPUT"
echo "build_string=$BUILD_STRING" >> "$GITHUB_OUTPUT"
# Always rebuild on push (packaging code changed); otherwise check for new version/build.
if [ "${{ github.event_name }}" = "push" ]; then
echo "Push event — forcing rebuild."
echo "should_build=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Check if we already have a release for this version
EXISTING_NOTES=$(gh release view "v$VERSION" --json body --jq '.body' 2>/dev/null || echo "")
if [ -z "$EXISTING_NOTES" ]; then
echo "No existing release for v$VERSION — will build."
echo "should_build=true" >> "$GITHUB_OUTPUT"
elif echo "$EXISTING_NOTES" | grep -q "build: $BUILD_STRING"; then
echo "Release v$VERSION already matches build $BUILD_STRING — skipping."
echo "should_build=false" >> "$GITHUB_OUTPUT"
else
echo "Release v$VERSION exists but build changed to $BUILD_STRING — rebuilding."
# The old release will be replaced by create-release if the build succeeds.
echo "should_build=true" >> "$GITHUB_OUTPUT"
fi
build-macos:
needs: check-version
if: needs.check-version.outputs.should_build == 'true'
strategy:
fail-fast: false
matrix:
include:
- runner: macos-15-intel
arch: x86_64
- runner: macos-latest
arch: arm64
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- uses: conda-incubator/setup-miniconda@v3
with:
miniforge-version: latest
environment-file: packaging/environment.yml
conda-remove-defaults: 'true'
- name: Build package
shell: bash -el {0}
env:
TOMVIZ_VERSION: ${{ needs.check-version.outputs.tomviz_version }}
TOMVIZ_PYTHON_VERSION: ${{ env.PYTHON_VERSION }}
run: |
cd packaging
python package.py --tomviz-version "$TOMVIZ_VERSION" --python-version "$TOMVIZ_PYTHON_VERSION"
# Retry cpack up to 3 times (hdiutil detach can fail transiently)
for attempt in 1 2 3; do
if cpack --config CPackConfig.cmake; then
break
fi
echo "CPack attempt $attempt failed, retrying in 10s..."
rm -rf _CPack_Packages
sleep 10
done
- name: Verify package (structural)
shell: bash -el {0}
env:
TOMVIZ_PYTHON_VERSION: ${{ env.PYTHON_VERSION }}
run: |
cd packaging
APP_DIR=$(find _build/install -name "*.app" -type d | head -1)
python verify.py "$APP_DIR" --python-version "$TOMVIZ_PYTHON_VERSION"
- name: Verify package (smoke test via mounted DMG)
run: |
DMG=$(ls packaging/Tomviz-*.dmg | head -1)
echo "Mounting $DMG ..."
MOUNT_POINT=$(hdiutil attach "$DMG" -nobrowse -readonly -plist | /usr/bin/python3 -c "
import sys, plistlib
data = plistlib.loads(sys.stdin.buffer.read())
for e in data.get('system-entities', []):
if 'mount-point' in e:
print(e['mount-point']); break
")
if [ -z "$MOUNT_POINT" ]; then
echo "FAIL: could not determine DMG mount point"
exit 1
fi
trap 'hdiutil detach "$MOUNT_POINT" 2>/dev/null || true' EXIT
echo "Mounted at: $MOUNT_POINT"
APP="$MOUNT_POINT/tomviz.app"
if [ ! -d "$APP" ]; then
echo "FAIL: tomviz.app not found in DMG"
ls "$MOUNT_POINT" || true
exit 1
fi
bash .github/scripts/smoke_test_launcher.sh "$APP/Contents/MacOS/tomviz"
- name: Check artifact size
run: bash .github/scripts/check_artifact_size.sh "packaging/Tomviz-*.dmg"
- name: Rename artifact
run: |
mv packaging/Tomviz-*.dmg "packaging/Tomviz-${{ needs.check-version.outputs.tomviz_version }}-${{ matrix.arch }}.dmg"
- uses: actions/upload-artifact@v4
with:
name: Tomviz-${{ needs.check-version.outputs.tomviz_version }}-${{ matrix.arch }}.dmg
path: packaging/Tomviz-*-${{ matrix.arch }}.dmg
build-windows:
needs: check-version
if: needs.check-version.outputs.should_build == 'true'
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
# Add dumpbin (from MSVC) to PATH so verify.py can check DLL deps.
# Uses Microsoft's vswhere (first-party, pre-installed on the runner) to
# locate the latest MSVC toolset; no third-party action involved.
- name: Add MSVC dumpbin to PATH
shell: bash
run: |
VSWHERE="C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe"
VS_PATH=$("$VSWHERE" -latest -property installationPath)
MSVC_VER=$(ls "$VS_PATH/VC/Tools/MSVC" | sort -V | tail -1)
DUMPBIN_DIR="$VS_PATH/VC/Tools/MSVC/$MSVC_VER/bin/Hostx64/x64"
if [ ! -x "$DUMPBIN_DIR/dumpbin.exe" ]; then
echo "FAIL: dumpbin not found at $DUMPBIN_DIR"
exit 1
fi
echo "Adding to PATH: $DUMPBIN_DIR"
echo "$DUMPBIN_DIR" >> "$GITHUB_PATH"
- uses: conda-incubator/setup-miniconda@v3
with:
miniforge-version: latest
environment-file: packaging/environment.yml
conda-remove-defaults: 'true'
- name: Build package
shell: bash -el {0}
env:
TOMVIZ_VERSION: ${{ needs.check-version.outputs.tomviz_version }}
TOMVIZ_PYTHON_VERSION: ${{ env.PYTHON_VERSION }}
run: |
cd packaging
python package.py --tomviz-version "$TOMVIZ_VERSION" --python-version "$TOMVIZ_PYTHON_VERSION"
cpack --config CPackConfig.cmake -G ZIP
cpack --config CPackConfig.cmake -G WIX || {
echo "=== WIX log ==="
cat _CPack_Packages/WIX/wix.log 2>/dev/null || echo "wix.log not found"
exit 1
}
- name: Verify package (structural)
shell: bash -el {0}
env:
TOMVIZ_PYTHON_VERSION: ${{ env.PYTHON_VERSION }}
run: |
cd packaging
python verify.py _build/install/tomviz --python-version "$TOMVIZ_PYTHON_VERSION"
- name: Verify Python imports
shell: bash
run: |
ENV_DIR="packaging/_build/install/tomviz/env"
export PATH="$ENV_DIR/Library/bin:$ENV_DIR/Scripts:$ENV_DIR:$PATH"
echo "=== Testing Python imports ==="
"$ENV_DIR/python.exe" .github/scripts/verify_imports.py
- name: Verify Qt plugins
shell: bash
run: |
ENV_DIR="packaging/_build/install/tomviz/env"
echo "=== Checking Qt plugins ==="
# Qt plugins can be in different locations depending on the conda layout
for PLUGIN_DIR in "$ENV_DIR/Library/plugins" "$ENV_DIR/Library/lib/qt6/plugins"; do
if [ -d "$PLUGIN_DIR/platforms" ]; then
echo "OK: Qt platforms plugin dir exists at $PLUGIN_DIR/platforms"
ls "$PLUGIN_DIR/platforms/"
exit 0
fi
done
echo "FAIL: Qt platforms plugin dir not found"
exit 1
- name: Verify MSI
shell: bash
run: |
MSI=$(ls packaging/Tomviz-*.msi 2>/dev/null | head -1)
if [ -z "$MSI" ]; then
echo "FAIL: No MSI found"
exit 1
fi
echo "=== Validating MSI: $MSI ==="
# Check it's a valid OLE compound file (MSI magic bytes)
MAGIC=$(xxd -l 8 "$MSI" | head -1)
if echo "$MAGIC" | grep -q "d0cf 11e0"; then
echo "OK: Valid MSI file (OLE compound document)"
else
echo "FAIL: Not a valid MSI file"
exit 1
fi
# Query MSI properties using PowerShell
powershell.exe -Command "
\$db = (New-Object -ComObject WindowsInstaller.Installer).OpenDatabase('$(cygpath -w "$MSI")', 0)
\$view = \$db.OpenView(\"SELECT Property, Value FROM Property WHERE Property='ProductName' OR Property='ProductVersion' OR Property='Manufacturer'\")
\$view.Execute()
while (\$record = \$view.Fetch()) {
Write-Host \" \$(\$record.StringData(1)): \$(\$record.StringData(2))\"
}
"
echo "OK: MSI properties verified"
- name: Verify package (smoke test via extracted ZIP)
shell: bash
env:
QT_QPA_PLATFORM: offscreen
run: |
# We previously ran `msiexec /i ... /qn` here to test the actual MSI
# install path, but it hung indefinitely on the runner (no log
# output, timed out at the workflow limit). Modern Windows Defender
# Tamper Protection also blocks programmatic disable, so we couldn't
# rule out AV interference. Falling back to a ZIP-extraction smoke
# test, which uses the same install layout. The MSI itself is still
# structurally validated by the "Verify MSI" step above.
ZIP=$(ls packaging/Tomviz-*.zip | head -1)
EXTRACT="$RUNNER_TEMP/tomviz-extracted"
rm -rf "$EXTRACT"
mkdir -p "$EXTRACT"
echo "Extracting $ZIP to $EXTRACT ..."
unzip -q "$ZIP" -d "$EXTRACT"
# CPack ZIP wraps everything in Tomviz-${VERSION}/; find tomviz.bat.
LAUNCHER=$(find "$EXTRACT" -name tomviz.bat -type f | head -1)
if [ -z "$LAUNCHER" ] || [ ! -f "$LAUNCHER" ]; then
echo "FAIL: tomviz.bat not found in extracted ZIP"
find "$EXTRACT" -maxdepth 3 -type f | head -20
exit 1
fi
echo "Launcher: $LAUNCHER"
bash .github/scripts/smoke_test_launcher.sh "$LAUNCHER"
- name: Check artifact sizes
shell: bash
run: |
bash .github/scripts/check_artifact_size.sh "packaging/Tomviz-*.zip"
bash .github/scripts/check_artifact_size.sh "packaging/Tomviz-*.msi"
- uses: actions/upload-artifact@v4
with:
name: Tomviz-${{ needs.check-version.outputs.tomviz_version }}.zip
path: packaging/Tomviz-*.zip
- uses: actions/upload-artifact@v4
with:
name: Tomviz-${{ needs.check-version.outputs.tomviz_version }}.msi
path: packaging/Tomviz-*.msi
build-linux:
needs: check-version
if: needs.check-version.outputs.should_build == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: conda-incubator/setup-miniconda@v3
with:
miniforge-version: latest
environment-file: packaging/environment.yml
conda-remove-defaults: 'true'
- name: Build package
shell: bash -el {0}
env:
TOMVIZ_VERSION: ${{ needs.check-version.outputs.tomviz_version }}
TOMVIZ_PYTHON_VERSION: ${{ env.PYTHON_VERSION }}
run: |
cd packaging
python package.py --tomviz-version "$TOMVIZ_VERSION" --python-version "$TOMVIZ_PYTHON_VERSION"
cpack --config CPackConfig.cmake
- name: Verify package (structural)
shell: bash -el {0}
env:
TOMVIZ_PYTHON_VERSION: ${{ env.PYTHON_VERSION }}
run: |
cd packaging
python verify.py _build/install/tomviz --python-version "$TOMVIZ_PYTHON_VERSION"
- name: Verify Python imports
run: |
ENV_DIR="packaging/_build/install/tomviz/env"
echo "=== Testing Python imports ==="
"$ENV_DIR/bin/python" .github/scripts/verify_imports.py
- name: Verify package (smoke test via extracted tar.gz)
env:
LIBGL_ALWAYS_SOFTWARE: "1"
run: |
sudo apt-get update -qq && sudo apt-get install -y -qq xvfb libegl1 libgl1-mesa-dri libglx-mesa0 >/dev/null 2>&1
TGZ=$(ls packaging/Tomviz-*.tar.gz | head -1)
EXTRACT="$RUNNER_TEMP/tomviz-extracted"
rm -rf "$EXTRACT"
mkdir -p "$EXTRACT"
echo "Extracting $TGZ to $EXTRACT ..."
tar -xzf "$TGZ" -C "$EXTRACT" --strip-components=1
LAUNCHER="$EXTRACT/tomviz/tomviz"
if [ ! -x "$LAUNCHER" ]; then
echo "FAIL: launcher not executable at $LAUNCHER"
ls -la "$EXTRACT/tomviz/" || true
exit 1
fi
echo "OK: launcher is executable at $LAUNCHER"
xvfb-run -a bash .github/scripts/smoke_test_launcher.sh "$LAUNCHER"
- name: Check artifact size
run: bash .github/scripts/check_artifact_size.sh "packaging/Tomviz-*.tar.gz"
- uses: actions/upload-artifact@v4
with:
name: Tomviz-${{ needs.check-version.outputs.tomviz_version }}.tar.gz
path: packaging/Tomviz-*.tar.gz
build-linux-rpm:
needs: check-version
if: needs.check-version.outputs.should_build == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: conda-incubator/setup-miniconda@v3
with:
miniforge-version: latest
environment-file: packaging/environment.yml
conda-remove-defaults: 'true'
- name: Build bundle
shell: bash -el {0}
env:
TOMVIZ_VERSION: ${{ needs.check-version.outputs.tomviz_version }}
TOMVIZ_PYTHON_VERSION: ${{ env.PYTHON_VERSION }}
run: |
cd packaging
python package.py --tomviz-version "$TOMVIZ_VERSION" --python-version "$TOMVIZ_PYTHON_VERSION"
- name: Verify bundle (structural)
shell: bash -el {0}
env:
TOMVIZ_PYTHON_VERSION: ${{ env.PYTHON_VERSION }}
run: |
cd packaging
python verify.py _build/install/tomviz --python-version "$TOMVIZ_PYTHON_VERSION"
- name: Build RPM
shell: bash -el {0}
env:
TOMVIZ_VERSION: ${{ needs.check-version.outputs.tomviz_version }}
run: |
sudo apt-get update -qq && sudo apt-get install -y -qq rpm
cd packaging
bash linux/build_rpm.sh --staged _build/install --version "$TOMVIZ_VERSION" --out _build
- name: Install + smoke test on RHEL 8 and RHEL 9
env:
TOMVIZ_PYTHON_VERSION: ${{ env.PYTHON_VERSION }}
run: |
cd packaging
RPM=$(cd _build && ls tomviz-*.rpm | head -1)
for img in rockylinux:8 rockylinux:9; do
echo "===================== Testing on $img ====================="
docker run --rm -v "$GITHUB_WORKSPACE:/src" -w /src/packaging "$img" \
bash -c "bash linux/test_rpm.sh \"/src/packaging/_build/$RPM\" \"/src/packaging/verify.py\" \"$TOMVIZ_PYTHON_VERSION\""
done
- name: Check artifact size
run: bash .github/scripts/check_artifact_size.sh "packaging/_build/tomviz-*.rpm" 200
- name: Rename artifact
run: |
mv packaging/_build/tomviz-*.rpm \
"packaging/Tomviz-${{ needs.check-version.outputs.tomviz_version }}.x86_64.rpm"
- uses: actions/upload-artifact@v4
with:
name: Tomviz-${{ needs.check-version.outputs.tomviz_version }}.rpm
path: packaging/Tomviz-*.rpm
build-flatpak:
needs: check-version
if: needs.check-version.outputs.should_build == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: conda-incubator/setup-miniconda@v3
with:
miniforge-version: latest
environment-file: packaging/environment.yml
conda-remove-defaults: 'true'
- name: Build bundle
shell: bash -el {0}
env:
TOMVIZ_VERSION: ${{ needs.check-version.outputs.tomviz_version }}
TOMVIZ_PYTHON_VERSION: ${{ env.PYTHON_VERSION }}
run: |
cd packaging
python package.py --tomviz-version "$TOMVIZ_VERSION" --python-version "$TOMVIZ_PYTHON_VERSION"
- name: Install flatpak tooling
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq flatpak flatpak-builder xvfb \
mesa-utils libgl1-mesa-dri
flatpak remote-add --user --if-not-exists flathub \
https://flathub.org/repo/flathub.flatpakrepo
- name: Build flatpak
env:
TOMVIZ_VERSION: ${{ needs.check-version.outputs.tomviz_version }}
run: |
cd packaging
bash flatpak/build_flatpak.sh --staged _build/install --version "$TOMVIZ_VERSION" --out _build
- name: Smoke test flatpak (xvfb + software GL)
env:
TOMVIZ_VERSION: ${{ needs.check-version.outputs.tomviz_version }}
run: |
cd packaging
bash flatpak/test_flatpak.sh "_build/org.tomviz.Tomviz-${TOMVIZ_VERSION}.flatpak"
- name: Check artifact size
run: bash .github/scripts/check_artifact_size.sh "packaging/_build/org.tomviz.Tomviz-*.flatpak" 200
- name: Rename artifact
run: |
mv packaging/_build/org.tomviz.Tomviz-*.flatpak \
"packaging/Tomviz-${{ needs.check-version.outputs.tomviz_version }}.flatpak"
- uses: actions/upload-artifact@v4
with:
name: Tomviz-${{ needs.check-version.outputs.tomviz_version }}.flatpak
path: packaging/Tomviz-*.flatpak
create-release:
needs: [check-version, build-macos, build-windows, build-linux, build-linux-rpm, build-flatpak]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
path: artifacts
- name: Collect release files
run: |
mkdir -p release
# Files are already named correctly from each build job
find artifacts -type f \( -name "*.dmg" -o -name "*.zip" -o -name "*.msi" -o -name "*.tar.gz" -o -name "*.rpm" -o -name "*.flatpak" \) -exec cp {} release/ \;
ls -la release/
- name: Create release on this repo
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ needs.check-version.outputs.tomviz_version }}
BUILD_STRING: ${{ needs.check-version.outputs.build_string }}
run: |
set -euo pipefail
REPO="${{ github.repository }}"
TAG="v${VERSION}"
NOTES="Standalone installers for Tomviz ${VERSION}.
**macOS DMGs and Windows MSI are unsigned.** Download and sign/notarize internally before distribution.
Built from conda-forge packages with Python ${{ env.PYTHON_VERSION }}.
conda-forge build: ${BUILD_STRING}"
# Remove any existing release for this tag (published or draft) so we start
# clean. Reusing the tag is intentional. Only tolerate "release not found";
# any other error (e.g. a 401) must fail the job instead of being masked,
# otherwise a stale draft silently lingers and is never published.
if gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then
gh release delete "$TAG" --repo "$REPO" --cleanup-tag -y
fi
# Create the release as a draft with no assets first. This is a small, fast
# call, separate from the multi-GB upload that intermittently 401s.
gh release create "$TAG" \
--repo "$REPO" \
--title "Tomviz ${VERSION}" \
--notes "$NOTES" \
--draft
# Upload the large assets separately, retrying on the transient HTTP 401 /
# network failures that have aborted the upload mid-flight. --clobber lets a
# retry overwrite assets that a previous attempt partially uploaded.
for attempt in 1 2 3; do
if gh release upload "$TAG" release/* --repo "$REPO" --clobber; then
break
fi
if [ "$attempt" -eq 3 ]; then
echo "Asset upload failed after $attempt attempts" >&2
exit 1
fi
echo "Asset upload attempt $attempt failed; retrying in 30s..." >&2
sleep 30
done
# Publish only after every asset is in place, so a failed upload never
# leaves a half-populated published release.
gh release edit "$TAG" --repo "$REPO" --draft=false