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:
@@ -3,3 +3,5 @@ build/
|
||||
.swiftpm/
|
||||
Package.resolved
|
||||
DerivedData/
|
||||
Resources/AppIcon.iconset/
|
||||
Resources/AppIcon.icns
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 |
Executable
+34
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user