Feat: improve local build script
This commit is contained in:
20
Cargo.lock
generated
20
Cargo.lock
generated
@@ -873,7 +873,7 @@ dependencies = [
|
|||||||
"cpal 0.17.1",
|
"cpal 0.17.1",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"doux 0.0.14",
|
"doux",
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
"egui_ratatui",
|
"egui_ratatui",
|
||||||
@@ -925,7 +925,7 @@ dependencies = [
|
|||||||
"cagire-ratatui",
|
"cagire-ratatui",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"doux 0.0.13",
|
"doux",
|
||||||
"egui_ratatui",
|
"egui_ratatui",
|
||||||
"nih_plug",
|
"nih_plug",
|
||||||
"nih_plug_egui",
|
"nih_plug_egui",
|
||||||
@@ -1822,22 +1822,6 @@ dependencies = [
|
|||||||
"litrs",
|
"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]]
|
[[package]]
|
||||||
name = "doux"
|
name = "doux"
|
||||||
version = "0.0.14"
|
version = "0.0.14"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ cagire = { path = "../..", default-features = false, features = ["block-renderer
|
|||||||
cagire-forth = { path = "../../crates/forth" }
|
cagire-forth = { path = "../../crates/forth" }
|
||||||
cagire-project = { path = "../../crates/project" }
|
cagire-project = { path = "../../crates/project" }
|
||||||
cagire-ratatui = { path = "../../crates/ratatui" }
|
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 = { git = "https://github.com/robbert-vdh/nih-plug", features = ["standalone"] }
|
||||||
nih_plug_egui = { git = "https://github.com/robbert-vdh/nih-plug" }
|
nih_plug_egui = { git = "https://github.com/robbert-vdh/nih-plug" }
|
||||||
egui_ratatui = "2.1"
|
egui_ratatui = "2.1"
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ impl Plugin for CagirePlugin {
|
|||||||
self.sample_rate,
|
self.sample_rate,
|
||||||
self.output_channels,
|
self.output_channels,
|
||||||
64,
|
64,
|
||||||
|
buffer_config.max_buffer_size as usize,
|
||||||
);
|
);
|
||||||
self.bridge
|
self.bridge
|
||||||
.sample_registry
|
.sample_registry
|
||||||
|
|||||||
BIN
scripts/__pycache__/build.cpython-313.pyc
Normal file
BIN
scripts/__pycache__/build.cpython-313.pyc
Normal file
Binary file not shown.
BIN
scripts/__pycache__/build.cpython-314.pyc
Normal file
BIN
scripts/__pycache__/build.cpython-314.pyc
Normal file
Binary file not shown.
@@ -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 <list> Comma-separated: macos-arm64,macos-x86_64,linux-x86_64,linux-aarch64,windows-x86_64"
|
|
||||||
echo " --targets <list> 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/"
|
|
||||||
949
scripts/build.py
Executable file
949
scripts/build.py
Executable file
@@ -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()
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Usage: scripts/make-appimage.sh <binary-path> <arch> <output-dir>
|
|
||||||
# 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 <binary-path> <arch> <output-dir>"
|
|
||||||
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
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Usage: scripts/make-dmg.sh <app-path> <output-dir>
|
|
||||||
# Produces a .dmg from a macOS .app bundle using only hdiutil.
|
|
||||||
|
|
||||||
if [[ $# -ne 2 ]]; then
|
|
||||||
echo "Usage: $0 <app-path> <output-dir>"
|
|
||||||
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"
|
|
||||||
9
scripts/platforms.toml
Normal file
9
scripts/platforms.toml
Normal file
@@ -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",
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user