YrXtals/scripts/macos/package.sh

300 lines
9.7 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Cross-compile + zip distributables from a single macOS host.
#
# Targets: macos-aarch64, macos-x86_64, windows-aarch64, windows-x86_64, linux-aarch64, linux-x86_64.
# Output: dist/YrXtals-<target>.zip per target.
#
# Toolchain prerequisites:
# rustup, zip, codesign
# librsvg, iconutil
# imagemagick, llvm (for the windows .ico embed via build.rs)
# zig, cargo-zigbuild (windows cross-compile)
# docker, cross (linux cross-compile, since cpal pulls alsa-sys)
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOT"
case "$(uname -s)" in
Darwin) ;;
*) echo "package.sh: macOS host only (needs codesign + iconutil for the .app bundle)" >&2; exit 1;;
esac
# mirror .cargo/config.toml's /tmp target dir.
CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-/tmp/yr_crystals-target}"
export CARGO_TARGET_DIR
# raises the per-process file descriptor limit for the linker
ulimit -n 65536 2>/dev/null || ulimit -n 8192 2>/dev/null || true
ALL_TARGETS=(
macos-aarch64
macos-x86_64
windows-aarch64
windows-x86_64
linux-aarch64
linux-x86_64
)
usage() {
cat >&2 <<EOF
usage: cargo xtask package --all
cargo xtask package --target <name> [--target <name> ...]
targets: ${ALL_TARGETS[*]}
EOF
exit 2
}
TARGETS=()
while [ $# -gt 0 ]; do
case "$1" in
--all) TARGETS=("${ALL_TARGETS[@]}"); shift ;;
--target) [ $# -ge 2 ] || usage; TARGETS+=("$2"); shift 2 ;;
-h|--help) usage ;;
*) echo "unknown arg: $1" >&2; usage ;;
esac
done
[ ${#TARGETS[@]} -eq 0 ] && usage
need() { command -v "$1" >/dev/null 2>&1 || { echo "ERROR: $1 not found. $2" >&2; exit 1; }; }
need rustup "install rustup from https://rustup.rs"
need zip "comes with macOS"
NEEDS_ZIG=0
NEEDS_MAC_ICON=0
NEEDS_WIN_ICON=0
NEEDS_CROSS=0
for t in "${TARGETS[@]}"; do
case "$t" in
windows-*) NEEDS_ZIG=1; NEEDS_WIN_ICON=1 ;;
linux-*) NEEDS_CROSS=1 ;;
macos-*) NEEDS_MAC_ICON=1 ;;
esac
done
if [ $NEEDS_ZIG -eq 1 ]; then
need zig "brew install zig"
need cargo-zigbuild "cargo install cargo-zigbuild"
fi
if [ $NEEDS_MAC_ICON -eq 1 ]; then
need iconutil "comes with Xcode Command Line Tools (xcode-select --install)"
fi
if [ $NEEDS_WIN_ICON -eq 1 ]; then
need rsvg-convert "brew install librsvg"
need magick "brew install imagemagick"
# mirrors build.rs's llvm-windres resolution order.
if ! command -v llvm-windres >/dev/null 2>&1 \
&& [ ! -x /opt/homebrew/opt/llvm/bin/llvm-windres ] \
&& [ ! -x /usr/local/opt/llvm/bin/llvm-windres ] \
&& [ ! -x "${LLVM_WINDRES:-}" ]; then
echo "ERROR: llvm-windres not found. brew install llvm (or set LLVM_WINDRES to its absolute path)" >&2
exit 1
fi
fi
if [ $NEEDS_CROSS -eq 1 ]; then
need cross "cargo install cross"
need docker "brew install --cask docker (or rancher, orbstack, etc) and start the daemon"
fi
PKG="$ROOT/build/package"
DIST="$ROOT/dist"
mkdir -p "$PKG" "$DIST"
# shared 256px PNG for the windows + linux zips.
ICON_PNG="$ROOT/build/icon.png"
if [ ! -f "$ICON_PNG" ] || [ "$ROOT/assets/Icon.svg" -nt "$ICON_PNG" ]; then
if command -v rsvg-convert >/dev/null 2>&1 && [ -f "$ROOT/assets/Icon.svg" ]; then
rsvg-convert --width 256 --height 256 "$ROOT/assets/Icon.svg" -o "$ICON_PNG"
fi
fi
ensure_icns() {
local icns="$ROOT/build/AppIcon.icns"
if [ -f "$icns" ] && [ "$ROOT/assets/Icon.svg" -ot "$icns" ]; then return; fi
[ -f "$ROOT/assets/Icon.svg" ] || return 0
command -v rsvg-convert >/dev/null 2>&1 || return 0
local iconset="$ROOT/build/AppIcon.iconset"
rm -rf "$iconset"
mkdir -p "$iconset"
for size in 16 32 64 128 256 512 1024; do
rsvg-convert --width="$size" --height="$size" \
"$ROOT/assets/Icon.svg" -o "$iconset/icon_${size}.png"
done
cp "$iconset/icon_16.png" "$iconset/icon_16x16.png"
cp "$iconset/icon_32.png" "$iconset/icon_16x16@2x.png"
cp "$iconset/icon_32.png" "$iconset/icon_32x32.png"
cp "$iconset/icon_64.png" "$iconset/icon_32x32@2x.png"
cp "$iconset/icon_128.png" "$iconset/icon_128x128.png"
cp "$iconset/icon_256.png" "$iconset/icon_128x128@2x.png"
cp "$iconset/icon_256.png" "$iconset/icon_256x256.png"
cp "$iconset/icon_512.png" "$iconset/icon_256x256@2x.png"
cp "$iconset/icon_512.png" "$iconset/icon_512x512.png"
cp "$iconset/icon_1024.png" "$iconset/icon_512x512@2x.png"
rm -f "$iconset"/icon_16.png "$iconset"/icon_32.png "$iconset"/icon_64.png \
"$iconset"/icon_128.png "$iconset"/icon_256.png "$iconset"/icon_512.png \
"$iconset"/icon_1024.png
iconutil -c icns "$iconset" -o "$icns"
rm -rf "$iconset"
}
zip_target() {
local target="$1" path="$2"
local out="$DIST/YrXtals-${target}.zip"
rm -f "$out"
(cd "$(dirname "$path")" && zip -r -q "$out" "$(basename "$path")")
echo "$out ($(du -h "$out" | cut -f1))"
}
build_macos() {
local arch="$1" rust_target
case "$arch" in
aarch64) rust_target=aarch64-apple-darwin ;;
x86_64) rust_target=x86_64-apple-darwin ;;
esac
rustup target add "$rust_target" >/dev/null 2>&1 || true
ensure_icns
echo "==> macOS $arch ($rust_target)"
export MACOSX_DEPLOYMENT_TARGET=14.0
cargo build --release --bin yr_crystals --target "$rust_target"
local bin="$CARGO_TARGET_DIR/$rust_target/release/yr_crystals"
[ -f "$bin" ] || { echo "ERROR: yr_crystals missing for $rust_target" >&2; exit 1; }
local stage="$PKG/macos-${arch}"
local app="$stage/Yr Xtals.app"
rm -rf "$stage"
mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources"
cp "$ROOT/macos/Info.plist" "$app/Contents/Info.plist"
cp "$bin" "$app/Contents/MacOS/yr_crystals"
[ -f "$ROOT/build/AppIcon.icns" ] && cp "$ROOT/build/AppIcon.icns" "$app/Contents/Resources/AppIcon.icns"
codesign --force --sign - "$app"
zip_target "macos-${arch}" "$app"
}
build_windows() {
local arch="$1" rust_target
case "$arch" in
aarch64) rust_target=aarch64-pc-windows-gnullvm ;;
x86_64) rust_target=x86_64-pc-windows-gnu ;;
esac
rustup target add "$rust_target" >/dev/null 2>&1 || true
echo "==> Windows $arch ($rust_target via cargo-zigbuild)"
cargo zigbuild --release --bin yr_crystals --target "$rust_target"
local stage="$PKG/windows-${arch}/YrXtals"
rm -rf "$stage"
mkdir -p "$stage"
cp "$CARGO_TARGET_DIR/$rust_target/release/yr_crystals.exe" "$stage/YrXtals.exe"
[ -f "$ICON_PNG" ] && cp "$ICON_PNG" "$stage/icon.png"
[ -f "$ROOT/LICENCE" ] && cp "$ROOT/LICENCE" "$stage/LICENCE"
[ -f "$ROOT/README.md" ] && cp "$ROOT/README.md" "$stage/README.md"
zip_target "windows-${arch}" "$stage"
}
build_linux() {
local arch="$1" rust_target
case "$arch" in
aarch64) rust_target=aarch64-unknown-linux-gnu ;;
x86_64) rust_target=x86_64-unknown-linux-gnu ;;
esac
# preinstalls the linux-x86_64 toolchain cross mounts into its amd64-only docker image.
rustup toolchain install stable-x86_64-unknown-linux-gnu --profile minimal --force-non-host >/dev/null 2>&1 || true
echo "==> Linux $arch ($rust_target via cross)"
# selects the amd64 variant of cross's image, the only platform manifest cross 0.2.5 publishes on ghcr.io.
# --target-dir target points the build at cross's mounted project volume, overriding the /tmp path from .cargo/config.toml.
(cd "$ROOT" && DOCKER_DEFAULT_PLATFORM=linux/amd64 cross build --release --bin yr_crystals --target "$rust_target" --target-dir target)
local bin="$ROOT/target/$rust_target/release/yr_crystals"
[ -f "$bin" ] || { echo "ERROR: yr_crystals missing for $rust_target" >&2; exit 1; }
local stage="$PKG/linux-${arch}/yrxtals"
rm -rf "$stage"
mkdir -p "$stage"
cp "$bin" "$stage/YrXtals"
chmod +x "$stage/YrXtals"
[ -f "$ICON_PNG" ] && cp "$ICON_PNG" "$stage/icon.png"
[ -f "$ROOT/LICENCE" ] && cp "$ROOT/LICENCE" "$stage/LICENCE"
[ -f "$ROOT/README.md" ] && cp "$ROOT/README.md" "$stage/README.md"
# self-contained installer bundled into the linux zip.
cat > "$stage/install.sh" <<'INSTALLER_EOF'
#!/usr/bin/env bash
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
BIN_DIR="${XDG_BIN_HOME:-$HOME/.local/bin}"
APP_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
ICON_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor/256x256/apps"
mkdir -p "$BIN_DIR" "$APP_DIR" "$ICON_DIR"
pkill -x YrXtals 2>/dev/null || true
sleep 0.2
install -m 755 "$HERE/YrXtals" "$BIN_DIR/YrXtals"
[ -f "$HERE/icon.png" ] && install -m 644 "$HERE/icon.png" "$ICON_DIR/yrxtals.png"
cat > "$APP_DIR/yrxtals.desktop" <<DESKTOP
[Desktop Entry]
Type=Application
Name=Yr Xtals
Comment=Audio visualizer
Exec=$BIN_DIR/YrXtals
Icon=yrxtals
Terminal=false
Categories=AudioVideo;Audio;
DESKTOP
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database "$APP_DIR" >/dev/null 2>&1 || true
fi
echo "Installed:"
echo " binary → $BIN_DIR/YrXtals"
echo " icon → $ICON_DIR/yrxtals.png"
echo " desktop → $APP_DIR/yrxtals.desktop"
case ":$PATH:" in
*":$BIN_DIR:"*) ;;
*) echo "note: $BIN_DIR is not on your PATH" >&2 ;;
esac
INSTALLER_EOF
chmod +x "$stage/install.sh"
zip_target "linux-${arch}" "$stage"
}
echo "packaging: ${TARGETS[*]}"
echo
for t in "${TARGETS[@]}"; do
case "$t" in
macos-aarch64) build_macos aarch64 ;;
macos-x86_64) build_macos x86_64 ;;
windows-aarch64) build_windows aarch64 ;;
windows-x86_64) build_windows x86_64 ;;
linux-aarch64) build_linux aarch64 ;;
linux-x86_64) build_linux x86_64 ;;
*) echo "unknown target: $t (valid: ${ALL_TARGETS[*]})" >&2; exit 2 ;;
esac
done
echo
echo "done. dist:"
ls -lh "$DIST"