Files
bwf-analyser/mac-app/Build BWF Analyser.command
T
Vincent 2d0b7fe8b5 BWF Analyser: browser page and macOS app
Reads and edits BWF metadata for production sound. One source tree builds a
single self-contained page and a native Tauri app with a Rust audio engine and
WAV writer. Around 370 checks across seven test suites.

First commit of the existing state, so that from here every change can be
seen and undone.
2026-08-17 22:50:39 +08:00

269 lines
9.2 KiB
Bash
Executable File

#!/bin/bash
#
# Double-click this file. It builds BWF Analyser.app next to itself.
#
# What it does, in order: makes sure Apple's command line tools and Rust are
# present (installing Rust itself if it isn't), compiles the Rust binary,
# assembles the .app bundle around it, signs it locally, and opens it.
#
# Safe to run again: a rebuild after the first one takes seconds, since cargo
# caches everything.
set -euo pipefail
cd "$(dirname "$0")"
BOLD=$'\033[1m'
DIM=$'\033[2m'
GREEN=$'\033[32m'
RED=$'\033[31m'
YELLOW=$'\033[33m'
RESET=$'\033[0m'
APP_NAME="BWF Analyser"
BUNDLE_ID="com.vincentrozenberg.bwf-analyser"
VERSION="1.5.1"
BINARY_NAME="bwf-analyser"
APP_DIR="$PWD/$APP_NAME.app"
step() { printf "\n%s==>%s %s%s%s\n" "$GREEN" "$RESET" "$BOLD" "$1" "$RESET"; }
info() { printf " %s%s%s\n" "$DIM" "$1" "$RESET"; }
warn() { printf " %s%s%s\n" "$YELLOW" "$1" "$RESET"; }
fail() {
printf "\n%sBuild failed:%s %s\n\n" "$RED" "$RESET" "$1"
printf "This window stays open so you can read the error above.\n"
printf "Press Return to close it.\n"
read -r _ || true
exit 1
}
trap 'fail "see the last message above"' ERR
printf "%s%s %s — build%s\n" "$BOLD" "$APP_NAME" "$VERSION" "$RESET"
printf "%sThis compiles a native macOS app from source. First run takes a while;%s\n" "$DIM" "$RESET"
printf "%severything after that is quick.%s\n" "$DIM" "$RESET"
# ---------------------------------------------------------------------------
# 1. Apple command line tools (needed for the linker)
# ---------------------------------------------------------------------------
step "Checking Apple command line tools"
if xcode-select -p >/dev/null 2>&1; then
info "already installed at $(xcode-select -p)"
else
warn "not installed — asking macOS to install them now."
warn "A system dialog will appear: click Install and accept the licence."
xcode-select --install >/dev/null 2>&1 || true
info "waiting for the installation to finish (this can take 10+ minutes)…"
waited=0
until xcode-select -p >/dev/null 2>&1; do
sleep 15
waited=$((waited + 15))
if [ "$waited" -ge 3600 ]; then
fail "command line tools still aren't installed. Finish the Apple installer, then run this again."
fi
if [ $((waited % 120)) -eq 0 ]; then
info "still waiting… ($((waited / 60)) min)"
fi
done
info "installed."
fi
# ---------------------------------------------------------------------------
# 2. Rust
# ---------------------------------------------------------------------------
step "Checking Rust"
# Some of Tauri's dependencies are published with edition 2024, which older
# toolchains refuse to even parse. An existing-but-ancient Rust is the most
# likely thing to go wrong here, so check the version, not just the presence.
MIN_MAJOR=1
MIN_MINOR=85
if ! command -v cargo >/dev/null 2>&1; then
if [ -f "$HOME/.cargo/env" ]; then
# shellcheck disable=SC1091
. "$HOME/.cargo/env"
fi
fi
rust_version() {
# "rustc 1.81.0 (2dbb1af80 2024-08-20)" -> "1.81.0"
rustc --version 2>/dev/null | awk '{print $2}'
}
rust_is_recent_enough() {
local version major minor
version="$(rust_version)"
[ -n "$version" ] || return 1
major="${version%%.*}"
minor="${version#*.}"
minor="${minor%%.*}"
[ "$major" -gt "$MIN_MAJOR" ] && return 0
[ "$major" -eq "$MIN_MAJOR" ] && [ "$minor" -ge "$MIN_MINOR" ]
}
install_rustup() {
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs |
sh -s -- -y --profile minimal --default-toolchain stable --no-modify-path ||
fail "could not install Rust — check your internet connection."
# shellcheck disable=SC1091
. "$HOME/.cargo/env"
}
if ! command -v cargo >/dev/null 2>&1; then
warn "not found — installing Rust into ~/.rustup and ~/.cargo (nothing else on your"
warn "system is touched, and 'rustup self uninstall' removes it completely)."
install_rustup
fi
if rust_is_recent_enough; then
info "$(cargo --version)"
else
warn "Rust $(rust_version) is too old — this needs $MIN_MAJOR.$MIN_MINOR or newer."
if command -v rustup >/dev/null 2>&1; then
info "updating your existing toolchain…"
rustup update stable || fail "rustup update failed."
rustup default stable || true
# shellcheck disable=SC1091
[ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"
else
warn "your Rust wasn't installed by rustup (Homebrew or a package, most likely),"
warn "so it can't be updated from here. Installing rustup alongside it."
install_rustup
fi
if ! rust_is_recent_enough; then
fail "Rust is still $(rust_version), and $MIN_MAJOR.$MIN_MINOR+ is required.
If you installed Rust with Homebrew, either run 'brew upgrade rust', or remove it
('brew uninstall rust') and let this script install rustup instead. If rustup is
present, check that '$HOME/.cargo/bin' comes first on your PATH."
fi
info "now on $(cargo --version)"
fi
# ---------------------------------------------------------------------------
# 3. Compile
# ---------------------------------------------------------------------------
step "Assembling the app's page"
# This script used to compile whatever dist/index.html happened to be sitting
# on disk. If that copy was stale, the app came out looking almost right — new
# button visible, nothing behind it — which is a miserable thing to debug. So
# build the page here, from source, every time.
if command -v python3 >/dev/null 2>&1 && [ -f "../build/build.py" ]; then
( cd .. && python3 build/build.py --tauri ) || fail "assembling dist/index.html failed. The error is above."
else
warn "python3 or build/build.py not found — using the dist/index.html already on disk."
fi
[ -f "dist/index.html" ] || fail "dist/index.html is missing — the app's frontend should sit next to this script."
info "$(wc -c < dist/index.html | tr -d ' ') bytes, written $(date -r dist/index.html '+%H:%M:%S')"
step "Compiling (first build downloads a few hundred crates — 5 to 20 minutes)"
# The page is baked into the binary by a macro, and cargo does not reliably
# notice that the folder it reads changed — so a page-only change can compile
# to "nothing to do" and produce an app with the previous page still inside.
# Touching main.rs forces the macro to run again. Cheap: it is one crate.
touch src-tauri/src/main.rs
cargo build --release --manifest-path src-tauri/Cargo.toml ||
fail "the Rust build failed. The compiler error is above."
BUILT_BINARY="src-tauri/target/release/$BINARY_NAME"
[ -f "$BUILT_BINARY" ] || fail "expected a binary at $BUILT_BINARY but it isn't there."
# ---------------------------------------------------------------------------
# 4. Assemble the .app bundle
# ---------------------------------------------------------------------------
step "Building $APP_NAME.app"
# A running copy has to go first. Replacing the bundle underneath a live
# process is allowed, but `open` at the end would then just activate the
# instance already running — the old code — which looks exactly like a build
# that silently did nothing.
if pgrep -x "$APP_NAME" >/dev/null 2>&1; then
info "quitting the running copy first"
pkill -x "$APP_NAME" 2>/dev/null || true
waited=0
while pgrep -x "$APP_NAME" >/dev/null 2>&1 && [ "$waited" -lt 10 ]; do
sleep 1
waited=$((waited + 1))
done
fi
rm -rf "$APP_DIR"
mkdir -p "$APP_DIR/Contents/MacOS" "$APP_DIR/Contents/Resources"
cp "$BUILT_BINARY" "$APP_DIR/Contents/MacOS/$APP_NAME"
chmod +x "$APP_DIR/Contents/MacOS/$APP_NAME"
cp "src-tauri/icons/icon.icns" "$APP_DIR/Contents/Resources/icon.icns"
cat > "$APP_DIR/Contents/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>$APP_NAME</string>
<key>CFBundleExecutable</key>
<string>$APP_NAME</string>
<key>CFBundleIconFile</key>
<string>icon.icns</string>
<key>CFBundleIdentifier</key>
<string>$BUNDLE_ID</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$APP_NAME</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$VERSION</string>
<key>CFBundleVersion</key>
<string>$VERSION</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.music</string>
<key>LSMinimumSystemVersion</key>
<string>10.15</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>MIT licensed</string>
<key>NSSupportsAutomaticGraphicsSwitching</key>
<true/>
</dict>
</plist>
PLIST
printf 'APPL????' > "$APP_DIR/Contents/PkgInfo"
# Ad-hoc signature. Not a developer certificate — it just satisfies macOS's
# requirement that a bundle be signed at all, which matters on Apple silicon.
codesign --force --sign - --timestamp=none "$APP_DIR" >/dev/null 2>&1 ||
warn "could not sign the bundle; it should still run, since you built it yourself."
# Locally built files aren't quarantined, but clear it anyway in case the
# folder came from a download.
xattr -cr "$APP_DIR" 2>/dev/null || true
step "Done"
printf " %s%s%s\n" "$BOLD" "$APP_DIR" "$RESET"
info "Drag it to /Applications if you want it in Launchpad."
open "$APP_DIR" || true
printf "\nPress Return to close this window.\n"
read -r _ || true