225 lines
7.5 KiB
Python
225 lines
7.5 KiB
Python
#!/usr/bin/env -S uv run --script
|
|
# /// script
|
|
# requires-python = ">=3.12"
|
|
# dependencies = ["httpx"]
|
|
# ///
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import os
|
|
import platform
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
import httpx
|
|
|
|
|
|
# Archives with bundled ffmpeg for macOS/Windows, raw binary for Linux
|
|
SONA_ASSET_MAP = {
|
|
"aarch64-apple-darwin": ("sona-darwin-arm64-with-ffmpeg.tar.gz", "sona", "ffmpeg"),
|
|
"x86_64-apple-darwin": ("sona-darwin-amd64-with-ffmpeg.tar.gz", "sona", "ffmpeg"),
|
|
"x86_64-unknown-linux-gnu": ("sona-linux-amd64", None, None),
|
|
"aarch64-unknown-linux-gnu": ("sona-linux-arm64", None, None),
|
|
"x86_64-pc-windows-msvc": ("sona-windows-amd64-with-ffmpeg.zip", "sona.exe", "ffmpeg.exe"),
|
|
}
|
|
|
|
HOST_TRIPLE_MAP = {
|
|
("Darwin", "arm64"): "aarch64-apple-darwin",
|
|
("Darwin", "x86_64"): "x86_64-apple-darwin",
|
|
("Linux", "x86_64"): "x86_64-unknown-linux-gnu",
|
|
("Linux", "aarch64"): "aarch64-unknown-linux-gnu",
|
|
("Windows", "AMD64"): "x86_64-pc-windows-msvc",
|
|
}
|
|
|
|
|
|
def has_feature(name: str, args: list[str]) -> bool:
|
|
return f"--{name}" in args or name in args
|
|
|
|
|
|
def run_cmd(*args: str) -> None:
|
|
subprocess.run(args, check=True)
|
|
|
|
|
|
def parse_target_arg(args: list[str]) -> str | None:
|
|
for i, arg in enumerate(args):
|
|
if arg == "--target" and i + 1 < len(args):
|
|
return args[i + 1]
|
|
return None
|
|
|
|
|
|
def detect_host_target() -> str | None:
|
|
return HOST_TRIPLE_MAP.get((platform.system(), platform.machine()))
|
|
|
|
|
|
def download_with_progress(client: httpx.Client, url: str, label: str) -> bytes:
|
|
with client.stream("GET", url) as response:
|
|
response.raise_for_status()
|
|
total = int(response.headers.get("content-length", 0))
|
|
downloaded = 0
|
|
chunks: list[bytes] = []
|
|
for chunk in response.iter_bytes():
|
|
chunks.append(chunk)
|
|
downloaded += len(chunk)
|
|
if total:
|
|
pct = downloaded * 100 // total
|
|
print(f"\r {label}: {pct}% ({downloaded // 1024 // 1024}MB / {total // 1024 // 1024}MB)", end="", flush=True)
|
|
if total:
|
|
print()
|
|
return b"".join(chunks)
|
|
|
|
|
|
def download_sona(script_root: Path, target_triple: str | None) -> None:
|
|
resolved_target = target_triple or detect_host_target()
|
|
if not resolved_target:
|
|
print(
|
|
"Warning: Could not detect host Rust target triple. "
|
|
"Pass --target <rust-target-triple> or place the binary manually in desktop/src-tauri/binaries/."
|
|
)
|
|
return
|
|
|
|
asset_entry = SONA_ASSET_MAP.get(resolved_target)
|
|
if not asset_entry:
|
|
print(
|
|
f"Warning: Unsupported target triple '{resolved_target}' for automatic sona download. "
|
|
"Place the binary manually in desktop/src-tauri/binaries/."
|
|
)
|
|
return
|
|
|
|
repo_root = script_root.parent
|
|
version_file = repo_root / ".sona-version"
|
|
try:
|
|
tag = version_file.read_text(encoding="utf-8").strip()
|
|
except OSError as exc:
|
|
print(f"Warning: Unable to read {version_file}: {exc}")
|
|
return
|
|
|
|
if not tag:
|
|
print(f"Warning: {version_file} is empty; skipping sona download.")
|
|
return
|
|
|
|
is_windows = resolved_target.endswith("windows-msvc")
|
|
sona_sidecar = f"sona-{resolved_target}" + (".exe" if is_windows else "")
|
|
binaries_dir = repo_root / "desktop" / "src-tauri" / "binaries"
|
|
sona_dest = binaries_dir / sona_sidecar
|
|
|
|
binaries_dir.mkdir(parents=True, exist_ok=True)
|
|
asset_name, sona_member, ffmpeg_member = asset_entry
|
|
ffmpeg_dest: Path | None = None
|
|
if ffmpeg_member is not None:
|
|
ffmpeg_sidecar = f"ffmpeg-{resolved_target}" + (".exe" if is_windows else "")
|
|
ffmpeg_dest = binaries_dir / ffmpeg_sidecar
|
|
|
|
if sona_dest.exists() and (ffmpeg_dest is None or ffmpeg_dest.exists()):
|
|
print(f"Sona sidecar already exists at {sona_dest}; skipping download.")
|
|
if ffmpeg_dest is not None:
|
|
print(f"FFmpeg sidecar already exists at {ffmpeg_dest}; skipping download.")
|
|
return
|
|
|
|
url = f"https://github.com/thewh1teagle/sona/releases/download/{tag}/{asset_name}"
|
|
|
|
try:
|
|
with httpx.Client(follow_redirects=True, timeout=120) as client:
|
|
data = download_with_progress(client, url, "sona")
|
|
except Exception as exc:
|
|
print(f"Warning: Failed to download sona from {url}: {exc}")
|
|
print(f"Warning: You can manually place the sidecar at {sona_dest}")
|
|
return
|
|
|
|
if sona_member is None:
|
|
# Raw binary (Linux) — no archive, no ffmpeg
|
|
sona_dest.write_bytes(data)
|
|
sona_dest.chmod(sona_dest.stat().st_mode | 0o111)
|
|
print(f"Downloaded sona sidecar to {sona_dest}")
|
|
return
|
|
|
|
# Extract sona and ffmpeg from archive
|
|
if ffmpeg_dest is None:
|
|
raise RuntimeError(f"Expected ffmpeg sidecar path for target {resolved_target}")
|
|
|
|
if asset_name.endswith(".zip"):
|
|
with zipfile.ZipFile(io.BytesIO(data)) as zf:
|
|
for name in zf.namelist():
|
|
basename = Path(name).name
|
|
if basename == sona_member:
|
|
sona_dest.write_bytes(zf.read(name))
|
|
elif basename == ffmpeg_member:
|
|
ffmpeg_dest.write_bytes(zf.read(name))
|
|
else:
|
|
with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as tf:
|
|
for member in tf.getmembers():
|
|
basename = Path(member.name).name
|
|
if basename == sona_member:
|
|
sona_dest.write_bytes(tf.extractfile(member).read())
|
|
elif basename == ffmpeg_member:
|
|
ffmpeg_dest.write_bytes(tf.extractfile(member).read())
|
|
|
|
for path in (sona_dest, ffmpeg_dest):
|
|
if path.exists() and not path.name.endswith(".exe"):
|
|
path.chmod(path.stat().st_mode | 0o111)
|
|
|
|
print(f"Extracted sona sidecar to {sona_dest}")
|
|
print(f"Extracted ffmpeg sidecar to {ffmpeg_dest}")
|
|
|
|
|
|
def main() -> int:
|
|
original_cwd = Path.cwd()
|
|
script_root = Path(__file__).resolve().parent
|
|
tauri_dir = script_root.parent / "desktop" / "src-tauri"
|
|
os.chdir(tauri_dir)
|
|
cwd = Path.cwd()
|
|
argv = sys.argv[1:]
|
|
target_triple = parse_target_arg(argv)
|
|
download_sona(script_root, target_triple)
|
|
|
|
platform_map = {
|
|
"Windows": "windows",
|
|
"Darwin": "macos",
|
|
"Linux": "linux",
|
|
}
|
|
current_platform = platform_map.get(platform.system())
|
|
if not current_platform:
|
|
raise RuntimeError(f"Unsupported platform: {platform.system()}")
|
|
|
|
if current_platform == "linux":
|
|
apt_packages = [
|
|
"pkg-config",
|
|
"build-essential",
|
|
"libglib2.0-dev",
|
|
"libgtk-3-dev",
|
|
"libwebkit2gtk-4.1-dev",
|
|
"clang",
|
|
"cmake",
|
|
"libasound2-dev",
|
|
"libxdo-dev",
|
|
]
|
|
run_cmd("sudo", "apt-get", "update")
|
|
for pkg in apt_packages:
|
|
run_cmd("sudo", "apt-get", "install", "-y", pkg)
|
|
|
|
github_env = os.environ.get("GITHUB_ENV")
|
|
|
|
if not github_env:
|
|
print("\nCommands to build:")
|
|
if original_cwd != cwd:
|
|
relative_path = os.path.relpath(cwd.parent, original_cwd)
|
|
print(f"cd {relative_path}")
|
|
print("pnpm install")
|
|
print("pnpm exec tauri build")
|
|
|
|
action_arg = next((arg for arg in argv if "--dev" in arg or "--build" in arg), None)
|
|
if action_arg:
|
|
os.chdir(cwd.parent)
|
|
|
|
run_cmd("pnpm", "install")
|
|
run_cmd("pnpm", "exec", "tauri", "dev" if "--dev" in action_arg else "build")
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|