diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index e58031d75..ca06aa2b0 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -120,10 +120,9 @@ jobs: - name: Package run: | set -euo pipefail - VERSION="${GITHUB_REF_NAME#v}" - if [ "$VERSION" = "$GITHUB_REF_NAME" ]; then - VERSION="$(cargo pkgid | sed 's/.*#//' | cut -d@ -f2-)+git" - fi + # The release version lives in [workspace.package] of the root + # Cargo.toml (single source of truth; tags do not carry it). + VERSION=$(sed -n '/^\[workspace\.package\]/,/^\[/s/^version = "\(.*\)"/\1/p' Cargo.toml | head -1) case "${{ matrix.distro }}" in debian) tooling/package/build-deb.sh "$VERSION" ;; fedora) tooling/package/build-rpm.sh "$VERSION" ;; @@ -274,6 +273,12 @@ jobs: - name: Package .app bundle run: cargo packager --release --formats app + # Pull the Homebrew dylibs the binaries reference into + # Contents/Frameworks and rewrite install names to + # @executable_path-relative (the script ad-hoc re-signs the bundle). + - name: Bundle dylibs into the .app + run: tooling/package/bundle-dylibs-macos.sh target/release/Oak.app + - name: Create DMG run: | rm -rf dmg-staging @@ -390,6 +395,17 @@ jobs: unset INCLUDE LIB cargo build --release --locked + # Collect the MSYS2 runtime DLLs (libstdc++/libgcc/OpenColorIO/...) + # into target/pkg/win-dlls; the packager `resources` glob then + # installs them next to the executables. + - name: Bundle runtime DLLs + run: | + unset INCLUDE LIB + pacman -S --needed --noconfirm mingw-w64-ucrt-x86_64-ntldd + tooling/package/bundle-dylibs-windows.sh target/pkg/win-dlls \ + target/release/oak-editor.exe target/release/oak-cli.exe \ + target/release/oak-worker.exe + - name: Package (NSIS) run: cargo packager --release --formats nsis diff --git a/Cargo.toml b/Cargo.toml index a064b5ee5..618049c02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,11 @@ exclude = ["gpui", "crates/oakengine.bk"] default-members = [".", "crates/oak-cli", "crates/oak-worker"] resolver = "2" +[workspace.package] +# Single source of truth for the release version: the root `oak` package +# inherits it, and with it cargo-packager and the CD packaging scripts. +version = "0.5.0" + [profile.release] # FFI discipline: every module crate exports an `extern "C"` ABI whose # entry points must never unwind/abort across the boundary; panics are @@ -51,7 +56,7 @@ panic = "unwind" [package] name = "oak" -version = "0.5.0" +version.workspace = true edition = "2021" description = "Oak Video Editor" license = "GPL-3.0-or-later" @@ -156,7 +161,14 @@ category = "Video" icons = ["icons/icon.png"] # The UI string tables (language packs); users can drop extra # .yaml files into the installed i18n directory. -resources = ["assets/i18n"] +# The Windows DLL staging dir is filled by +# tooling/package/bundle-dylibs-windows.sh before `cargo packager` runs +# (NSIS/WiX place resources next to the executable, which is exactly +# where Windows resolves DLLs from). Empty locally = no bundled DLLs. +resources = [ + "assets/i18n", + { src = "target/pkg/win-dlls/*.dll", target = "." }, +] binaries = [ { path = "oak-editor", main = true }, { path = "oak-cli", main = false }, diff --git a/tooling/package/bundle-dylibs-macos.sh b/tooling/package/bundle-dylibs-macos.sh new file mode 100755 index 000000000..84098fbaa --- /dev/null +++ b/tooling/package/bundle-dylibs-macos.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# Oak Video Editor - Non-Linear Video Editor +# Copyright (C) 2026 Oak Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Bundle every non-system dynamic library an .app depends on into +# Contents/Frameworks and rewrite the install names to +# @executable_path/../Frameworks, so the app runs on machines without +# Homebrew. (FFmpeg/OCIO are already static; what remains are the +# Homebrew codec/filter dylibs the static FFmpeg references.) +# +# System libraries (/usr/lib, /System) are never copied. The set is +# computed to a fixpoint: freshly copied dylibs can themselves reference +# further non-system dylibs. @executable_path resolves to Contents/MacOS +# even when the reference lives in a Frameworks dylib, so one rewrite +# pattern fits both the executables and the libraries. +# +# Every modified Mach-O is ad-hoc re-signed afterwards: rewriting install +# names invalidates the code signature, and unsigned-but-damaged binaries +# are killed at launch on Apple Silicon. +# +# Usage: tooling/package/bundle-dylibs-macos.sh path/to/Oak.app +set -euo pipefail + +APP="${1:?usage: bundle-dylibs-macos.sh path/to/Oak.app}" +MACOS_DIR="$APP/Contents/MacOS" +FW_DIR="$APP/Contents/Frameworks" +[ -d "$MACOS_DIR" ] || { echo "$APP: no Contents/MacOS directory" >&2; exit 1; } +mkdir -p "$FW_DIR" + +is_system() { + case "$1" in + /usr/lib/*|/System/*|@*) return 0 ;; + *) return 1 ;; + esac +} + +# Rewrite every non-system reference of the Mach-O file $1 to +# @executable_path/../Frameworks, copying the target in when new. +collect() { + local file="$1" dep base + otool -L "$file" | awk 'NR>1 {print $1}' | while read -r dep; do + is_system "$dep" && continue + [ -f "$dep" ] || continue + base=$(basename "$dep") + if [ ! -f "$FW_DIR/$base" ]; then + cp -L "$dep" "$FW_DIR/$base" # -L: follow Homebrew's symlinks + chmod u+w "$FW_DIR/$base" + echo "bundled $base" + fi + install_name_tool -change "$dep" "@executable_path/../Frameworks/$base" "$file" + done +} + +# Fixpoint over the executables plus whatever the previous round copied. +prev=-1 +for round in 1 2 3 4 5; do + count=$(find "$FW_DIR" -name '*.dylib' | wc -l | tr -d ' ') + [ "$count" = "$prev" ] && break + prev=$count + for f in "$MACOS_DIR"/*; do + [ -f "$f" ] && collect "$f" + done + for f in "$FW_DIR"/*.dylib; do + [ -f "$f" ] && collect "$f" + done +done + +# Re-sign: install_name_tool invalidates the seal. Per-file signing +# instead of `codesign --deep` (deprecated and unreliable with dylibs). +find "$FW_DIR" "$MACOS_DIR" -type f -exec codesign --force --sign - {} \; +codesign --force --sign - "$APP" + +echo "bundled $prev dylibs into $FW_DIR" diff --git a/tooling/package/bundle-dylibs-windows.sh b/tooling/package/bundle-dylibs-windows.sh new file mode 100755 index 000000000..93683b558 --- /dev/null +++ b/tooling/package/bundle-dylibs-windows.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Oak Video Editor - Non-Linear Video Editor +# Copyright (C) 2026 Oak Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Collect every MSYS2/MinGW runtime DLL the given executables depend on +# into a staging directory. cargo packager's NSIS/WiX resources then +# place them next to the .exe — the first place Windows resolves DLLs +# from — so the installer works on machines without MSYS2. +# +# ntldd -R already recurses through a binary's own dependency tree; the +# outer loop reaches the fixpoint for DLLs copied in earlier rounds (a +# freshly copied DLL can itself reference further MSYS2 DLLs that the +# initial scan did not surface through the executables alone). +# C:\Windows\System32 entries are filtered out by the mingw/ucrt/clang +# path match — only the toolchain runtime lands in the package. +# +# Must run inside the MSYS2 UCRT64 shell with +# mingw-w64-ucrt-x86_64-ntldd installed. +# +# Usage: tooling/package/bundle-dylibs-windows.sh ... +set -euo pipefail + +OUT="${1:?usage: bundle-dylibs-windows.sh ...}" +shift +[ "$#" -ge 1 ] || { echo "no executables given" >&2; exit 1; } +mkdir -p "$OUT" + +if ! command -v ntldd >/dev/null; then + echo "ntldd not found; install with: pacman -S mingw-w64-ucrt-x86_64-ntldd" >&2 + exit 1 +fi + +for round in 1 2 3 4 5; do + changed=0 + # shellcheck disable=SC2046 + while IFS= read -r dep; do + [ -n "$dep" ] || continue + unix_dep=$(cygpath -u "$dep") + base=$(basename "$unix_dep") + if [ ! -f "$OUT/$base" ]; then + cp -v "$unix_dep" "$OUT/$base" + changed=1 + fi + done < <(ntldd -R "$@" $(ls "$OUT"/*.dll 2>/dev/null) 2>/dev/null \ + | grep -E 'mingw|ucrt|clang' | cut -d'>' -f2 | cut -d' ' -f2 | sort -u) + [ "$changed" = 1 ] || break +done + +echo "bundled $(ls "$OUT" | wc -l | tr -d ' ') DLLs into $OUT"