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