#!/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" < CFBundleDevelopmentRegion en CFBundleDisplayName $APP_NAME CFBundleExecutable $APP_NAME CFBundleIconFile icon.icns CFBundleIdentifier $BUNDLE_ID CFBundleInfoDictionaryVersion 6.0 CFBundleName $APP_NAME CFBundlePackageType APPL CFBundleShortVersionString $VERSION CFBundleVersion $VERSION LSApplicationCategoryType public.app-category.music LSMinimumSystemVersion 10.15 NSHighResolutionCapable NSHumanReadableCopyright MIT licensed NSSupportsAutomaticGraphicsSwitching 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