feat(launcher): icon + universal build

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.
This commit is contained in:
L'électron rare
2026-05-07 12:16:13 +02:00
parent 8dab7236d4
commit 7dfff1b7fa
6 changed files with 182 additions and 13 deletions
+2
View File
@@ -3,3 +3,5 @@ build/
.swiftpm/
Package.resolved
DerivedData/
Resources/AppIcon.iconset/
Resources/AppIcon.icns
+2
View File
@@ -6,6 +6,8 @@
<string>en</string>
<key>CFBundleExecutable</key>
<string>AVLiveLauncher</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>CFBundleIdentifier</key>
<string>cc.saillant.AVLiveLauncher</string>
<key>CFBundleInfoDictionaryVersion</key>
+86
View File
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<defs>
<radialGradient id="bg" cx="50%" cy="35%" r="65%">
<stop offset="0" stop-color="#3b1e8c"/>
<stop offset="0.6" stop-color="#1a0b3d"/>
<stop offset="1" stop-color="#05021a"/>
</radialGradient>
<radialGradient id="electron" cx="50%" cy="40%" r="55%">
<stop offset="0" stop-color="#fffae0"/>
<stop offset="0.45" stop-color="#ffd000"/>
<stop offset="1" stop-color="#ff5500"/>
</radialGradient>
<radialGradient id="glow" cx="50%" cy="50%" r="50%">
<stop offset="0" stop-color="#ffea00" stop-opacity="0.55"/>
<stop offset="1" stop-color="#ffea00" stop-opacity="0"/>
</radialGradient>
<filter id="blur" x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur stdDeviation="14"/>
</filter>
</defs>
<!-- macOS Big Sur rounded rect base -->
<rect x="64" y="64" width="896" height="896" rx="220" fill="url(#bg)"/>
<!-- Chaotic orbital ellipses (not concentric, "fou") -->
<g transform="translate(512 512)" stroke-width="7" fill="none" opacity="0.9">
<ellipse cx="0" cy="-30" rx="400" ry="130" transform="rotate(18)" stroke="#5cd0ff"/>
<ellipse cx="20" cy="10" rx="340" ry="220" transform="rotate(-37)" stroke="#a76dff"/>
<ellipse cx="-15" cy="40" rx="395" ry="90" transform="rotate(78)" stroke="#ff5577"/>
</g>
<!-- Lightning bolts radiating outward -->
<g fill="none" stroke="#ffea00" stroke-width="16" stroke-linejoin="round" stroke-linecap="round">
<polyline points="220,200 330,310 270,330 410,470"/>
<polyline points="800,200 700,310 760,340 660,460"/>
<polyline points="200,810 300,720 260,690 380,590"/>
<polyline points="820,810 720,720 770,690 660,580"/>
<polyline points="500,140 540,260 500,260 540,360"/>
<polyline points="500,860 540,750 500,750 540,650"/>
</g>
<!-- Outer halo glow -->
<circle cx="512" cy="512" r="280" fill="url(#glow)" filter="url(#blur)"/>
<!-- Electron body -->
<circle cx="512" cy="512" r="170" fill="url(#electron)" stroke="#ff8800" stroke-width="5"/>
<!-- Crazy eyes: asymmetric pupils, looking different directions -->
<g>
<ellipse cx="448" cy="488" rx="44" ry="56" fill="#fff" stroke="#000" stroke-width="4"/>
<ellipse cx="576" cy="488" rx="44" ry="56" fill="#fff" stroke="#000" stroke-width="4"/>
<circle cx="430" cy="468" r="18" fill="#000"/>
<circle cx="592" cy="510" r="20" fill="#000"/>
<!-- highlight glints -->
<circle cx="425" cy="463" r="6" fill="#fff"/>
<circle cx="587" cy="505" r="6" fill="#fff"/>
</g>
<!-- Crazy tongue sticking out -->
<path d="M 470 575
Q 488 605 504 595
L 504 640
Q 504 660 522 660
Q 540 660 540 640
L 540 595
Q 555 605 572 575
Q 555 590 540 590
Q 525 590 512 588
Q 499 590 484 590
Q 469 590 470 575 Z"
fill="#ff4477" stroke="#000" stroke-width="4"/>
<line x1="522" y1="595" x2="522" y2="650" stroke="#000" stroke-width="3" opacity="0.4"/>
<!-- Scattered spark dots -->
<g fill="#ffea00">
<circle cx="280" cy="490" r="11"/>
<circle cx="745" cy="540" r="13"/>
<circle cx="430" cy="270" r="9"/>
<circle cx="610" cy="780" r="11"/>
<circle cx="180" cy="610" r="7"/>
<circle cx="860" cy="420" r="7"/>
<circle cx="350" cy="780" r="8"/>
<circle cx="700" cy="240" r="9"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

+34
View File
@@ -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)"
@@ -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 `<root>/bin/oscope-of` (bare) or
/// `<root>/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() {
+27 -8
View File
@@ -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