#!/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-.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 < [--target ...] 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" </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"