Files
bwf-analyser/mac-app/Build BWF Analyser.command
T
Vincent 16c3e6e103 Drop the browser build; space works the transport; fix bulk track rename
The browser page is gone, with its shim, its shell CSS and its suite. build.py
has no --tauri flag and one output: the app's page. The patch lists lose the
SHARED_ prefix, which only ever meant "shared between the two builds", and
test-pdf.js was quietly loading the browser page — it reads the app's now.

Space plays and pauses, in the main window off the player at the bottom and in
the mixer off the same transport. It is swallowed rather than passed on: that
stops the window scrolling, and stops a focused button taking the same press as
a second activation, which toggles twice and looks like a dead key. Typing, and
any dialog other than the mixer, keep it. Deciding what is in front needs both
markers — closing does not always remove .open, and .modal without it is
display:none regardless of the hidden attribute.

The mixer footer put the next file's name adrift in the middle of the row. The
margin-left: auto meant to park it beside Close could do nothing, because the
framework's flex: 1 1 auto had both nav groups growing to fill the row and left
no free space to absorb. Pinned to flex: 0 0 auto. The file-detail modal shares
the markup and follows.

Bulk track rename wrote nothing, on every file, and had never worked. A rename
is keyed on the name it replaces rather than a path, so the frame-rate pass
threw reading edit.path off it — before any byte was written, so nothing was
corrupted, only untouched. The failure handler discarded the reason and the run
still ended on a tidy Done, which is why 0 saved read as progress; it now names
the file and the cause in the log.

429 checks. The gap that hid the rename bug was end-to-end: the writer was unit
tested and the panel rendered, and nothing drove one into the other.
2026-08-18 00:14:36 +08:00

269 lines
9.1 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 ) || 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