From f78b4374b60ab6fc39bc2acf74ce7a8763a19841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Tue, 17 Mar 2026 12:31:50 +0100 Subject: [PATCH] Feat: improve local build script --- Cargo.lock | 20 +- plugins/cagire-plugins/Cargo.toml | 2 +- plugins/cagire-plugins/src/lib.rs | 1 + scripts/__pycache__/build.cpython-313.pyc | Bin 0 -> 40605 bytes scripts/__pycache__/build.cpython-314.pyc | Bin 0 -> 51628 bytes scripts/build-all.sh | 473 ----------- scripts/build.py | 949 ++++++++++++++++++++++ scripts/make-appimage.sh | 141 ---- scripts/make-dmg.sh | 52 -- scripts/platforms.toml | 9 + 10 files changed, 962 insertions(+), 685 deletions(-) create mode 100644 scripts/__pycache__/build.cpython-313.pyc create mode 100644 scripts/__pycache__/build.cpython-314.pyc delete mode 100755 scripts/build-all.sh create mode 100755 scripts/build.py delete mode 100755 scripts/make-appimage.sh delete mode 100755 scripts/make-dmg.sh create mode 100644 scripts/platforms.toml 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 0000000000000000000000000000000000000000..4bbb956a178f15538943d066dd563378cf065bd8 GIT binary patch literal 40605 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 E{~*9Dv;Y7A literal 0 HcmV?d00001 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", +]