From 7dfff1b7fa2a1b7f25996291eb7b479e8e5dc802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Thu, 7 May 2026 12:16:13 +0200 Subject: [PATCH] feat(launcher): icon + universal build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Context: the launcher .app needs (a) a real icon visible in Dock / Mission Control / Finder when its log window is open, and (b) a binary that runs on both Intel MBPs (the current target on the 192.168.0.189 machine) and Apple Silicon. Initial build was arm64-only. Approach: SVG icon rendered via librsvg into a multi-size .iconset, packed into AppIcon.icns by iconutil (no Xcode asset catalog needed). build.sh now compiles for both arm64 and x86_64 separately and lipos them into a fat binary before bundling. The icon is regenerated only when the SVG is newer than the existing .icns. Also switched the default oscope-of binary path to look inside the .app bundle that oF Release produces, with a walk-up-to-bin/ heuristic for the cwd. Changes: - Resources/icon.svg : 1024x1024 design — orange electron with asymmetric googly eyes, sticking-out tongue, three rotated chaotic orbital ellipses (cyan / purple / pink), radiating yellow lightning bolts and scattered sparks on a dark purple radial gradient with Big-Sur rounded-rect mask - Resources/make_icon.sh : rsvg-convert at 16/32/128/256/512 px @1x and @2x, then iconutil -c icns - Resources/Info.plist : CFBundleIconFile = AppIcon - build.sh : iterate ARCHS (default 'arm64 x86_64'), build each, lipo -create them, regen icon if SVG is newer, copy AppIcon.icns into Contents/Resources - ProcessManager.swift : * default oscopePath now prefers .../bin/oscope-of.app/Contents/MacOS/oscope-of (the bundle that openFrameworks produces) with fallback to bare binary * new oscopeProjectRoot(from:) walks up the path to find the parent of 'bin/', so cwd is the oF project root regardless of bundle vs bare binary, letting oF locate bin/data/ - .gitignore : exclude generated AppIcon.iconset and AppIcon.icns Impact: ./build.sh now produces a universal .app that launches on both arm64 and Intel macs (smoke-tested on Big Sur Intel MBP). The icon appears in the Dock when the log window is open, and oscope-of starts correctly regardless of how it was built. --- launcher/.gitignore | 2 + launcher/Resources/Info.plist | 2 + launcher/Resources/icon.svg | 86 +++++++++++++++++++ launcher/Resources/make_icon.sh | 34 ++++++++ .../AVLiveLauncher/ProcessManager.swift | 36 ++++++-- launcher/build.sh | 35 ++++++-- 6 files changed, 182 insertions(+), 13 deletions(-) create mode 100644 launcher/Resources/icon.svg create mode 100755 launcher/Resources/make_icon.sh diff --git a/launcher/.gitignore b/launcher/.gitignore index 9629e8d..a63d06b 100644 --- a/launcher/.gitignore +++ b/launcher/.gitignore @@ -3,3 +3,5 @@ build/ .swiftpm/ Package.resolved DerivedData/ +Resources/AppIcon.iconset/ +Resources/AppIcon.icns diff --git a/launcher/Resources/Info.plist b/launcher/Resources/Info.plist index e95ddab..750d58d 100644 --- a/launcher/Resources/Info.plist +++ b/launcher/Resources/Info.plist @@ -6,6 +6,8 @@ en CFBundleExecutable AVLiveLauncher + CFBundleIconFile + AppIcon CFBundleIdentifier cc.saillant.AVLiveLauncher CFBundleInfoDictionaryVersion diff --git a/launcher/Resources/icon.svg b/launcher/Resources/icon.svg new file mode 100644 index 0000000..d12f510 --- /dev/null +++ b/launcher/Resources/icon.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/launcher/Resources/make_icon.sh b/launcher/Resources/make_icon.sh new file mode 100755 index 0000000..f180529 --- /dev/null +++ b/launcher/Resources/make_icon.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# Convertit Resources/icon.svg en AppIcon.icns (multi-sizes) +set -eu +cd "$(dirname "$0")" + +SVG=icon.svg +SET=AppIcon.iconset + +[ -f "$SVG" ] || { echo "missing $SVG" >&2; exit 1; } +command -v rsvg-convert >/dev/null || { echo "needs rsvg-convert (brew install librsvg)" >&2; exit 1; } +command -v iconutil >/dev/null || { echo "needs iconutil (Xcode CLT)" >&2; exit 1; } + +rm -rf "$SET" AppIcon.icns +mkdir -p "$SET" + +# (size_in_pt, scale, filename) +render() { + px=$(( $1 * $2 )) + rsvg-convert -w "$px" -h "$px" "$SVG" -o "$SET/icon_${1}x${1}${3}.png" +} + +render 16 1 "" +render 16 2 "@2x" +render 32 1 "" +render 32 2 "@2x" +render 128 1 "" +render 128 2 "@2x" +render 256 1 "" +render 256 2 "@2x" +render 512 1 "" +render 512 2 "@2x" + +iconutil -c icns "$SET" -o AppIcon.icns +echo "==> AppIcon.icns ($(du -k AppIcon.icns | cut -f1) KB)" diff --git a/launcher/Sources/AVLiveLauncher/ProcessManager.swift b/launcher/Sources/AVLiveLauncher/ProcessManager.swift index e9536a4..5ad6ccb 100644 --- a/launcher/Sources/AVLiveLauncher/ProcessManager.swift +++ b/launcher/Sources/AVLiveLauncher/ProcessManager.swift @@ -31,8 +31,20 @@ final class ProcessManager: ObservableObject { ?? "/Applications/SuperCollider.app/Contents/MacOS/sclang" soundAlgoLoadFile = defaults.string(forKey: "soundAlgoLoadFile") ?? "\(home)/Documents/Projets/AV-Live/sound_algo/00_load.scd" - oscopePath = defaults.string(forKey: "oscopePath") - ?? "\(home)/Documents/Projets/AV-Live/oscope-of/bin/oscope-of" + // openFrameworks Release produces a .app bundle in bin/. Default to the + // executable inside the bundle, with fallback to the bare binary if + // the user built with a custom Makefile target. + let avLive = "\(home)/Documents/Projets/AV-Live/oscope-of" + let bundled = "\(avLive)/bin/oscope-of.app/Contents/MacOS/oscope-of" + let bare = "\(avLive)/bin/oscope-of" + let stored = defaults.string(forKey: "oscopePath") + if let s = stored, !s.isEmpty { + oscopePath = s + } else if FileManager.default.fileExists(atPath: bundled) { + oscopePath = bundled + } else { + oscopePath = bare + } } // MARK: - sclang @@ -84,9 +96,7 @@ final class ProcessManager: ObservableObject { } let p = Process() p.executableURL = URL(fileURLWithPath: oscopePath) - p.currentDirectoryURL = URL(fileURLWithPath: oscopePath) - .deletingLastPathComponent() - .deletingLastPathComponent() + p.currentDirectoryURL = oscopeProjectRoot(from: oscopePath) attach(process: p, label: "oscope") do { try p.run() @@ -109,6 +119,22 @@ final class ProcessManager: ObservableObject { oscopeProc?.terminate() } + /// openFrameworks expects the working dir to be the project root (the + /// directory containing `bin/`) so it can find `bin/data/`. The binary + /// path can be either `/bin/oscope-of` (bare) or + /// `/bin/oscope-of.app/Contents/MacOS/oscope-of` (release bundle) — + /// walk up to the parent of `bin/` either way. + private func oscopeProjectRoot(from binaryPath: String) -> URL { + var url = URL(fileURLWithPath: binaryPath).deletingLastPathComponent() + while url.path != "/" { + if url.lastPathComponent == "bin" { + return url.deletingLastPathComponent() + } + url.deleteLastPathComponent() + } + return URL(fileURLWithPath: binaryPath).deletingLastPathComponent() + } + // MARK: - utilities func stopAll() { diff --git a/launcher/build.sh b/launcher/build.sh index a7b2114..a8fe778 100755 --- a/launcher/build.sh +++ b/launcher/build.sh @@ -3,17 +3,25 @@ set -eu cd "$(dirname "$0")" -echo "==> swift build (release)" -swift build -c release - BIN_NAME="AVLiveLauncher" -BIN_PATH=".build/release/${BIN_NAME}" APP_PATH="build/${BIN_NAME}.app" +ARCHS=${ARCHS:-arm64 x86_64} -if [ ! -x "${BIN_PATH}" ]; then - echo "Build did not produce ${BIN_PATH}" >&2 - exit 1 -fi +# Build each arch and lipo them together when more than one +BIN_PARTS="" +for arch in ${ARCHS}; do + echo "==> swift build (release, ${arch})" + swift build -c release --arch "${arch}" + PART=".build/${arch}-apple-macosx/release/${BIN_NAME}" + [ -x "${PART}" ] || { echo "Missing ${PART}" >&2; exit 1; } + BIN_PARTS="${BIN_PARTS} ${PART}" +done + +BIN_PATH=".build/release/${BIN_NAME}-universal" +mkdir -p "$(dirname "${BIN_PATH}")" +# shellcheck disable=SC2086 +lipo -create -output "${BIN_PATH}" ${BIN_PARTS} +echo "==> universal binary $(lipo -info "${BIN_PATH}")" echo "==> bundling ${APP_PATH}" rm -rf "${APP_PATH}" @@ -21,6 +29,17 @@ mkdir -p "${APP_PATH}/Contents/MacOS" "${APP_PATH}/Contents/Resources" cp "${BIN_PATH}" "${APP_PATH}/Contents/MacOS/${BIN_NAME}" cp Resources/Info.plist "${APP_PATH}/Contents/Info.plist" +# Build AppIcon.icns from SVG if missing or stale +if [ ! -f Resources/AppIcon.icns ] \ + || [ Resources/icon.svg -nt Resources/AppIcon.icns ]; then + if command -v rsvg-convert >/dev/null 2>&1; then + ( cd Resources && ./make_icon.sh ) + else + echo "warn: rsvg-convert missing, skipping icon regen" >&2 + fi +fi +[ -f Resources/AppIcon.icns ] && cp Resources/AppIcon.icns "${APP_PATH}/Contents/Resources/AppIcon.icns" + # Ad-hoc sign so Gatekeeper at least lets the user open it manually codesign --force --sign - "${APP_PATH}" 2>/dev/null || true