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.
269 lines
9.2 KiB
Bash
Executable File
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
|