Skip to content

fix(build): 覆盖 Serato 嵌套原生模块的 macOS 合并规则 #1007

fix(build): 覆盖 Serato 嵌套原生模块的 macOS 合并规则

fix(build): 覆盖 Serato 嵌套原生模块的 macOS 合并规则 #1007

Workflow file for this run

name: Release
on:
workflow_dispatch:
push:
branches:
- main
tags:
- '*.*.*'
permissions:
contents: write
jobs:
validate_release_tag:
if: >-
${{
github.event_name == 'workflow_dispatch' ||
startsWith(github.ref, 'refs/tags/') ||
(
github.ref == 'refs/heads/main' &&
startsWith(github.event.head_commit.message, 'chore(release):')
)
}}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Validate release tag matches package version
env:
TAG_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
if [[ "${GITHUB_REF}" != refs/tags/* ]]; then
echo "No tag push; release tag validation is not required for this run."
exit 0
fi
package_version="$(node -p "require('./package.json').version")"
if [[ "$TAG_NAME" != "$package_version" ]]; then
echo "::error::Release tag '$TAG_NAME' must exactly match package.json version '$package_version'."
exit 1
fi
echo "Validated release tag: $TAG_NAME"
typecheck:
needs: [validate_release_tag]
if: >-
${{
github.event_name == 'workflow_dispatch' ||
startsWith(github.ref, 'refs/tags/') ||
(
github.ref == 'refs/heads/main' &&
startsWith(github.event.head_commit.message, 'chore(release):')
)
}}
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup PNPM
uses: pnpm/action-setup@v4
with:
version: 9.15.9
run_install: false
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 22
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile=false --prefer-offline --ignore-scripts
- name: Typecheck (vue-tsc)
run: npx vue-tsc --noEmit
build_win:
needs: [validate_release_tag]
if: >-
${{
github.event_name == 'workflow_dispatch' ||
startsWith(github.ref, 'refs/tags/') ||
(
github.ref == 'refs/heads/main' &&
startsWith(github.event.head_commit.message, 'chore(release):')
)
}}
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup PNPM
uses: pnpm/action-setup@v4
with:
version: 9.15.9
run_install: false
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 22
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Restore Demucs models cache (Windows)
uses: actions/cache@v5
with:
path: |
vendor/demucs/models
key: demucs-models-${{ runner.os }}-${{ hashFiles('scripts/demucs-model-manifest.json', 'scripts/ensure-demucs-runtime.mjs', 'scripts/lib/*.mjs') }}
restore-keys: |
demucs-models-${{ runner.os }}-
- name: Restore Rekordbox runtime cache (Windows)
uses: actions/cache@v5
with:
path: |
vendor/rekordbox-desktop-runtime/win32-x64/python
key: rekordbox-runtime-${{ runner.os }}-${{ hashFiles('scripts/ensure-rekordbox-desktop-runtime.mjs', 'scripts/rekordbox-desktop-runtime-requirements.txt', 'scripts/lib/demucs-standalone-python.mjs', 'scripts/lib/demucs-runtime-support.mjs') }}
restore-keys: |
rekordbox-runtime-${{ runner.os }}-
- name: Setup Python (for node-gyp)
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Ensure setuptools available
run: |
python -m pip install --upgrade pip setuptools wheel
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- name: Restore Cargo cache (Windows)
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
rust_package/target
key: cargo-${{ runner.os }}-x86_64-pc-windows-msvc-${{ hashFiles('rust_package/Cargo.lock', 'rust_package/Cargo.toml', 'rust_package/build.rs', 'rust_package/src/**', 'rust_package/native/qm/**', 'rust_package/native/soundtouch/**', 'rust_package/native/chromaprint/**', 'rust_package/native/ffmpeg/frkb_ffmpeg_decode_wrapper.*') }}
restore-keys: |
cargo-${{ runner.os }}-x86_64-pc-windows-msvc-
- name: Restore Electron builder cache (Windows)
uses: actions/cache@v5
with:
path: |
~/AppData/Local/electron/Cache
~/AppData/Local/electron-builder/Cache
key: electron-builder-${{ runner.os }}-${{ hashFiles('package.json', 'pnpm-lock.yaml') }}
restore-keys: |
electron-builder-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile=false --prefer-offline
- name: Ensure Demucs models (Windows)
run: node scripts/ensure-demucs-runtime.mjs --models-only --ci
- name: Ensure Rekordbox Desktop runtime (Windows)
run: pnpm run rekordbox:runtime:ensure
- name: Validate Rekordbox Desktop runtime (Windows)
run: pnpm run rekordbox:runtime:validate -- --platform-key win32-x64
- name: Prepare FFmpeg (Windows)
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
New-Item -ItemType Directory -Force -Path vendor/ffmpeg/win32-x64 | Out-Null
$headers = @{
'User-Agent' = 'TrackStudio-Release'
'Authorization' = "Bearer $env:GH_TOKEN"
'X-GitHub-Api-Version' = '2022-11-28'
}
$release = Invoke-RestMethod -Uri 'https://api.github.com/repos/BtbN/FFmpeg-Builds/releases/latest' -Headers $headers
$asset =
$release.assets |
Where-Object { $_.name -match '^ffmpeg-N-.*-win64-gpl\.zip$' } |
Select-Object -First 1
if (-not $asset) {
$asset =
$release.assets |
Where-Object { $_.name -match '^ffmpeg-.*-win64-gpl\.zip$' } |
Select-Object -First 1
}
if (-not $asset) {
throw 'win64 gpl ffmpeg asset not found in latest BtbN release.'
}
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile ffmpeg.zip
Expand-Archive -Path ffmpeg.zip -DestinationPath ffmpeg_unzip -Force
$ff = (Get-ChildItem -Recurse -Filter ffmpeg.exe ffmpeg_unzip | Select-Object -First 1).FullName
$ffprobe = (Get-ChildItem -Recurse -Filter ffprobe.exe ffmpeg_unzip | Select-Object -First 1).FullName
if (-not $ff) {
throw "ffmpeg.exe not found in downloaded archive."
}
if (-not $ffprobe) {
throw "ffprobe.exe not found in downloaded archive."
}
Copy-Item -Path $ff -Destination vendor/ffmpeg/win32-x64/ffmpeg.exe -Force
Copy-Item -Path $ffprobe -Destination vendor/ffmpeg/win32-x64/ffprobe.exe -Force
Remove-Item ffmpeg.zip -Force
Remove-Item ffmpeg_unzip -Recurse -Force
- name: Prepare FFmpeg native dependencies (Windows)
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$headers = @{
'User-Agent' = 'TrackStudio-Release'
'Authorization' = "Bearer $env:GH_TOKEN"
'X-GitHub-Api-Version' = '2022-11-28'
}
$release = Invoke-RestMethod -Uri 'https://api.github.com/repos/BtbN/FFmpeg-Builds/releases/latest' -Headers $headers
$asset =
$release.assets |
Where-Object { $_.name -match '^ffmpeg-N-.*-win64-gpl-shared.*\.zip$' } |
Select-Object -First 1
if (-not $asset) {
$asset =
$release.assets |
Where-Object { $_.name -match '^ffmpeg-.*-win64-gpl-shared.*\.zip$' } |
Select-Object -First 1
}
if (-not $asset) {
throw 'win64 gpl shared ffmpeg asset not found in latest BtbN release.'
}
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile ffmpeg-shared.zip
Expand-Archive -Path ffmpeg-shared.zip -DestinationPath ffmpeg_shared_unzip -Force
$binDir = Get-ChildItem -Recurse -Directory ffmpeg_shared_unzip | Where-Object { $_.Name -eq 'bin' } | Select-Object -First 1
if (-not $binDir) {
throw "bin directory not found in shared ffmpeg archive."
}
$sharedRoot = $binDir.Parent.FullName
$includeDir = Join-Path $sharedRoot 'include'
$libDir = Join-Path $sharedRoot 'lib'
if (-not (Test-Path $includeDir)) {
throw "include directory not found in shared ffmpeg archive."
}
if (-not (Test-Path $libDir)) {
throw "lib directory not found in shared ffmpeg archive."
}
New-Item -ItemType Directory -Force -Path vendor/ffmpeg/win32-x64/dll | Out-Null
Get-ChildItem -Path $binDir.FullName -Filter "*.dll" | ForEach-Object {
Copy-Item -Path $_.FullName -Destination "vendor/ffmpeg/win32-x64/dll/$($_.Name)" -Force
}
$nativeFfmpegDir = 'rust_package/native/ffmpeg/win32-x64'
New-Item -ItemType Directory -Force -Path "$nativeFfmpegDir/include" | Out-Null
New-Item -ItemType Directory -Force -Path "$nativeFfmpegDir/lib" | Out-Null
foreach ($dirName in @('libavcodec', 'libavformat', 'libavutil', 'libswresample')) {
Copy-Item -Path (Join-Path $includeDir $dirName) -Destination "$nativeFfmpegDir/include/" -Recurse -Force
}
foreach ($libName in @('avcodec.lib', 'avformat.lib', 'avutil.lib', 'swresample.lib')) {
Copy-Item -Path (Join-Path $libDir $libName) -Destination "$nativeFfmpegDir/lib/$libName" -Force
}
Remove-Item ffmpeg-shared.zip -Force
Remove-Item ffmpeg_shared_unzip -Recurse -Force
Write-Host "FFmpeg DLLs copied to vendor/ffmpeg/win32-x64/dll/"
Get-ChildItem vendor/ffmpeg/win32-x64/dll/ | ForEach-Object { Write-Host " $($_.Name) ($([math]::Round($_.Length/1MB,1))MB)" }
Write-Host "FFmpeg native headers/libs copied to $nativeFfmpegDir"
- name: Validate bundled media tools (Windows)
run: node scripts/validate-bundled-media-tools.mjs --mode package --platform-key win32-x64
- name: Build Rust N-API (rust_package)
working-directory: rust_package
run: npx --yes @napi-rs/cli@2.18.4 build --platform --release --target x86_64-pc-windows-msvc
- name: Build (vite)
env:
CLOUD_SYNC_BASE_URL_PROD: ${{ secrets.CLOUD_SYNC_BASE_URL_PROD }}
CLOUD_SYNC_API_SECRET_KEY: ${{ secrets.CLOUD_SYNC_API_SECRET_KEY }}
FRKB_LASTFM_API_KEY: ${{ secrets.FRKB_LASTFM_API_KEY }}
run: pnpm build
- name: Build Windows installer frontend
run: pnpm installer-ui:build
- name: Package (electron-builder, no publish)
run: npx electron-builder --win --x64 --publish never
- name: Verify packaged native module (Windows)
shell: pwsh
run: |
$resourcesRoot = "dist/win-unpacked/resources"
$dllDir = Join-Path $resourcesRoot "ffmpeg/win32-x64/dll"
$nativeModule = "dist/win-unpacked/resources/app.asar.unpacked/node_modules/rust_package/index.win32-x64-msvc.node"
if (-not (Test-Path $dllDir)) {
throw "Packaged FFmpeg DLL directory not found: $dllDir"
}
if (-not (Test-Path $nativeModule)) {
throw "Packaged Rust native module not found: $nativeModule"
}
$env:PATH = "$dllDir;$env:PATH"
node -e "require('./dist/win-unpacked/resources/app.asar.unpacked/node_modules/rust_package'); console.log('Packaged Rust native module loaded.')"
- name: Verify packaged Demucs resources (Windows)
shell: pwsh
run: |
$resourcesRoot = "dist/win-unpacked/resources"
if (-not (Test-Path $resourcesRoot)) {
throw "Windows unpacked resources directory not found: $resourcesRoot"
}
$required = @(
"ffmpeg/win32-x64/ffmpeg.exe",
"ffmpeg/win32-x64/ffprobe.exe",
"demucs/bootstrap/mixtape_demucs_bootstrap.py",
"demucs/bootstrap/beat_this_bridge.py",
"demucs/bootstrap/rkb_official_phase_selector.py",
"demucs/models/htdemucs.yaml",
"demucs/models/955717e8-8726e21a.th",
"rekordboxDesktopLibrary/bridge.py",
"rekordboxDesktopRuntime/win32-x64/python/python.exe",
"rekordboxDesktopRuntime/win32-x64/python/Lib/site-packages/pyrekordbox/__init__.py"
)
$missing = @()
foreach ($relativePath in $required) {
$targetPath = Join-Path $resourcesRoot $relativePath
if (-not (Test-Path $targetPath)) {
$missing += $relativePath
}
}
$unexpectedRuntimeRoot = Join-Path $resourcesRoot "demucs/win32-x64"
if (Test-Path $unexpectedRuntimeRoot) {
$missing += "unexpected:demucs/win32-x64"
}
if ($missing.Count -gt 0) {
Write-Error "Missing packaged Demucs resources (Windows):"
$missing | ForEach-Object { Write-Error " - $_" }
exit 1
}
Write-Host "Packaged Demucs resources verified (Windows, runtime excluded)."
python scripts/verify_packaged_python_import_closure.py `
--source-root scripts `
--packaged-root "$resourcesRoot/demucs/bootstrap" `
--entry beat_this_bridge.py
# 不再复制 rc.yml,严格遵循 GitHub provider 读取 latest.yml / latest-mac.yml
- name: Upload dist artifacts (Windows)
uses: actions/upload-artifact@v7
if: always() && startsWith(github.ref, 'refs/tags/')
with:
name: win-artifacts
path: |
dist/*.exe
dist/*.blockmap
dist/*.yml
build_rekordbox_runtime_mac_x64:
needs: [validate_release_tag]
if: >-
${{
github.event_name == 'workflow_dispatch' ||
startsWith(github.ref, 'refs/tags/') ||
(
github.ref == 'refs/heads/main' &&
startsWith(github.event.head_commit.message, 'chore(release):')
)
}}
runs-on: macos-15-intel
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 22
- name: Restore Rekordbox runtime cache (macOS x64)
uses: actions/cache@v5
with:
path: |
vendor/rekordbox-desktop-runtime/darwin-x64/python
key: rekordbox-runtime-${{ runner.os }}-${{ runner.arch }}-darwin-x64-${{ hashFiles('scripts/ensure-rekordbox-desktop-runtime.mjs', 'scripts/rekordbox-desktop-runtime-requirements.txt', 'scripts/lib/demucs-standalone-python.mjs', 'scripts/lib/demucs-runtime-support.mjs') }}
restore-keys: |
rekordbox-runtime-${{ runner.os }}-${{ runner.arch }}-darwin-x64-
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Ensure Rekordbox Desktop runtime (macOS x64)
run: node scripts/ensure-rekordbox-desktop-runtime.mjs --platform-key darwin-x64
- name: Validate Rekordbox Desktop runtime (macOS x64)
run: node scripts/validate-rekordbox-desktop-runtime.mjs --platform-key darwin-x64
- name: Archive Rekordbox Desktop runtime (macOS x64)
run: |
set -euo pipefail
mkdir -p dist
tar -czf dist/rekordbox-runtime-darwin-x64.tar.gz \
-C vendor/rekordbox-desktop-runtime/darwin-x64 \
python
- name: Upload Rekordbox runtime artifact (macOS x64)
uses: actions/upload-artifact@v7
with:
name: rekordbox-runtime-darwin-x64
path: dist/rekordbox-runtime-darwin-x64.tar.gz
- name: Fetch Intel FFmpeg static libs
run: |
set -euo pipefail
# The hosted Intel runner ships an unrelated HashiCorp tap with a
# broken vagrant formula, which makes `brew update` emit an error.
if brew tap | grep -Fxq 'hashicorp/tap'; then
# The runner may have packer installed from this tap; force only
# removes the tap metadata and leaves unrelated formulae intact.
brew untap --force hashicorp/tap
fi
brew update
brew install ffmpeg
mkdir -p dist/ffmpeg-x64-dev/lib dist/ffmpeg-x64-dev/include
for lib in libavcodec libavformat libavutil libswresample; do
cp -L "$(brew --prefix)/lib/${lib}.a" dist/ffmpeg-x64-dev/lib/
cp -RL "$(brew --prefix)/include/${lib}" dist/ffmpeg-x64-dev/include/
done
- name: Upload Intel FFmpeg dev artifact
uses: actions/upload-artifact@v7
with:
name: ffmpeg-x64-dev
path: dist/ffmpeg-x64-dev
build_mac:
if: >-
${{
github.event_name == 'workflow_dispatch' ||
startsWith(github.ref, 'refs/tags/') ||
(
github.ref == 'refs/heads/main' &&
startsWith(github.event.head_commit.message, 'chore(release):')
)
}}
runs-on: macos-latest
needs: [validate_release_tag, build_rekordbox_runtime_mac_x64]
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup PNPM
uses: pnpm/action-setup@v4
with:
version: 9.15.9
run_install: false
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 22
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Restore Rekordbox runtime cache (macOS arm64)
uses: actions/cache@v5
with:
path: |
vendor/rekordbox-desktop-runtime/darwin-arm64/python
key: rekordbox-runtime-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('scripts/ensure-rekordbox-desktop-runtime.mjs', 'scripts/rekordbox-desktop-runtime-requirements.txt', 'scripts/lib/demucs-standalone-python.mjs', 'scripts/lib/demucs-runtime-support.mjs') }}
restore-keys: |
rekordbox-runtime-${{ runner.os }}-${{ runner.arch }}-
- name: Setup Python (for node-gyp)
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Ensure setuptools available
run: |
python -m pip install --upgrade pip setuptools wheel
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin, x86_64-apple-darwin
- name: Ensure Rust targets
run: rustup target add aarch64-apple-darwin x86_64-apple-darwin
- name: Restore Cargo cache (macOS)
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
rust_package/target
key: cargo-${{ runner.os }}-universal-${{ hashFiles('rust_package/Cargo.lock', 'rust_package/Cargo.toml', 'rust_package/build.rs', 'rust_package/src/**', 'rust_package/native/qm/**', 'rust_package/native/soundtouch/**', 'rust_package/native/chromaprint/**', 'rust_package/native/ffmpeg/frkb_ffmpeg_decode_wrapper.*') }}
restore-keys: |
cargo-${{ runner.os }}-universal-
- name: Restore Electron builder cache (macOS)
uses: actions/cache@v5
with:
path: |
~/Library/Caches/electron
~/Library/Caches/electron-builder
key: electron-builder-${{ runner.os }}-${{ hashFiles('package.json', 'pnpm-lock.yaml') }}
restore-keys: |
electron-builder-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile=false --prefer-offline
- name: Ensure Demucs models (macOS)
run: node scripts/ensure-demucs-runtime.mjs --models-only --ci
- name: Ensure Rekordbox Desktop runtime (macOS arm64)
run: pnpm run rekordbox:runtime:ensure
- name: Download Intel FFmpeg dev artifact
uses: actions/download-artifact@v8
with:
name: ffmpeg-x64-dev
path: artifacts/ffmpeg-x64-dev
- name: Download Rekordbox runtime artifact (macOS x64)
uses: actions/download-artifact@v8
with:
name: rekordbox-runtime-darwin-x64
path: artifacts/rekordbox-runtime-darwin-x64
- name: Restore Rekordbox runtime artifact (macOS x64)
run: |
set -euo pipefail
mkdir -p vendor/rekordbox-desktop-runtime/darwin-x64
tar -xzf artifacts/rekordbox-runtime-darwin-x64/rekordbox-runtime-darwin-x64.tar.gz \
-C vendor/rekordbox-desktop-runtime/darwin-x64
- name: Validate Rekordbox Desktop runtime (macOS universal)
run: pnpm run rekordbox:runtime:validate:mac-universal
- name: Prepare FFmpeg (macOS both arch)
run: |
set -euxo pipefail
mkdir -p vendor/ffmpeg/darwin-arm64 vendor/ffmpeg/darwin-x64 vendor/ffmpeg/darwin
echo "=== Host uname ==="
uname -a
sw_vers
# 使用 Homebrew 安装 arm64 版本 (native on arm64 runner)
echo "Installing arm64 ffmpeg via Homebrew"
# Keep unrelated broken taps from making Homebrew metadata updates noisy.
if brew tap | grep -Fxq 'hashicorp/tap'; then
# The runner may have packer installed from this tap; force only
# removes the tap metadata and leaves unrelated formulae intact.
brew untap --force hashicorp/tap
fi
brew update
brew install ffmpeg
ARM64_FFMPEG_PATH="$(brew --prefix)/bin/ffmpeg"
ARM64_FFPROBE_PATH="$(brew --prefix)/bin/ffprobe"
file "$ARM64_FFMPEG_PATH"
file "$ARM64_FFPROBE_PATH"
install -m 755 "$ARM64_FFMPEG_PATH" vendor/ffmpeg/darwin-arm64/ffmpeg
install -m 755 "$ARM64_FFPROBE_PATH" vendor/ffmpeg/darwin-arm64/ffprobe
download_and_verify_zip() {
local url="$1"
local output="$2"
local attempt
for attempt in 1 2 3 4 5; do
rm -f "$output"
echo "Downloading $output (attempt ${attempt}/5)"
if curl --fail --location --retry 3 --retry-all-errors --retry-delay 2 --connect-timeout 30 --max-time 600 "$url" -o "$output" \
&& unzip -tq "$output" >/dev/null; then
return 0
fi
echo "WARN: downloaded archive is missing or invalid: $output"
if [ -f "$output" ]; then
file "$output" || true
wc -c "$output" || true
fi
sleep $((attempt * 3))
done
echo "ERROR: failed to download a valid zip archive: $output"
exit 1
}
# 带 zip 校验重试下载 x64 版本,避免 GitHub 资产链路偶发返回错误页却被当成包解压。
echo "Downloading x64 ffmpeg/ffprobe from ffbinaries..."
download_and_verify_zip "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.4.1/ffmpeg-4.4.1-osx-64.zip" ffmpeg-x64.zip
download_and_verify_zip "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.4.1/ffprobe-4.4.1-osx-64.zip" ffprobe-x64.zip
unzip -q ffmpeg-x64.zip -d ffmpeg-x64-temp
unzip -q ffprobe-x64.zip -d ffprobe-x64-temp
X64_FFMPEG_PATH="$(find ffmpeg-x64-temp -type f -name ffmpeg | head -n 1 || true)"
X64_FFPROBE_PATH="$(find ffprobe-x64-temp -type f -name ffprobe | head -n 1 || true)"
if [ -z "$X64_FFMPEG_PATH" ] || [ -z "$X64_FFPROBE_PATH" ]; then
echo "ERROR: x64 ffmpeg/ffprobe binary missing from downloaded archives"
exit 1
fi
file "$X64_FFMPEG_PATH"
file "$X64_FFPROBE_PATH"
if ! file "$X64_FFMPEG_PATH" | grep -q "x86_64"; then
echo "ERROR: x64 ffmpeg artifact is not x86_64"
exit 1
fi
if ! file "$X64_FFPROBE_PATH" | grep -q "x86_64"; then
echo "ERROR: x64 ffprobe artifact is not x86_64"
exit 1
fi
install -m 755 "$X64_FFMPEG_PATH" vendor/ffmpeg/darwin-x64/ffmpeg
install -m 755 "$X64_FFPROBE_PATH" vendor/ffmpeg/darwin-x64/ffprobe
rm -rf ffmpeg-x64-temp ffprobe-x64-temp ffmpeg-x64.zip ffprobe-x64.zip
if [ ! -f vendor/ffmpeg/darwin-x64/ffprobe ]; then
echo "ERROR: x64 ffprobe is missing"
exit 1
fi
# 输出原始二进制信息
echo "=== Raw arm64 binary ==="
file vendor/ffmpeg/darwin-arm64/ffmpeg
file vendor/ffmpeg/darwin-arm64/ffprobe
ls -lh vendor/ffmpeg/darwin-arm64/ffmpeg
ls -lh vendor/ffmpeg/darwin-arm64/ffprobe
shasum -a 256 vendor/ffmpeg/darwin-arm64/ffmpeg || true
shasum -a 256 vendor/ffmpeg/darwin-arm64/ffprobe || true
lipo -archs vendor/ffmpeg/darwin-arm64/ffmpeg || true
echo "=== Raw x64 binary ==="
file vendor/ffmpeg/darwin-x64/ffmpeg
file vendor/ffmpeg/darwin-x64/ffprobe
ls -lh vendor/ffmpeg/darwin-x64/ffmpeg
ls -lh vendor/ffmpeg/darwin-x64/ffprobe
shasum -a 256 vendor/ffmpeg/darwin-x64/ffmpeg || true
shasum -a 256 vendor/ffmpeg/darwin-x64/ffprobe || true
lipo -archs vendor/ffmpeg/darwin-x64/ffmpeg || true
# 生成通用二进制并放入固定目录
echo "Creating universal ffmpeg via lipo"
lipo -create -output vendor/ffmpeg/darwin/ffmpeg \
vendor/ffmpeg/darwin-arm64/ffmpeg \
vendor/ffmpeg/darwin-x64/ffmpeg
echo "Creating universal ffprobe via lipo"
lipo -create -output vendor/ffmpeg/darwin/ffprobe \
vendor/ffmpeg/darwin-arm64/ffprobe \
vendor/ffmpeg/darwin-x64/ffprobe
chmod +x vendor/ffmpeg/darwin/ffmpeg
chmod +x vendor/ffmpeg/darwin/ffprobe
echo "=== Universal binary ==="
file vendor/ffmpeg/darwin/ffmpeg
file vendor/ffmpeg/darwin/ffprobe
ls -lh vendor/ffmpeg/darwin/ffmpeg
ls -lh vendor/ffmpeg/darwin/ffprobe
shasum -a 256 vendor/ffmpeg/darwin/ffmpeg || true
shasum -a 256 vendor/ffmpeg/darwin/ffprobe || true
lipo -archs vendor/ffmpeg/darwin/ffmpeg || true
lipo -archs vendor/ffmpeg/darwin/ffprobe || true
# 清理单架构副本
rm -rf vendor/ffmpeg/darwin-arm64 vendor/ffmpeg/darwin-x64
echo "Listing vendor/ffmpeg directory"
find vendor/ffmpeg -maxdepth 2 -type f -exec ls -l {} \;
- name: Prepare FFmpeg native dependencies (macOS)
run: |
set -euxo pipefail
copy_ffmpeg_dev_files() {
local source_prefix="$1"
local dest_dir="$2"
local lib_dir="$source_prefix/lib"
local include_dir="$source_prefix/include"
mkdir -p "$dest_dir/lib" "$dest_dir/include"
for lib in libavcodec libavformat libavutil libswresample; do
if [ -f "$lib_dir/${lib}.a" ]; then
cp -L "$lib_dir/${lib}.a" "$dest_dir/lib/"
echo "Copied ${lib}.a from ${lib_dir}"
else
echo "ERROR: ${lib}.a not found at ${lib_dir}/"
exit 1
fi
done
for dir in libavcodec libavformat libavutil libswresample; do
if [ -d "$include_dir/$dir" ]; then
rm -rf "$dest_dir/include/$dir"
cp -RL "$include_dir/$dir" "$dest_dir/include/"
echo "Copied ${dir} headers from ${include_dir}"
else
echo "ERROR: ${dir} headers not found at ${include_dir}/"
exit 1
fi
done
if [ ! -f "$dest_dir/include/libavcodec/avcodec.h" ]; then
echo "ERROR: copied FFmpeg headers are incomplete at ${dest_dir}/include"
find "$dest_dir/include" -maxdepth 2 -type l -o -type f | head -n 40 || true
exit 1
fi
echo "=== FFmpeg native deps: ${dest_dir} ==="
file "$dest_dir/lib/libavcodec.a" || true
ls -lh "$dest_dir/lib/"
ls "$dest_dir/include/"
}
ARM64_BREW_PREFIX="$(brew --prefix)"
copy_ffmpeg_dev_files "$ARM64_BREW_PREFIX" "rust_package/native/ffmpeg/darwin-arm64"
copy_ffmpeg_dev_files "artifacts/ffmpeg-x64-dev" "rust_package/native/ffmpeg/darwin-x64"
if ! lipo -archs rust_package/native/ffmpeg/darwin-x64/lib/libavcodec.a | grep -q "x86_64"; then
echo "ERROR: Intel FFmpeg static lib is not x86_64"
lipo -archs rust_package/native/ffmpeg/darwin-x64/lib/libavcodec.a || true
exit 1
fi
if ! lipo -archs rust_package/native/ffmpeg/darwin-arm64/lib/libavcodec.a | grep -q "arm64"; then
echo "ERROR: arm64 FFmpeg static lib is not arm64"
lipo -archs rust_package/native/ffmpeg/darwin-arm64/lib/libavcodec.a || true
exit 1
fi
- name: Validate bundled media tools (macOS)
run: node scripts/validate-bundled-media-tools.mjs --mode package --platform-key darwin-universal
- name: Build Rust N-API for mac (both arch)
working-directory: rust_package
run: |
npx --yes @napi-rs/cli@2.18.4 build --platform --release --target x86_64-apple-darwin
npx --yes @napi-rs/cli@2.18.4 build --platform --release --target aarch64-apple-darwin
- name: Clean FFmpeg native build inputs (macOS)
run: rm -rf rust_package/native/ffmpeg/darwin-arm64 rust_package/native/ffmpeg/darwin-x64
- name: Build (vite)
env:
CLOUD_SYNC_BASE_URL_PROD: ${{ secrets.CLOUD_SYNC_BASE_URL_PROD }}
CLOUD_SYNC_API_SECRET_KEY: ${{ secrets.CLOUD_SYNC_API_SECRET_KEY }}
FRKB_LASTFM_API_KEY: ${{ secrets.FRKB_LASTFM_API_KEY }}
run: pnpm build
- name: Package macOS (universal, no publish)
env:
DEBUG: electron-builder
run: |
sed -i.bak '/^electron_mirror=/d' .npmrc && rm -f .npmrc.bak
npx electron-builder --mac --universal --publish never
- name: Verify macOS RC signing mode
run: |
set -euo pipefail
APP_DIR="$(find dist/mac-universal -maxdepth 1 -name '*.app' -type d | head -n 1 || true)"
if [ -z "$APP_DIR" ]; then
echo "macOS app bundle not found under dist/mac-universal"
exit 1
fi
for binary_path in \
"$APP_DIR/Contents/MacOS/FRKB" \
"$APP_DIR/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework"; do
signature_info="$(codesign -dvvv "$binary_path" 2>&1)"
if grep -q 'flags=.*runtime' <<<"$signature_info"; then
echo "Unexpected Hardened Runtime signature: $binary_path"
echo "$signature_info"
exit 1
fi
done
- name: Verify packaged Demucs resources (macOS)
run: |
set -euo pipefail
APP_DIR="$(find dist/mac-universal -maxdepth 1 -name '*.app' -type d | head -n 1 || true)"
if [ -z "$APP_DIR" ]; then
echo "macOS app bundle not found under dist/mac-universal"
exit 1
fi
RESOURCES_DIR="${APP_DIR}/Contents/Resources"
if [ ! -d "$RESOURCES_DIR" ]; then
echo "macOS resources directory not found: ${RESOURCES_DIR}"
exit 1
fi
REQUIRED_PATHS=(
"ffmpeg/darwin/ffmpeg"
"ffmpeg/darwin/ffprobe"
"demucs/bootstrap/mixtape_demucs_bootstrap.py"
"demucs/bootstrap/beat_this_bridge.py"
"demucs/bootstrap/rkb_official_phase_selector.py"
"demucs/models/htdemucs.yaml"
"demucs/models/955717e8-8726e21a.th"
"rekordboxDesktopLibrary/bridge.py"
"rekordboxDesktopRuntime/darwin-arm64/python/bin/python3"
"rekordboxDesktopRuntime/darwin-x64/python/bin/python3"
)
UNEXPECTED_PATHS=(
"demucs/darwin-arm64"
"demucs/darwin-x64"
"demucs/win32-x64"
)
MISSING=0
for relative_path in "${REQUIRED_PATHS[@]}"; do
target_path="${RESOURCES_DIR}/${relative_path}"
if [ ! -e "$target_path" ]; then
echo "Missing packaged Demucs resource (macOS): ${relative_path}"
MISSING=1
fi
done
for relative_path in "${UNEXPECTED_PATHS[@]}"; do
target_path="${RESOURCES_DIR}/${relative_path}"
if [ -e "$target_path" ]; then
echo "Unexpected packaged Demucs runtime (macOS): ${relative_path}"
MISSING=1
fi
done
if [ "$MISSING" -ne 0 ]; then
exit 1
fi
python3 scripts/verify_packaged_python_import_closure.py \
--source-root scripts \
--packaged-root "${RESOURCES_DIR}/demucs/bootstrap" \
--entry beat_this_bridge.py
echo "Packaged Demucs resources verified (macOS, runtime excluded)."
# 不再复制 rc.yml / rc-mac.yml,保持标准文件名
- name: Upload dist artifacts (macOS)
uses: actions/upload-artifact@v7
if: always() && startsWith(github.ref, 'refs/tags/')
with:
name: mac-artifacts
path: |
dist/*.dmg
dist/*.yml
dist/Track-Studio-*-mac-universal.zip
dist/Track-Studio-*-mac-universal.zip.blockmap
dist/*.blockmap
dist/*.zip
release:
if: >-
${{
(github.event_name == 'push' || github.event_name == 'workflow_dispatch') &&
startsWith(github.ref, 'refs/tags/')
}}
needs: [validate_release_tag, typecheck, build_win, build_mac]
runs-on: ubuntu-latest
steps:
- name: Download Windows artifacts
uses: actions/download-artifact@v8
with:
name: win-artifacts
path: artifacts/win
- name: Download macOS artifacts
uses: actions/download-artifact@v8
with:
name: mac-artifacts
path: artifacts/mac
- name: Show downloaded files
run: ls -R artifacts | cat
- name: Create/Update GitHub Release and upload assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG_NAME: ${{ github.ref_name }}
PRERELEASE: ${{ contains(github.ref_name, '-') }}
run: |
set -euo pipefail
shopt -s nullglob
assets=(
artifacts/win/*.exe
artifacts/win/latest.yml
artifacts/win/*.blockmap
artifacts/mac/*.dmg
artifacts/mac/latest-mac.yml
artifacts/mac/*.zip
artifacts/mac/*.blockmap
)
if [ ${#assets[@]} -eq 0 ]; then
echo "No release assets found."
exit 1
fi
if gh release view "$TAG_NAME" >/dev/null 2>&1; then
gh release upload "$TAG_NAME" "${assets[@]}" --clobber
if [ "$PRERELEASE" = "true" ]; then
gh release edit "$TAG_NAME" --title "$TAG_NAME" --prerelease
else
gh release edit "$TAG_NAME" --title "$TAG_NAME" --latest
fi
else
if [ "$PRERELEASE" = "true" ]; then
gh release create "$TAG_NAME" "${assets[@]}" --title "$TAG_NAME" --prerelease --verify-tag --generate-notes
else
gh release create "$TAG_NAME" "${assets[@]}" --title "$TAG_NAME" --latest --verify-tag --generate-notes
fi
fi
- name: Remove superseded RC releases
if: contains(github.ref_name, '-rc.')
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
base_version="${TAG_NAME%%-rc.*}"
mapfile -t stale_tags < <(
gh release list --limit 100 --json tagName,isPrerelease \
--jq ".[] | select(.isPrerelease and .tagName != \"${TAG_NAME}\" and (.tagName | startswith(\"${base_version}-rc.\"))) | .tagName"
)
for stale_tag in "${stale_tags[@]}"; do
echo "Removing superseded RC release and tag: ${stale_tag}"
gh release delete "${stale_tag}" --yes --cleanup-tag
done