diff --git a/Cargo.lock b/Cargo.lock index d6400f5..2a07b06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -873,7 +873,7 @@ dependencies = [ "cpal 0.17.1", "crossbeam-channel", "crossterm", - "doux 0.0.14", + "doux", "eframe", "egui", "egui_ratatui", @@ -925,7 +925,7 @@ dependencies = [ "cagire-ratatui", "crossbeam-channel", "crossterm", - "doux 0.0.13", + "doux", "egui_ratatui", "nih_plug", "nih_plug_egui", @@ -1822,22 +1822,6 @@ dependencies = [ "litrs", ] -[[package]] -name = "doux" -version = "0.0.13" -source = "git+https://github.com/sova-org/doux?tag=v0.0.13#b8150d907e4cc2764e82fdaa424df41ceef9b0d2" -dependencies = [ - "arc-swap", - "clap", - "cpal 0.17.1", - "crossbeam-channel", - "ringbuf", - "rosc", - "rustyline", - "soundfont", - "symphonia", -] - [[package]] name = "doux" version = "0.0.14" diff --git a/plugins/cagire-plugins/Cargo.toml b/plugins/cagire-plugins/Cargo.toml index c7dba50..2fe055f 100644 --- a/plugins/cagire-plugins/Cargo.toml +++ b/plugins/cagire-plugins/Cargo.toml @@ -14,7 +14,7 @@ cagire = { path = "../..", default-features = false, features = ["block-renderer cagire-forth = { path = "../../crates/forth" } cagire-project = { path = "../../crates/project" } cagire-ratatui = { path = "../../crates/ratatui" } -doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.13", features = ["native", "soundfont"] } +doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.14", features = ["native", "soundfont"] } nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", features = ["standalone"] } nih_plug_egui = { git = "https://github.com/robbert-vdh/nih-plug" } egui_ratatui = "2.1" diff --git a/plugins/cagire-plugins/src/lib.rs b/plugins/cagire-plugins/src/lib.rs index 1e54325..2cbb676 100644 --- a/plugins/cagire-plugins/src/lib.rs +++ b/plugins/cagire-plugins/src/lib.rs @@ -185,6 +185,7 @@ impl Plugin for CagirePlugin { self.sample_rate, self.output_channels, 64, + buffer_config.max_buffer_size as usize, ); self.bridge .sample_registry diff --git a/scripts/__pycache__/build.cpython-313.pyc b/scripts/__pycache__/build.cpython-313.pyc new file mode 100644 index 0000000..4bbb956 Binary files /dev/null and b/scripts/__pycache__/build.cpython-313.pyc differ diff --git a/scripts/__pycache__/build.cpython-314.pyc b/scripts/__pycache__/build.cpython-314.pyc new file mode 100644 index 0000000..e3c1119 Binary files /dev/null and b/scripts/__pycache__/build.cpython-314.pyc differ diff --git a/scripts/build-all.sh b/scripts/build-all.sh deleted file mode 100755 index 55e8d0e..0000000 --- a/scripts/build-all.sh +++ /dev/null @@ -1,473 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -export MACOSX_DEPLOYMENT_TARGET="12.0" - -cd "$(git rev-parse --show-toplevel)" - -PLUGIN_NAME="cagire-plugins" -LIB_NAME="cagire_plugins" # cargo converts hyphens to underscores -OUT="releases" - -PLATFORMS=( - "aarch64-apple-darwin" - "x86_64-apple-darwin" - "x86_64-unknown-linux-gnu" - "aarch64-unknown-linux-gnu" - "x86_64-pc-windows-gnu" -) - -PLATFORM_LABELS=( - "macOS aarch64 (native)" - "macOS x86_64 (native)" - "Linux x86_64 (cross)" - "Linux aarch64 (cross)" - "Windows x86_64 (cross)" -) - -PLATFORM_ALIASES=( - "macos-arm64" - "macos-x86_64" - "linux-x86_64" - "linux-aarch64" - "windows-x86_64" -) - -# --- CLI argument parsing --- - -cli_platforms="" -cli_targets="" -cli_yes=false -cli_all=false - -while [[ $# -gt 0 ]]; do - case "$1" in - --platforms) cli_platforms="$2"; shift 2 ;; - --targets) cli_targets="$2"; shift 2 ;; - --yes) cli_yes=true; shift ;; - --all) cli_all=true; shift ;; - -h|--help) - echo "Usage: $0 [OPTIONS]" - echo "" - echo "Options:" - echo " --platforms Comma-separated: macos-arm64,macos-x86_64,linux-x86_64,linux-aarch64,windows-x86_64" - echo " --targets Comma-separated: cli,desktop,plugins" - echo " --all Build all platforms and targets" - echo " --yes Skip confirmation prompt" - echo "" - echo "Without options, runs interactively." - exit 0 - ;; - *) echo "Unknown option: $1"; exit 1 ;; - esac -done - -resolve_platform_alias() { - local alias="$1" - for i in "${!PLATFORM_ALIASES[@]}"; do - if [[ "${PLATFORM_ALIASES[$i]}" == "$alias" ]]; then - echo "$i" - return - fi - done - echo "Unknown platform: $alias" >&2 - exit 1 -} - -# --- Helpers --- - -prompt_platforms() { - echo "Select platform (0=all, comma-separated):" - echo " 0) All" - for i in "${!PLATFORMS[@]}"; do - echo " $((i+1))) ${PLATFORM_LABELS[$i]}" - done - read -rp "> " choice - - if [[ "$choice" == "0" || -z "$choice" ]]; then - selected_platforms=("${PLATFORMS[@]}") - selected_labels=("${PLATFORM_LABELS[@]}") - else - IFS=',' read -ra indices <<< "$choice" - selected_platforms=() - selected_labels=() - for idx in "${indices[@]}"; do - idx="${idx// /}" - idx=$((idx - 1)) - if (( idx < 0 || idx >= ${#PLATFORMS[@]} )); then - echo "Invalid platform index: $((idx+1))" - exit 1 - fi - selected_platforms+=("${PLATFORMS[$idx]}") - selected_labels+=("${PLATFORM_LABELS[$idx]}") - done - fi -} - -prompt_targets() { - echo "" - echo "Select targets (0=all, comma-separated):" - echo " 0) All" - echo " 1) cagire" - echo " 2) cagire-desktop" - echo " 3) cagire-plugins (CLAP/VST3)" - read -rp "> " choice - - build_cagire=false - build_desktop=false - build_plugins=false - - if [[ "$choice" == "0" || -z "$choice" ]]; then - build_cagire=true - build_desktop=true - build_plugins=true - else - IFS=',' read -ra targets <<< "$choice" - for t in "${targets[@]}"; do - t="${t// /}" - case "$t" in - 1) build_cagire=true ;; - 2) build_desktop=true ;; - 3) build_plugins=true ;; - *) echo "Invalid target: $t"; exit 1 ;; - esac - done - fi -} - -confirm_summary() { - echo "" - echo "=== Build Summary ===" - echo "" - echo "Platforms:" - for label in "${selected_labels[@]}"; do - echo " - $label" - done - echo "" - echo "Targets:" - $build_cagire && echo " - cagire" - $build_desktop && echo " - cagire-desktop" - $build_plugins && echo " - cagire-plugins (CLAP/VST3)" - echo "" - read -rp "Proceed? [Y/n] " yn - case "${yn,,}" in - n|no) echo "Aborted."; exit 0 ;; - esac -} - -platform_os() { - case "$1" in - *windows*) echo "windows" ;; - *linux*) echo "linux" ;; - *apple*) echo "macos" ;; - esac -} - -platform_arch() { - case "$1" in - aarch64*) echo "aarch64" ;; - x86_64*) echo "x86_64" ;; - esac -} - -platform_suffix() { - case "$1" in - *windows*) echo ".exe" ;; - *) echo "" ;; - esac -} - -is_cross_target() { - case "$1" in - *linux*|*windows*) return 0 ;; - *) return 1 ;; - esac -} - -native_target() { - [[ "$1" == "aarch64-apple-darwin" ]] -} - -release_dir() { - if native_target "$1"; then - echo "target/release" - else - echo "target/$1/release" - fi -} - -target_flag() { - if native_target "$1"; then - echo "" - else - echo "--target $1" - fi -} - -builder_for() { - if is_cross_target "$1"; then - echo "cross" - else - echo "cargo" - fi -} - -build_binary() { - local platform="$1" - shift - local builder - builder=$(builder_for "$platform") - local tf - tf=$(target_flag "$platform") - # shellcheck disable=SC2086 - $builder build --release $tf "$@" -} - -bundle_plugins_native() { - local platform="$1" - local tf - tf=$(target_flag "$platform") - # shellcheck disable=SC2086 - cargo xtask bundle "$PLUGIN_NAME" --release $tf -} - -bundle_desktop_native() { - local platform="$1" - local tf - tf=$(target_flag "$platform") - # shellcheck disable=SC2086 - cargo bundle --release --features desktop --bin cagire-desktop $tf -} - -bundle_plugins_cross() { - local platform="$1" - local rd - rd=$(release_dir "$platform") - local os - os=$(platform_os "$platform") - local arch - arch=$(platform_arch "$platform") - - # Build the cdylib with cross - # shellcheck disable=SC2046 - build_binary "$platform" -p "$PLUGIN_NAME" - - # Determine source library file - local src_lib - case "$os" in - linux) src_lib="$rd/lib${LIB_NAME}.so" ;; - windows) src_lib="$rd/${LIB_NAME}.dll" ;; - esac - - if [[ ! -f "$src_lib" ]]; then - echo " ERROR: Expected library not found: $src_lib" - return 1 - fi - - # Assemble CLAP bundle (flat file) - local clap_out="$OUT/${PLUGIN_NAME}-${os}-${arch}.clap" - cp "$src_lib" "$clap_out" - echo " CLAP -> $clap_out" - - # Assemble VST3 bundle (directory tree) - local vst3_dir="$OUT/${PLUGIN_NAME}-${os}-${arch}.vst3" - local vst3_contents - case "$os" in - linux) - vst3_contents="$vst3_dir/Contents/${arch}-linux" - mkdir -p "$vst3_contents" - cp "$src_lib" "$vst3_contents/${PLUGIN_NAME}.so" - ;; - windows) - vst3_contents="$vst3_dir/Contents/${arch}-win" - mkdir -p "$vst3_contents" - cp "$src_lib" "$vst3_contents/${PLUGIN_NAME}.vst3" - ;; - esac - echo " VST3 -> $vst3_dir/" -} - -copy_artifacts() { - local platform="$1" - local rd - rd=$(release_dir "$platform") - local os - os=$(platform_os "$platform") - local arch - arch=$(platform_arch "$platform") - local suffix - suffix=$(platform_suffix "$platform") - - if $build_cagire; then - local src="$rd/cagire${suffix}" - local dst="$OUT/cagire-${os}-${arch}${suffix}" - cp "$src" "$dst" - echo " cagire -> $dst" - fi - - if $build_desktop; then - local src="$rd/cagire-desktop${suffix}" - local dst="$OUT/cagire-desktop-${os}-${arch}${suffix}" - cp "$src" "$dst" - echo " cagire-desktop -> $dst" - - # macOS .app bundle - if [[ "$os" == "macos" ]]; then - local app_src="$rd/bundle/osx/Cagire.app" - if [[ ! -d "$app_src" ]]; then - echo " ERROR: .app bundle not found at $app_src" - echo " Did 'cargo bundle' succeed?" - return 1 - fi - local app_dst="$OUT/Cagire-${arch}.app" - rm -rf "$app_dst" - cp -R "$app_src" "$app_dst" - echo " Cagire.app -> $app_dst" - scripts/make-dmg.sh "$app_dst" "$OUT" - fi - fi - - # NSIS installer for Windows targets - if [[ "$os" == "windows" ]] && command -v makensis &>/dev/null; then - echo " Building NSIS installer..." - local version - version=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/') - local abs_root - abs_root=$(pwd) - makensis -DVERSION="$version" \ - -DCLI_EXE="$abs_root/$rd/cagire.exe" \ - -DDESKTOP_EXE="$abs_root/$rd/cagire-desktop.exe" \ - -DICON="$abs_root/assets/Cagire.ico" \ - -DOUTDIR="$abs_root/$OUT" \ - nsis/cagire.nsi - echo " Installer -> $OUT/cagire-${version}-windows-x86_64-setup.exe" - fi - - # AppImage for Linux targets - if [[ "$os" == "linux" ]]; then - if $build_cagire; then - scripts/make-appimage.sh "$rd/cagire" "$arch" "$OUT" - fi - if $build_desktop; then - scripts/make-appimage.sh "$rd/cagire-desktop" "$arch" "$OUT" - fi - fi - - # Plugin artifacts for native targets (cross handled in bundle_plugins_cross) - if $build_plugins && ! is_cross_target "$platform"; then - local bundle_dir="target/bundled" - - # CLAP - local clap_src="$bundle_dir/${PLUGIN_NAME}.clap" - if [[ -e "$clap_src" ]]; then - local clap_dst="$OUT/${PLUGIN_NAME}-${os}-${arch}.clap" - cp -r "$clap_src" "$clap_dst" - echo " CLAP -> $clap_dst" - fi - - # VST3 - local vst3_src="$bundle_dir/${PLUGIN_NAME}.vst3" - if [[ -d "$vst3_src" ]]; then - local vst3_dst="$OUT/${PLUGIN_NAME}-${os}-${arch}.vst3" - rm -rf "$vst3_dst" - cp -r "$vst3_src" "$vst3_dst" - echo " VST3 -> $vst3_dst/" - fi - fi -} - -# --- Main --- - -if $cli_all; then - selected_platforms=("${PLATFORMS[@]}") - selected_labels=("${PLATFORM_LABELS[@]}") - build_cagire=true - build_desktop=true - build_plugins=true -elif [[ -n "$cli_platforms" || -n "$cli_targets" ]]; then - # Resolve platforms from CLI - if [[ -n "$cli_platforms" ]]; then - selected_platforms=() - selected_labels=() - IFS=',' read -ra aliases <<< "$cli_platforms" - for alias in "${aliases[@]}"; do - alias="${alias// /}" - idx=$(resolve_platform_alias "$alias") - selected_platforms+=("${PLATFORMS[$idx]}") - selected_labels+=("${PLATFORM_LABELS[$idx]}") - done - else - selected_platforms=("${PLATFORMS[@]}") - selected_labels=("${PLATFORM_LABELS[@]}") - fi - - # Resolve targets from CLI - build_cagire=false - build_desktop=false - build_plugins=false - if [[ -n "$cli_targets" ]]; then - IFS=',' read -ra tgts <<< "$cli_targets" - for t in "${tgts[@]}"; do - t="${t// /}" - case "$t" in - cli) build_cagire=true ;; - desktop) build_desktop=true ;; - plugins) build_plugins=true ;; - *) echo "Unknown target: $t (expected: cli, desktop, plugins)"; exit 1 ;; - esac - done - else - build_cagire=true - build_desktop=true - build_plugins=true - fi -else - prompt_platforms - prompt_targets -fi - -if ! $cli_yes && [[ -z "$cli_platforms" ]] && ! $cli_all; then - confirm_summary -fi - -mkdir -p "$OUT" - -step=0 -total=${#selected_platforms[@]} - -for platform in "${selected_platforms[@]}"; do - step=$((step + 1)) - echo "" - echo "=== [$step/$total] $platform ===" - - if $build_cagire; then - echo " -> cagire" - build_binary "$platform" - fi - - if $build_desktop; then - echo " -> cagire-desktop" - build_binary "$platform" --features desktop --bin cagire-desktop - if ! is_cross_target "$platform"; then - echo " -> bundling cagire-desktop .app" - bundle_desktop_native "$platform" - fi - fi - - if $build_plugins; then - echo " -> cagire-plugins" - if is_cross_target "$platform"; then - bundle_plugins_cross "$platform" - else - bundle_plugins_native "$platform" - fi - fi - - echo " Copying artifacts..." - copy_artifacts "$platform" -done - -echo "" -echo "=== Done ===" -echo "" -ls -lhR "$OUT/" diff --git a/scripts/build.py b/scripts/build.py new file mode 100755 index 0000000..56cc7c5 --- /dev/null +++ b/scripts/build.py @@ -0,0 +1,949 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.11" +# dependencies = ["rich>=13.0", "questionary>=2.0"] +# /// +"""Cagire release builder — replaces build-all.sh, make-dmg.sh, make-appimage.sh.""" + +from __future__ import annotations + +import argparse +import hashlib +import os +import platform +import shutil +import subprocess +import sys +import tempfile +import threading +import time +import tomllib +from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass, field +from pathlib import Path + +from rich.console import Console +from rich.layout import Layout +from rich.live import Live +from rich.panel import Panel +from rich.progress_bar import ProgressBar +from rich.table import Table +from rich.text import Text + +import questionary + +console = Console() + +# --------------------------------------------------------------------------- +# Build progress tracking (shared between threads) +# --------------------------------------------------------------------------- + +_progress_lock = threading.Lock() +_build_progress: dict[str, tuple[str, int]] = {} # alias -> (phase, step) +_build_logs: list[tuple[str, str]] = [] # (alias, line) + + +def _update_phase(alias: str, phase: str, step: int) -> None: + with _progress_lock: + _build_progress[alias] = (phase, step) + + +class BuildLog(list): + """A list that also feeds lines into the shared log buffer.""" + + def __init__(self, alias: str): + super().__init__() + self._alias = alias + + def append(self, line: str) -> None: + super().append(line) + with _progress_lock: + _build_logs.append((self._alias, line)) + +# --------------------------------------------------------------------------- +# Data structures +# --------------------------------------------------------------------------- + +PLUGIN_NAME = "cagire-plugins" +LIB_NAME = "cagire_plugins" +OUT = "releases" + + +@dataclass(frozen=True) +class Platform: + triple: str + label: str + alias: str + os: str + arch: str + cross: bool + native: bool + + +def _parse_triple(triple: str) -> Platform: + """Derive a full Platform from a Rust target triple.""" + parts = triple.split("-") + arch = parts[0] + if "apple" in triple: + os_name, cross = "macos", False + elif "linux" in triple: + os_name, cross = "linux", True + elif "windows" in triple: + os_name, cross = "windows", True + else: + raise ValueError(f"Unknown OS in triple: {triple}") + host_arch = platform.machine() + host_triple_arch = "aarch64" if host_arch == "arm64" else host_arch + native = (os_name == "macos" and arch == host_triple_arch and platform.system() == "Darwin") \ + or (os_name == "linux" and arch == host_triple_arch and platform.system() == "Linux") + mode = "native" if native else "cross" if cross else "native" + alias_arch = "arm64" if (os_name == "macos" and arch == "aarch64") else arch + alias = f"{os_name}-{alias_arch}" + label = f"{'macOS' if os_name == 'macos' else os_name.capitalize()} {arch} ({mode})" + return Platform(triple, label, alias, os_name, arch, cross, native) + + +def load_platforms(root: Path) -> list[Platform]: + """Load platform definitions from scripts/platforms.toml.""" + with open(root / "scripts" / "platforms.toml", "rb") as f: + data = tomllib.load(f) + return [_parse_triple(t) for t in data["triples"]] + + +@dataclass +class BuildConfig: + cli: bool = True + desktop: bool = True + plugins: bool = True + + +@dataclass +class PlatformResult: + platform: Platform + success: bool + elapsed: float + artifacts: list[str] = field(default_factory=list) + log_lines: list[str] = field(default_factory=list) + error: str | None = None + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def get_version(root: Path) -> str: + with open(root / "Cargo.toml", "rb") as f: + cargo = tomllib.load(f) + return cargo["workspace"]["package"]["version"] + + +def builder_for(p: Platform) -> str: + return "cross" if p.cross else "cargo" + + +def release_dir(root: Path, p: Platform) -> Path: + if p.native: + return root / "target" / "release" + return root / "target" / p.triple / "release" + + +def target_flags(p: Platform) -> list[str]: + if p.native: + return [] + return ["--target", p.triple] + + +def suffix_for(p: Platform) -> str: + return ".exe" if p.os == "windows" else "" + + +def run_cmd( + cmd: list[str], log: list[str], env: dict[str, str] | None = None, + input: str | None = None, cwd: Path | None = None, +) -> None: + log.append(f" $ {' '.join(cmd)}") + merged_env = {**os.environ, **(env or {})} + proc = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, env=merged_env, + stdin=subprocess.PIPE if input else subprocess.DEVNULL, + cwd=cwd, + ) + if input: + proc.stdin.write(input) + proc.stdin.close() + for line in proc.stdout: + log.append(f" {line.rstrip()}") + rc = proc.wait() + if rc != 0: + raise subprocess.CalledProcessError(rc, cmd) + + +# --------------------------------------------------------------------------- +# Build functions +# --------------------------------------------------------------------------- + +def _macos_env(p: Platform) -> dict[str, str] | None: + if p.os == "macos": + return {"MACOSX_DEPLOYMENT_TARGET": "12.0"} + return None + + +def build_binary(root: Path, p: Platform, log: list[str], extra_args: list[str] | None = None) -> None: + cmd = [builder_for(p), "build", "--release", *target_flags(p), *(extra_args or [])] + log.append(f" Building: {' '.join(extra_args or ['default'])}") + run_cmd(cmd, log, env=_macos_env(p)) + + +def bundle_plugins(root: Path, p: Platform, log: list[str]) -> None: + if p.cross: + _bundle_plugins_cross(root, p, log) + else: + _bundle_plugins_native(root, p, log) + + +def _bundle_plugins_native(root: Path, p: Platform, log: list[str]) -> None: + log.append(" Bundling plugins (native xtask)") + cmd = ["cargo", "xtask", "bundle", PLUGIN_NAME, "--release", *target_flags(p)] + run_cmd(cmd, log, env=_macos_env(p)) + + +def _bundle_plugins_cross(root: Path, p: Platform, log: list[str]) -> None: + log.append(" Bundling plugins (cross)") + build_binary(root, p, log, extra_args=["-p", PLUGIN_NAME]) + + rd = release_dir(root, p) + if p.os == "linux": + src_lib = rd / f"lib{LIB_NAME}.so" + elif p.os == "windows": + src_lib = rd / f"{LIB_NAME}.dll" + else: + raise RuntimeError(f"Unexpected cross OS: {p.os}") + + if not src_lib.exists(): + raise FileNotFoundError(f"Expected library not found: {src_lib}") + + out = root / OUT + + # CLAP — flat file + clap_dst = out / f"{PLUGIN_NAME}-{p.os}-{p.arch}.clap" + shutil.copy2(src_lib, clap_dst) + log.append(f" CLAP -> {clap_dst}") + + # VST3 — directory tree + vst3_dir = out / f"{PLUGIN_NAME}-{p.os}-{p.arch}.vst3" + if p.os == "linux": + contents = vst3_dir / "Contents" / f"{p.arch}-linux" + contents.mkdir(parents=True, exist_ok=True) + shutil.copy2(src_lib, contents / f"{PLUGIN_NAME}.so") + elif p.os == "windows": + contents = vst3_dir / "Contents" / f"{p.arch}-win" + contents.mkdir(parents=True, exist_ok=True) + shutil.copy2(src_lib, contents / f"{PLUGIN_NAME}.vst3") + log.append(f" VST3 -> {vst3_dir}/") + + +def bundle_desktop_app(root: Path, p: Platform, log: list[str]) -> None: + if p.cross: + return + log.append(" Bundling desktop .app") + cmd = ["cargo", "bundle", "--release", "--features", "desktop", "--bin", "cagire-desktop", *target_flags(p)] + run_cmd(cmd, log, env=_macos_env(p)) + + +# --------------------------------------------------------------------------- +# Packaging: DMG +# --------------------------------------------------------------------------- + + +def make_dmg(root: Path, app_path: Path, arch: str, output_dir: Path, log: list[str]) -> str | None: + log.append(f" Building DMG for {app_path.name}") + + binary = app_path / "Contents" / "MacOS" / "cagire-desktop" + result = subprocess.run(["lipo", "-info", str(binary)], capture_output=True, text=True) + if result.returncode != 0: + log.append(f" ERROR: lipo failed on {binary}") + return None + + lipo_out = result.stdout.strip() + if "Architectures in the fat file" in lipo_out: + dmg_arch = "universal" + else: + raw_arch = lipo_out.split()[-1] + dmg_arch = "aarch64" if raw_arch == "arm64" else raw_arch + + staging = Path(tempfile.mkdtemp()) + try: + shutil.copytree(app_path, staging / "Cagire.app") + (staging / "Applications").symlink_to("/Applications") + readme = root / "assets" / "DMG-README.txt" + if readme.exists(): + shutil.copy2(readme, staging / "README.txt") + + dmg_name = f"Cagire-{dmg_arch}.dmg" + output_dir.mkdir(parents=True, exist_ok=True) + dmg_path = output_dir / dmg_name + + run_cmd([ + "hdiutil", "create", + "-volname", "Cagire", + "-srcfolder", str(staging), + "-ov", "-format", "UDZO", + str(dmg_path), + ], log) + log.append(f" DMG -> {dmg_path}") + return str(dmg_path) + finally: + shutil.rmtree(staging, ignore_errors=True) + + +# --------------------------------------------------------------------------- +# Packaging: AppImage +# --------------------------------------------------------------------------- + +APPRUN_SCRIPT = """\ +#!/bin/sh +SELF="$(readlink -f "$0")" +HERE="$(dirname "$SELF")" +exec "$HERE/usr/bin/cagire" "$@" +""" + + +def _build_appdir(root: Path, binary: Path, appdir: Path) -> None: + (appdir / "usr" / "bin").mkdir(parents=True) + shutil.copy2(binary, appdir / "usr" / "bin" / "cagire") + (appdir / "usr" / "bin" / "cagire").chmod(0o755) + + icons = appdir / "usr" / "share" / "icons" / "hicolor" / "512x512" / "apps" + icons.mkdir(parents=True) + shutil.copy2(root / "assets" / "Cagire.png", icons / "cagire.png") + shutil.copy2(root / "assets" / "cagire.desktop", appdir / "cagire.desktop") + + apprun = appdir / "AppRun" + apprun.write_text(APPRUN_SCRIPT) + apprun.chmod(0o755) + + (appdir / "cagire.png").symlink_to("usr/share/icons/hicolor/512x512/apps/cagire.png") + + +def _download_if_missing(url: str, dest: Path, log: list[str]) -> None: + if dest.exists(): + return + dest.parent.mkdir(parents=True, exist_ok=True) + log.append(f" Downloading {url}") + run_cmd(["curl", "-fSL", url, "-o", str(dest)], log) + + +def _make_appimage_native(root: Path, binary: Path, arch: str, output_dir: Path, log: list[str]) -> str: + cache = root / ".cache" + runtime = cache / f"runtime-{arch}" + _download_if_missing( + f"https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-{arch}", + runtime, log, + ) + + linuxdeploy = cache / f"linuxdeploy-{arch}.AppImage" + _download_if_missing( + f"https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-{arch}.AppImage", + linuxdeploy, log, + ) + linuxdeploy.chmod(0o755) + + appdir = Path(tempfile.mkdtemp()) / "AppDir" + _build_appdir(root, binary, appdir) + + app_name = binary.name + env = {"ARCH": arch, "LDAI_RUNTIME_FILE": str(runtime)} + run_cmd([ + str(linuxdeploy), + "--appimage-extract-and-run", + "--appdir", str(appdir), + "--desktop-file", str(appdir / "cagire.desktop"), + "--icon-file", str(appdir / "usr" / "share" / "icons" / "hicolor" / "512x512" / "apps" / "cagire.png"), + "--output", "appimage", + ], log, env=env, cwd=root) + + # linuxdeploy drops the AppImage in cwd + candidates = sorted(root.glob("*.AppImage"), key=lambda p: p.stat().st_mtime, reverse=True) + if not candidates: + raise FileNotFoundError("No AppImage produced by linuxdeploy") + + output_dir.mkdir(parents=True, exist_ok=True) + final = output_dir / f"{app_name}-linux-{arch}.AppImage" + shutil.move(str(candidates[0]), final) + log.append(f" AppImage -> {final}") + return str(final) + + +def _make_appimage_docker(root: Path, binary: Path, arch: str, output_dir: Path, log: list[str]) -> str: + cache = root / ".cache" + runtime = cache / f"runtime-{arch}" + _download_if_missing( + f"https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-{arch}", + runtime, log, + ) + + appdir = Path(tempfile.mkdtemp()) / "AppDir" + _build_appdir(root, binary, appdir) + + docker_platform = "linux/amd64" if arch == "x86_64" else "linux/arm64" + image_tag = f"cagire-appimage-{arch}" + + log.append(f" Building Docker image {image_tag} ({docker_platform})") + dockerfile = "FROM ubuntu:22.04\nRUN apt-get update && apt-get install -y --no-install-recommends squashfs-tools && rm -rf /var/lib/apt/lists/*\n" + run_cmd([ + "docker", "build", "--platform", docker_platform, "-q", "-t", image_tag, "-", + ], log, input=dockerfile) + + squashfs = cache / f"appimage-{arch}.squashfs" + run_cmd([ + "docker", "run", "--rm", "--platform", docker_platform, + "-v", f"{appdir}:/appdir:ro", + "-v", f"{cache}:/cache", + image_tag, + "mksquashfs", "/appdir", f"/cache/appimage-{arch}.squashfs", + "-root-owned", "-noappend", "-comp", "gzip", "-no-progress", + ], log) + + output_dir.mkdir(parents=True, exist_ok=True) + app_name = binary.name + final = output_dir / f"{app_name}-linux-{arch}.AppImage" + with open(final, "wb") as out_f: + out_f.write(runtime.read_bytes()) + out_f.write(squashfs.read_bytes()) + final.chmod(0o755) + squashfs.unlink(missing_ok=True) + log.append(f" AppImage -> {final}") + return str(final) + + +def make_appimage(root: Path, binary: Path, arch: str, output_dir: Path, log: list[str]) -> str: + log.append(f" Building AppImage for {binary.name} ({arch})") + host_arch = platform.machine() + if host_arch == arch and platform.system() == "Linux": + return _make_appimage_native(root, binary, arch, output_dir, log) + return _make_appimage_docker(root, binary, arch, output_dir, log) + + +# --------------------------------------------------------------------------- +# Packaging: NSIS +# --------------------------------------------------------------------------- + + +def make_nsis(root: Path, rd: Path, version: str, output_dir: Path, log: list[str]) -> str | None: + if not shutil.which("makensis"): + log.append(" makensis not found, skipping NSIS installer") + return None + + log.append(" Building NSIS installer") + abs_root = str(root.resolve()) + run_cmd([ + "makensis", + f"-DVERSION={version}", + f"-DCLI_EXE={abs_root}/{rd.relative_to(root)}/cagire.exe", + f"-DDESKTOP_EXE={abs_root}/{rd.relative_to(root)}/cagire-desktop.exe", + f"-DICON={abs_root}/assets/Cagire.ico", + f"-DOUTDIR={abs_root}/{OUT}", + str(root / "nsis" / "cagire.nsi"), + ], log) + + installer = f"cagire-{version}-windows-x86_64-setup.exe" + log.append(f" Installer -> {output_dir / installer}") + return str(output_dir / installer) + + +# --------------------------------------------------------------------------- +# Artifact copying & packaging dispatch +# --------------------------------------------------------------------------- + + +def copy_artifacts(root: Path, p: Platform, config: BuildConfig, log: list[str]) -> list[str]: + rd = release_dir(root, p) + out = root / OUT + sx = suffix_for(p) + version = get_version(root) + artifacts: list[str] = [] + + if config.cli: + src = rd / f"cagire{sx}" + dst = out / f"cagire-{p.os}-{p.arch}{sx}" + shutil.copy2(src, dst) + log.append(f" cagire -> {dst}") + artifacts.append(str(dst)) + + if config.desktop: + src = rd / f"cagire-desktop{sx}" + dst = out / f"cagire-desktop-{p.os}-{p.arch}{sx}" + shutil.copy2(src, dst) + log.append(f" cagire-desktop -> {dst}") + artifacts.append(str(dst)) + + if p.os == "macos": + app_src = rd / "bundle" / "osx" / "Cagire.app" + if not app_src.is_dir(): + raise FileNotFoundError(f".app bundle not found at {app_src}") + app_dst = out / f"Cagire-{p.arch}.app" + if app_dst.exists(): + shutil.rmtree(app_dst) + shutil.copytree(app_src, app_dst) + log.append(f" Cagire.app -> {app_dst}") + artifacts.append(str(app_dst)) + + dmg = make_dmg(root, app_dst, p.arch, out, log) + if dmg: + artifacts.append(dmg) + + if p.os == "windows": + nsis = make_nsis(root, rd, version, out, log) + if nsis: + artifacts.append(nsis) + + if p.os == "linux": + if config.cli: + ai = make_appimage(root, rd / "cagire", p.arch, out, log) + artifacts.append(ai) + if config.desktop: + ai = make_appimage(root, rd / "cagire-desktop", p.arch, out, log) + artifacts.append(ai) + + if config.plugins and not p.cross: + bundle_dir = root / "target" / "bundled" + clap_src = bundle_dir / f"{PLUGIN_NAME}.clap" + if clap_src.exists(): + clap_dst = out / f"{PLUGIN_NAME}-{p.os}-{p.arch}.clap" + if clap_src.is_dir(): + if clap_dst.exists(): + shutil.rmtree(clap_dst) + shutil.copytree(clap_src, clap_dst) + else: + shutil.copy2(clap_src, clap_dst) + log.append(f" CLAP -> {clap_dst}") + artifacts.append(str(clap_dst)) + + vst3_src = bundle_dir / f"{PLUGIN_NAME}.vst3" + if vst3_src.is_dir(): + vst3_dst = out / f"{PLUGIN_NAME}-{p.os}-{p.arch}.vst3" + if vst3_dst.exists(): + shutil.rmtree(vst3_dst) + shutil.copytree(vst3_src, vst3_dst) + log.append(f" VST3 -> {vst3_dst}/") + artifacts.append(str(vst3_dst)) + + return artifacts + + +# --------------------------------------------------------------------------- +# Per-platform orchestration +# --------------------------------------------------------------------------- + + +def _count_phases(p: Platform, config: BuildConfig) -> int: + n = 0 + if config.cli: + n += 1 + if config.desktop: + n += 1 + if not p.cross: + n += 1 # bundle .app + if config.plugins: + n += 1 + n += 1 # copy artifacts / packaging + return n + + +def build_platform(root: Path, p: Platform, config: BuildConfig) -> PlatformResult: + log = BuildLog(p.alias) + t0 = time.monotonic() + step = 0 + try: + if config.cli: + _update_phase(p.alias, "compiling cli", step) + build_binary(root, p, log) + step += 1 + + if config.desktop: + _update_phase(p.alias, "compiling desktop", step) + build_binary(root, p, log, extra_args=["--features", "desktop", "--bin", "cagire-desktop"]) + step += 1 + if not p.cross: + _update_phase(p.alias, "bundling .app", step) + bundle_desktop_app(root, p, log) + step += 1 + + if config.plugins: + _update_phase(p.alias, "bundling plugins", step) + bundle_plugins(root, p, log) + step += 1 + + _update_phase(p.alias, "packaging", step) + log.append(" Copying artifacts...") + artifacts = copy_artifacts(root, p, config, log) + + elapsed = time.monotonic() - t0 + return PlatformResult(p, True, elapsed, artifacts, log) + + except Exception as e: + elapsed = time.monotonic() - t0 + log.append(f" ERROR: {e}") + return PlatformResult(p, False, elapsed, [], log, str(e)) + + +def _build_display( + platforms: list[Platform], + config: BuildConfig, + completed: dict[str, PlatformResult], + start_times: dict[str, float], + log_max_lines: int, +) -> Layout: + layout = Layout() + status_height = len(platforms) + 4 + layout.split_column( + Layout(name="status", size=status_height), + Layout(name="logs"), + ) + + table = Table(padding=(0, 1), expand=True) + table.add_column("Platform", style="cyan", min_width=28, no_wrap=True) + table.add_column("Phase", min_width=20, no_wrap=True) + table.add_column("Progress", min_width=22, no_wrap=True) + table.add_column("Time", justify="right", min_width=6, no_wrap=True) + + with _progress_lock: + progress_snapshot = dict(_build_progress) + + for p in platforms: + alias = p.alias + total = _count_phases(p, config) + + if alias in completed: + r = completed[alias] + if r.success: + n = len(r.artifacts) + phase = Text(f"OK ({n} artifacts)", style="green") + bar = ProgressBar(total=total, completed=total, width=20, complete_style="green") + else: + phase = Text(f"FAIL", style="red") + bar = ProgressBar(total=total, completed=total, width=20, complete_style="red") + elapsed = f"{r.elapsed:.0f}s" + elif alias in progress_snapshot: + ph, step = progress_snapshot[alias] + phase = Text(ph, style="yellow") + bar = ProgressBar(total=total, completed=step, width=20) + elapsed = f"{time.monotonic() - start_times.get(alias, time.monotonic()):.0f}s" + else: + phase = Text("waiting", style="dim") + bar = ProgressBar(total=total, completed=0, width=20) + elapsed = "" + + table.add_row(p.label, phase, bar, elapsed) + + layout["status"].update(Panel(table, title="[bold blue]Build Progress[/]", border_style="blue")) + + with _progress_lock: + recent = _build_logs[-log_max_lines:] + + if recent: + lines: list[str] = [] + for alias, line in recent: + short = alias.split("-")[0][:3] + lines.append(f"[dim]{short}[/] {line.rstrip()}") + log_text = "\n".join(lines) + else: + log_text = "[dim]waiting for output...[/]" + + layout["logs"].update(Panel(log_text, title="[bold]Build Output[/]", border_style="dim")) + + return layout + + +def run_builds( + root: Path, platforms: list[Platform], config: BuildConfig, version: str, verbose: bool = False, +) -> list[PlatformResult]: + (root / OUT).mkdir(parents=True, exist_ok=True) + + _build_progress.clear() + _build_logs.clear() + for p in platforms: + _update_phase(p.alias, "waiting", 0) + + results: list[PlatformResult] = [] + completed: dict[str, PlatformResult] = {} + start_times: dict[str, float] = {p.alias: time.monotonic() for p in platforms} + + term_height = console.size.height + log_max_lines = max(term_height - len(platforms) - 10, 5) + + def make_display() -> Layout: + return _build_display(platforms, config, completed, start_times, log_max_lines) + + with Live(make_display(), console=console, refresh_per_second=4) as live: + if len(platforms) == 1: + p = platforms[0] + r = build_platform(root, p, config) + completed[p.alias] = r + results.append(r) + live.update(make_display()) + else: + with ThreadPoolExecutor(max_workers=len(platforms)) as pool: + futures = {pool.submit(build_platform, root, p, config): p for p in platforms} + pending = set(futures.keys()) + while pending: + done = {f for f in pending if f.done()} + for f in done: + r = f.result() + completed[r.platform.alias] = r + results.append(r) + pending.discard(f) + live.update(make_display()) + if pending: + time.sleep(0.25) + + for r in results: + _print_platform_log(r, verbose) + + return results + + +def _print_platform_log(r: PlatformResult, verbose: bool = False) -> None: + if r.success and not verbose: + return + style = "green" if r.success else "red" + status = "OK" if r.success else "FAIL" + console.print(Panel( + "\n".join(r.log_lines) if r.log_lines else "[dim]no output[/]", + title=f"{r.platform.label} [{status}] {r.elapsed:.1f}s", + border_style=style, + )) + + +# --------------------------------------------------------------------------- +# CLI & interactive mode +# --------------------------------------------------------------------------- + + +def prompt_platforms(platforms: list[Platform], alias_map: dict[str, Platform]) -> list[Platform]: + choices = [ + questionary.Choice("All platforms", value="all", checked=True), + *[questionary.Choice(p.label, value=p.alias) for p in platforms], + ] + selected = questionary.checkbox("Select platforms:", choices=choices).ask() + if selected is None: + sys.exit(0) + if "all" in selected or not selected: + return list(platforms) + return [alias_map[alias] for alias in selected] + + +def prompt_targets() -> BuildConfig: + choices = [ + questionary.Choice("cagire (CLI)", value="cli", checked=True), + questionary.Choice("cagire-desktop", value="desktop", checked=True), + questionary.Choice("cagire-plugins (CLAP/VST3)", value="plugins", checked=True), + ] + selected = questionary.checkbox("Select targets:", choices=choices).ask() + if selected is None: + sys.exit(0) + if not selected: + return BuildConfig() + return BuildConfig( + cli="cli" in selected, + desktop="desktop" in selected, + plugins="plugins" in selected, + ) + + +def confirm_summary(platforms: list[Platform], config: BuildConfig) -> None: + console.print("[bold]Platforms:[/]") + for p in platforms: + console.print(f" [cyan]{p.label}[/]") + console.print("[bold]Targets:[/]") + if config.cli: + console.print(" cagire") + if config.desktop: + console.print(" cagire-desktop") + if config.plugins: + console.print(" cagire-plugins") + console.print() + + if not questionary.confirm("Proceed?", default=True).ask(): + console.print("[dim]Aborted.[/]") + sys.exit(0) + + +def print_results(results: list[PlatformResult], wall_time: float) -> None: + table = Table(title="Results", title_style="bold") + table.add_column("Platform", style="cyan", min_width=26) + table.add_column("Status", justify="center") + table.add_column("Time", justify="right") + table.add_column("Artifacts") + + succeeded = 0 + for r in results: + if r.success: + status = "[green]OK[/]" + names = ", ".join(Path(a).name for a in r.artifacts) + detail = names or "no artifacts" + succeeded += 1 + else: + status = "[red]FAIL[/]" + detail = f"[red]{r.error or 'unknown error'}[/]" + table.add_row(r.platform.label, status, f"{r.elapsed:.1f}s", detail) + + console.print(table) + + total = len(results) + color = "green" if succeeded == total else "red" + console.print(f"\n[{color}]{succeeded}/{total}[/] succeeded in [bold]{wall_time:.1f}s[/] (wall clock)") + + +def resolve_cli_platforms(raw: str, alias_map: dict[str, Platform]) -> list[Platform]: + platforms = [] + for alias in raw.split(","): + alias = alias.strip() + if alias not in alias_map: + console.print(f"[red]Unknown platform:[/] {alias}") + console.print(f"Valid: {', '.join(alias_map.keys())}") + sys.exit(1) + platforms.append(alias_map[alias]) + return platforms + + +def resolve_cli_targets(raw: str) -> BuildConfig: + cfg = BuildConfig(cli=False, desktop=False, plugins=False) + for t in raw.split(","): + t = t.strip() + if t == "cli": + cfg.cli = True + elif t == "desktop": + cfg.desktop = True + elif t == "plugins": + cfg.plugins = True + else: + console.print(f"[red]Unknown target:[/] {t} (expected: cli, desktop, plugins)") + sys.exit(1) + return cfg + + +def check_git_clean(root: Path) -> tuple[str, bool]: + """Return (short SHA, is_clean).""" + sha = subprocess.check_output( + ["git", "rev-parse", "--short", "HEAD"], text=True, cwd=root, + ).strip() + status = subprocess.check_output( + ["git", "status", "--porcelain"], text=True, cwd=root, + ).strip() + return sha, len(status) == 0 + + +def check_prerequisites(platforms: list[Platform], config: BuildConfig) -> None: + """Verify required tools are available, fail fast if not.""" + need_cross = any(p.cross for p in platforms) + need_docker = any(p.cross and p.os == "linux" for p in platforms) + need_bundle = config.desktop and any(not p.cross and p.os == "macos" for p in platforms) + need_nsis = any(p.os == "windows" for p in platforms) + + checks: list[tuple[str, bool]] = [("cargo", True)] + if need_cross: + checks.append(("cross", True)) + if need_docker: + checks.append(("docker", True)) + if need_bundle: + checks.append(("cargo-bundle", True)) + if need_nsis: + checks.append(("makensis", False)) + + console.print("[bold]Prerequisites:[/]") + missing_critical: list[str] = [] + for tool, critical in checks: + found = shutil.which(tool) is not None + if not found and critical: + missing_critical.append(tool) + if found: + status = "[green]found[/]" + elif critical: + status = "[red]MISSING[/]" + else: + status = "[yellow]missing (optional)[/]" + console.print(f" {tool}: {status}") + + if missing_critical: + console.print(f"\n[red]Missing critical tools: {', '.join(missing_critical)}[/]") + sys.exit(1) + console.print() + + +def write_checksums(results: list[PlatformResult], out_dir: Path) -> Path: + """Write SHA256 checksums for all artifacts.""" + lines: list[str] = [] + for r in results: + if not r.success: + continue + for artifact_path in r.artifacts: + p = Path(artifact_path) + if p.is_dir(): + continue + h = hashlib.sha256(p.read_bytes()).hexdigest() + lines.append(f"SHA256 ({p.name}) = {h}") + lines.sort() + checksum_file = out_dir / "checksums.sha256" + checksum_file.write_text("\n".join(lines) + "\n") + return checksum_file + + +def main() -> None: + parser = argparse.ArgumentParser(description="Cagire release builder") + parser.add_argument("--platforms", help="Comma-separated: macos-arm64,macos-x86_64,linux-x86_64,linux-aarch64,windows-x86_64") + parser.add_argument("--targets", help="Comma-separated: cli,desktop,plugins") + parser.add_argument("--all", action="store_true", help="Build all platforms and targets") + parser.add_argument("--yes", action="store_true", help="Skip confirmation prompt") + parser.add_argument("--verbose", "-v", action="store_true", help="Show build logs for all platforms (not just failures)") + parser.add_argument("--force", action="store_true", help="Allow building from a dirty git tree") + parser.add_argument("--no-checksums", action="store_true", help="Skip SHA256 checksum generation") + args = parser.parse_args() + + root = Path(subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip()) + + all_platforms = load_platforms(root) + alias_map = {p.alias: p for p in all_platforms} + + version = get_version(root) + sha, clean = check_git_clean(root) + dirty_tag = "" if clean else ", dirty" + console.print(Panel(f"Cagire [bold]{version}[/] ({sha}{dirty_tag}) — release builder", style="blue")) + + if not clean and not args.force: + console.print("[red]Working tree is dirty. Commit your changes or use --force.[/]") + sys.exit(1) + + if args.all: + platforms = list(all_platforms) + config = BuildConfig() + elif args.platforms or args.targets: + platforms = resolve_cli_platforms(args.platforms, alias_map) if args.platforms else list(all_platforms) + config = resolve_cli_targets(args.targets) if args.targets else BuildConfig() + else: + platforms = prompt_platforms(all_platforms, alias_map) + config = prompt_targets() + + if not args.yes and not args.all and not (args.platforms or args.targets): + confirm_summary(platforms, config) + + check_prerequisites(platforms, config) + + t0 = time.monotonic() + results = run_builds(root, platforms, config, version, verbose=args.verbose) + wall_time = time.monotonic() - t0 + + print_results(results, wall_time) + + if not args.no_checksums and any(r.success for r in results): + checksum_file = write_checksums(results, root / OUT) + console.print(f"[green]Checksums written to {checksum_file}[/]") + + if any(not r.success for r in results): + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/make-appimage.sh b/scripts/make-appimage.sh deleted file mode 100755 index c2949fc..0000000 --- a/scripts/make-appimage.sh +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Usage: scripts/make-appimage.sh -# Produces an AppImage from a Linux binary. -# On native Linux with matching arch: uses linuxdeploy. -# Otherwise (cross-compilation): builds AppImage via mksquashfs in Docker. - -if [[ $# -ne 3 ]]; then - echo "Usage: $0 " - exit 1 -fi - -BINARY="$1" -ARCH="$2" -OUTDIR="$3" - -REPO_ROOT="$(git rev-parse --show-toplevel)" -CACHE_DIR="$REPO_ROOT/.cache" -APP_NAME="$(basename "$BINARY")" - -RUNTIME_URL="https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-${ARCH}" -RUNTIME="$CACHE_DIR/runtime-${ARCH}" - -build_appdir() { - local appdir="$1" - mkdir -p "$appdir/usr/bin" - cp "$BINARY" "$appdir/usr/bin/cagire" - chmod +x "$appdir/usr/bin/cagire" - - mkdir -p "$appdir/usr/share/icons/hicolor/512x512/apps" - cp "$REPO_ROOT/assets/Cagire.png" "$appdir/usr/share/icons/hicolor/512x512/apps/cagire.png" - - cp "$REPO_ROOT/assets/cagire.desktop" "$appdir/cagire.desktop" - - # AppRun entry point - cat > "$appdir/AppRun" <<'APPRUN' -#!/bin/sh -SELF="$(readlink -f "$0")" -HERE="$(dirname "$SELF")" -exec "$HERE/usr/bin/cagire" "$@" -APPRUN - chmod +x "$appdir/AppRun" - - # Symlink icon at root for AppImage spec - ln -sf usr/share/icons/hicolor/512x512/apps/cagire.png "$appdir/cagire.png" - ln -sf cagire.desktop "$appdir/.DirIcon" 2>/dev/null || true -} - -download_runtime() { - mkdir -p "$CACHE_DIR" - if [[ ! -f "$RUNTIME" ]]; then - echo " Downloading AppImage runtime for $ARCH..." - curl -fSL "$RUNTIME_URL" -o "$RUNTIME" - fi -} - -run_native() { - local linuxdeploy_url="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-${ARCH}.AppImage" - local linuxdeploy="$CACHE_DIR/linuxdeploy-${ARCH}.AppImage" - - mkdir -p "$CACHE_DIR" - if [[ ! -f "$linuxdeploy" ]]; then - echo " Downloading linuxdeploy for $ARCH..." - curl -fSL "$linuxdeploy_url" -o "$linuxdeploy" - chmod +x "$linuxdeploy" - fi - - local appdir - appdir="$(mktemp -d)/AppDir" - build_appdir "$appdir" - - export ARCH - export LDAI_RUNTIME_FILE="$RUNTIME" - "$linuxdeploy" \ - --appimage-extract-and-run \ - --appdir "$appdir" \ - --desktop-file "$appdir/cagire.desktop" \ - --icon-file "$appdir/usr/share/icons/hicolor/512x512/apps/cagire.png" \ - --output appimage - - local appimage - appimage=$(ls -1t ./*.AppImage 2>/dev/null | head -1 || true) - if [[ -z "$appimage" ]]; then - echo " ERROR: No AppImage produced" - exit 1 - fi - mkdir -p "$OUTDIR" - mv "$appimage" "$OUTDIR/${APP_NAME}-linux-${ARCH}.AppImage" - echo " AppImage -> $OUTDIR/${APP_NAME}-linux-${ARCH}.AppImage" -} - -run_docker() { - local platform - case "$ARCH" in - x86_64) platform="linux/amd64" ;; - aarch64) platform="linux/arm64" ;; - *) echo "Unsupported arch: $ARCH"; exit 1 ;; - esac - - local appdir - appdir="$(mktemp -d)/AppDir" - build_appdir "$appdir" - - local image_tag="cagire-appimage-${ARCH}" - - echo " Building Docker image $image_tag ($platform)..." - docker build --platform "$platform" -q -t "$image_tag" - <<'DOCKERFILE' -FROM ubuntu:22.04 -RUN apt-get update && apt-get install -y --no-install-recommends \ - squashfs-tools \ - && rm -rf /var/lib/apt/lists/* -DOCKERFILE - - echo " Creating squashfs via Docker ($image_tag)..." - docker run --rm --platform "$platform" \ - -v "$appdir:/appdir:ro" \ - -v "$CACHE_DIR:/cache" \ - "$image_tag" \ - mksquashfs /appdir /cache/appimage-${ARCH}.squashfs \ - -root-owned -noappend -comp gzip -no-progress - - mkdir -p "$OUTDIR" - local final="$OUTDIR/${APP_NAME}-linux-${ARCH}.AppImage" - cat "$RUNTIME" "$CACHE_DIR/appimage-${ARCH}.squashfs" > "$final" - chmod +x "$final" - rm -f "$CACHE_DIR/appimage-${ARCH}.squashfs" - echo " AppImage -> $final" -} - -HOST_ARCH="$(uname -m)" - -download_runtime - -echo " Building AppImage for ${APP_NAME} ($ARCH)..." - -if [[ "$HOST_ARCH" == "$ARCH" ]] && [[ "$(uname -s)" == "Linux" ]]; then - run_native -else - run_docker -fi diff --git a/scripts/make-dmg.sh b/scripts/make-dmg.sh deleted file mode 100755 index d568c48..0000000 --- a/scripts/make-dmg.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Usage: scripts/make-dmg.sh -# Produces a .dmg from a macOS .app bundle using only hdiutil. - -if [[ $# -ne 2 ]]; then - echo "Usage: $0 " - exit 1 -fi - -APP_PATH="$1" -OUTDIR="$2" -REPO_ROOT="$(git rev-parse --show-toplevel)" - -if [[ ! -d "$APP_PATH" ]]; then - echo "ERROR: $APP_PATH is not a directory" - exit 1 -fi - -LIPO_OUTPUT=$(lipo -info "$APP_PATH/Contents/MacOS/cagire-desktop" 2>/dev/null) - -if [[ -z "$LIPO_OUTPUT" ]]; then - echo "ERROR: could not determine architecture from $APP_PATH" - exit 1 -fi - -if echo "$LIPO_OUTPUT" | grep -q "Architectures in the fat file"; then - ARCH="universal" -else - ARCH=$(echo "$LIPO_OUTPUT" | awk '{print $NF}') - case "$ARCH" in - arm64) ARCH="aarch64" ;; - esac -fi - -STAGING="$(mktemp -d)" -trap 'rm -rf "$STAGING"' EXIT - -cp -R "$APP_PATH" "$STAGING/Cagire.app" -ln -s /Applications "$STAGING/Applications" -cp "$REPO_ROOT/assets/DMG-README.txt" "$STAGING/README.txt" - -DMG_NAME="Cagire-${ARCH}.dmg" -mkdir -p "$OUTDIR" - -hdiutil create -volname "Cagire" \ - -srcfolder "$STAGING" \ - -ov -format UDZO \ - "$OUTDIR/$DMG_NAME" - -echo " DMG -> $OUTDIR/$DMG_NAME" diff --git a/scripts/platforms.toml b/scripts/platforms.toml new file mode 100644 index 0000000..010f560 --- /dev/null +++ b/scripts/platforms.toml @@ -0,0 +1,9 @@ +# Cagire build targets — each triple defines a compilation platform. +# Everything else (os, arch, cross, alias, label) is derived by build.py. +triples = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", + "x86_64-pc-windows-gnu", +]