Skip to content

Merge branch 'feature/multi-lasers': multi-DAC laser output with per-… #10

Merge branch 'feature/multi-lasers': multi-DAC laser output with per-…

Merge branch 'feature/multi-lasers': multi-DAC laser output with per-… #10

Workflow file for this run

name: Release
# Builds standalone packages for macOS and Windows (each serves the built
# frontend itself - see DEVELOPER.md's "Standalone / packaged builds"
# section) and publishes both as assets on a single GitHub Release whenever
# something is pushed to main.
#
# - macOS: a BeamCommander3.app bundle + .dmg (drag-to-Applications), built
# with clang++ via backend/build.sh, on macos-14 (Apple Silicon/arm64).
# - Windows: a portable .zip (laser_daemon.exe + assets + a run.bat
# launcher), built with MSVC via CMake (backend/CMakeLists.txt - only used
# for this job, macOS keeps using build.sh), on windows-latest (x64).
on:
push:
branches: [main]
workflow_dispatch: {}
permissions:
contents: write
concurrency:
group: release
cancel-in-progress: false
jobs:
version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.v.outputs.version }}
steps:
- id: v
run: echo "version=v$(date -u +%Y.%m.%d)-${GITHUB_RUN_NUMBER}" >> "$GITHUB_OUTPUT"
build-macos:
needs: version
runs-on: macos-14
env:
LIBERA_DIR: /tmp/libera-laser
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install build prerequisites
run: brew install cmake ninja libusb pkg-config
- name: Resolve libera-laser commit SHA (for cache key)
id: libera-sha
run: |
sha="$(git ls-remote https://github.com/sebleedelisle/libera-laser.git HEAD | cut -f1)"
echo "sha=$sha" >> "$GITHUB_OUTPUT"
- name: Cache libera-laser build
id: libera-cache
uses: actions/cache@v4
with:
path: ${{ env.LIBERA_DIR }}
key: libera-laser-macos-14-${{ steps.libera-sha.outputs.sha }}
- name: Build libera-laser
if: steps.libera-cache.outputs.cache-hit != 'true'
run: |
rm -rf "$LIBERA_DIR"
git clone --depth=1 https://github.com/sebleedelisle/libera-laser.git "$LIBERA_DIR"
cd "$LIBERA_DIR"
git submodule update --init --recursive
cmake --preset release \
-DLIBERA_BUILD_EXAMPLES=OFF \
-DLIBERA_BUILD_TESTS=OFF \
-DLIBERA_BUILD_LASER_TOOL=OFF
cmake --build --preset release
- name: Build backend (laser_daemon)
run: cd backend && bash build.sh
- name: Build frontend
run: |
cd frontend
npm ci
npm run build
- name: Assemble release package (.app + .dmg)
id: package
run: |
version="${{ needs.version.outputs.version }}"
name="beamcommander3-macos-arm64-${version}"
app="/tmp/BeamCommander3.app"
rm -rf "$app"
mkdir -p "$app/Contents/MacOS" "$app/Contents/Frameworks" "$app/Contents/Resources"
sed "s/__VERSION__/${version}/g" packaging/Info.plist.template > "$app/Contents/Info.plist"
cp packaging/launcher.sh "$app/Contents/MacOS/BeamCommander3"
cp packaging/AppIcon.icns "$app/Contents/Resources/AppIcon.icns"
cp backend/laser_daemon "$app/Contents/MacOS/laser_daemon"
cp backend/midi_map.json "$app/Contents/Resources/"
cp -r frontend/dist "$app/Contents/Resources/frontend_dist"
chmod +x "$app/Contents/MacOS/BeamCommander3" "$app/Contents/MacOS/laser_daemon"
# laser_daemon dynamically links Homebrew's libusb, which only
# exists on the runner (e.g. /opt/homebrew/opt/libusb/...) - bundle
# the dylib into Contents/Frameworks (standard app-bundle layout)
# and repoint the load command at it via @executable_path, so the
# release doesn't require Homebrew or libusb pre-installed.
libusb_path="$(otool -L "$app/Contents/MacOS/laser_daemon" | awk '/libusb.*\.dylib/{print $1; exit}')"
if [[ -n "$libusb_path" ]]; then
cp "$libusb_path" "$app/Contents/Frameworks/libusb-1.0.0.dylib"
chmod 644 "$app/Contents/Frameworks/libusb-1.0.0.dylib"
install_name_tool -change "$libusb_path" "@executable_path/../Frameworks/libusb-1.0.0.dylib" "$app/Contents/MacOS/laser_daemon"
fi
# Ad-hoc sign the whole bundle (recursively - covers the bundled
# dylib and laser_daemon too). Re-signing the copied dylib is what
# actually matters here: Homebrew's original signature doesn't
# survive being re-hosted inside our (differently-identified) app
# bundle once quarantined after download, which macOS's library
# validation policy then refuses to load ("library load disallowed
# by system policy") unless every nested binary is re-signed
# consistently with the app itself.
codesign --deep --force -s - "$app"
codesign --verify --deep --strict "$app"
# Package as a .dmg with a drag-to-Applications shortcut, like a
# normal downloadable Mac app - not just a bare zip of files.
dmg_stage="/tmp/dmg-stage"
rm -rf "$dmg_stage"; mkdir -p "$dmg_stage"
cp -r "$app" "$dmg_stage/"
ln -s /Applications "$dmg_stage/Applications"
dmg_path="/tmp/${name}.dmg"
hdiutil create -volname "BeamCommander3" -srcfolder "$dmg_stage" -ov -format UDZO "$dmg_path"
echo "dmg_path=$dmg_path" >> "$GITHUB_OUTPUT"
- name: Upload macOS artifact
uses: actions/upload-artifact@v4
with:
name: macos-release
path: ${{ steps.package.outputs.dmg_path }}
build-windows:
needs: version
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install Ninja
run: choco install ninja -y
- name: Set up MSVC dev environment
uses: ilammy/msvc-dev-cmd@v1
- name: Resolve libera-laser commit SHA (for cache key)
id: libera-sha
run: |
$line = git ls-remote https://github.com/sebleedelisle/libera-laser.git HEAD
$sha = ($line -split "\s+")[0]
"sha=$sha" >> $env:GITHUB_OUTPUT
- name: Cache libera-laser checkout
id: libera-cache
uses: actions/cache@v4
with:
path: C:/libera-laser
key: libera-laser-windows-${{ steps.libera-sha.outputs.sha }}
- name: Clone libera-laser
if: steps.libera-cache.outputs.cache-hit != 'true'
run: |
Remove-Item -Recurse -Force C:/libera-laser -ErrorAction SilentlyContinue
git clone --depth=1 https://github.com/sebleedelisle/libera-laser.git C:/libera-laser
cd C:/libera-laser
git submodule update --init --recursive
- name: Configure backend (CMake + Ninja)
run: cmake -S backend -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DLIBERA_DIR=C:/libera-laser
- name: Build backend (laser_daemon.exe)
run: cmake --build build
- name: Build frontend
run: |
cd frontend
npm ci
npm run build
- name: Assemble release package (installer)
id: package
run: |
$version = "${{ needs.version.outputs.version }}"
$stage = Join-Path $env:RUNNER_TEMP "win_stage"
Remove-Item -Recurse -Force $stage -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $stage | Out-Null
Copy-Item build/laser_daemon.exe $stage/
Copy-Item backend/midi_map.json $stage/
Copy-Item -Recurse frontend/dist $stage/frontend_dist
Copy-Item packaging/run_windows.bat $stage/run.bat
# laser_daemon.exe dynamically loads libusb, bundled as a prebuilt
# DLL in libera-laser's vendored helios_dac submodule (there's no
# Windows equivalent of Homebrew to install it system-wide) - ship
# it alongside the exe so the release is standalone.
$dllDir = "C:/libera-laser/libs/helios_dac/sdk/cpp/libusb_bin/Windows/x64/Release/dll"
Copy-Item "$dllDir/*.dll" $stage/
# Build BeamCommander3-Setup.exe with Inno Setup (preinstalled on
# windows-latest runners) instead of shipping a bare zip - gives
# users a normal installer (Start Menu shortcut, optional desktop
# shortcut, proper uninstaller) instead of a portable folder.
$iscc = "C:/Program Files (x86)/Inno Setup 6/ISCC.exe"
$iconFile = Join-Path $env:GITHUB_WORKSPACE "packaging/AppIcon.ico"
& $iscc /DMyAppVersion=$version /DStageDir=$stage /DIconFile=$iconFile packaging/windows_installer.iss
if ($LASTEXITCODE -ne 0) { throw "ISCC failed with exit code $LASTEXITCODE" }
$setupPath = Join-Path $env:GITHUB_WORKSPACE "packaging/BeamCommander3-Setup.exe"
"setup_path=$setupPath" >> $env:GITHUB_OUTPUT
- name: Upload Windows artifact
uses: actions/upload-artifact@v4
with:
name: windows-release
path: ${{ steps.package.outputs.setup_path }}
release:
needs: [version, build-macos, build-windows]
runs-on: ubuntu-latest
steps:
- name: Download macOS artifact
uses: actions/download-artifact@v4
with:
name: macos-release
path: artifacts/macos
- name: Download Windows artifact
uses: actions/download-artifact@v4
with:
name: windows-release
path: artifacts/windows
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.version.outputs.version }}
name: ${{ needs.version.outputs.version }}
body: |
Automated build from `${{ github.sha }}`.
**macOS (Apple Silicon)**: download the `.dmg`, drag
**BeamCommander3** into **Applications**, then open it from
there - it starts the backend and opens http://localhost:8000 in
your browser automatically. Not notarized (no Apple Developer
ID), so on first launch macOS will refuse to open it as "from an
unidentified developer" - right-click the app in Applications
and choose **Open** once to approve it (only needed the first
time).
**Windows (x64)**: download and run `BeamCommander3-Setup.exe`.
It's not code-signed, so SmartScreen may warn - click "More
info" > "Run anyway". Installs to your user profile (no admin
needed), adds Start Menu/optional desktop shortcuts, and offers
to launch the app right after install - it starts
`laser_daemon.exe` and opens http://localhost:8000 automatically.
Both packages bundle the built frontend and all runtime
dependencies - no Node.js, Xcode, or Visual Studio required to
run them.
files: |
artifacts/macos/*.dmg
artifacts/windows/*.exe