diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 902f4e8..3b5d63e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,104 +1,228 @@ -name: CI +name: Build and Release Andromeda on: + workflow_dispatch: push: - branches: - - main - pull_request: - branches: - - main + branches: [main] -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} - cancel-in-progress: ${{ github.ref_name != 'main' }} - -env: - CARGO_TERM_COLOR: always +permissions: + contents: write jobs: - typos: - name: Spellcheck - runs-on: ubuntu-latest + build: + name: Build ${{ matrix.asset-name }} + runs-on: ${{ matrix.os }} + continue-on-error: true # Don't fail the entire workflow if one target fails + strategy: + fail-fast: false # Continue with other builds even if one fails + matrix: + include: + # Linux (x86_64) + - os: ubuntu-24.04 + rust-target: x86_64-unknown-linux-gnu + asset-name: andromeda-linux-amd64 + installer-name: andromeda-installer-linux-amd64 + + # Linux (ARM64) - cross-compilation + # - os: ubuntu-24.04 + # rust-target: aarch64-unknown-linux-gnu + # asset-name: andromeda-linux-arm64 + # installer-name: andromeda-installer-linux-arm64 + # cross-compile: true + + # macOS (Intel) + - os: macos-13 + rust-target: x86_64-apple-darwin + asset-name: andromeda-macos-amd64 + installer-name: andromeda-installer-macos-amd64 + + # macOS (Apple Silicon/ARM) + - os: macos-14 + rust-target: aarch64-apple-darwin + asset-name: andromeda-macos-arm64 + installer-name: andromeda-installer-macos-arm64 + + # Windows (x86_64) + - os: windows-latest + rust-target: x86_64-pc-windows-msvc + asset-name: andromeda-windows-amd64.exe + installer-name: andromeda-installer-windows-amd64.exe + + # Windows (ARM64) + - os: windows-latest + rust-target: aarch64-pc-windows-msvc + asset-name: andromeda-windows-arm64.exe + installer-name: andromeda-installer-windows-arm64.exe + steps: - uses: actions/checkout@v4 - - name: Spell check - uses: crate-ci/typos@master - lint: - name: Lint + - name: Install the rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: "nightly-2025-09-05" + targets: ${{ matrix.rust-target }} + + # - name: Install cross-compilation dependencies + # if: matrix.cross-compile + # run: | + # sudo apt-get update + # sudo apt-get install -y gcc-aarch64-linux-gnu libc6-dev-arm64-cross pkg-config + + - name: Build + continue-on-error: true # Allow individual builds to fail + run: cargo build --release --target ${{ matrix.rust-target }} --manifest-path ./cli/Cargo.toml + env: + # Cross-compilation environment variables + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc + # PKG_CONFIG settings for cross-compilation + PKG_CONFIG_ALLOW_CROSS: 1 + + - name: Build installer + continue-on-error: true # Allow individual builds to fail + run: cargo build --bin andromeda-installer --release --target ${{ matrix.rust-target }} --manifest-path ./cli/Cargo.toml + env: + # Cross-compilation environment variables + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc + # PKG_CONFIG settings for cross-compilation + PKG_CONFIG_ALLOW_CROSS: 1 + + - name: Build satellites + continue-on-error: true # Allow individual builds to fail + run: | + cargo build --bin andromeda-run --release --target ${{ matrix.rust-target }} --manifest-path ./cli/Cargo.toml + cargo build --bin andromeda-compile --release --target ${{ matrix.rust-target }} --manifest-path ./cli/Cargo.toml + cargo build --bin andromeda-fmt --release --target ${{ matrix.rust-target }} --manifest-path ./cli/Cargo.toml + cargo build --bin andromeda-lint --release --target ${{ matrix.rust-target }} --manifest-path ./cli/Cargo.toml + cargo build --bin andromeda-check --release --target ${{ matrix.rust-target }} --manifest-path ./cli/Cargo.toml + cargo build --bin andromeda-bundle --release --target ${{ matrix.rust-target }} --manifest-path ./cli/Cargo.toml + env: + # Cross-compilation environment variables + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc + # PKG_CONFIG settings for cross-compilation + PKG_CONFIG_ALLOW_CROSS: 1 + + - name: Prepare binaries + shell: bash + run: | + cd target/${{ matrix.rust-target }}/release/ + + # Prepare main binary + if [ -f "andromeda.exe" ]; then + mv andromeda.exe ${{ matrix.asset-name }} + elif [ -f "andromeda" ]; then + mv andromeda ${{ matrix.asset-name }} + else + echo "Main binary not found, build may have failed" + exit 1 + fi + + # Prepare installer binary + if [ -f "andromeda-installer.exe" ]; then + cp "andromeda-installer.exe" "${{ matrix.installer-name }}" + elif [ -f "andromeda-installer" ]; then + cp "andromeda-installer" "${{ matrix.installer-name }}" + else + echo "Warning: Installer binary not found" + fi + + # Prepare satellite binaries + for satellite in run compile fmt lint check bundle; do + if [ -f "andromeda-${satellite}.exe" ]; then + # Windows binaries + cp "andromeda-${satellite}.exe" "andromeda-${satellite}-${{ matrix.rust-target }}.exe" + elif [ -f "andromeda-${satellite}" ]; then + # Unix binaries + cp "andromeda-${satellite}" "andromeda-${satellite}-${{ matrix.rust-target }}" + else + echo "Warning: andromeda-${satellite} binary not found" + fi + done + + - name: Upload Main Binary as Artifact + uses: actions/upload-artifact@v4 + if: success() # Only upload if binary was prepared successfully + with: + name: ${{ matrix.asset-name }} + path: target/${{ matrix.rust-target }}/release/${{ matrix.asset-name }} + + - name: Upload Installer Binary as Artifact + uses: actions/upload-artifact@v4 + if: success() # Only upload if installer was prepared successfully + with: + name: ${{ matrix.installer-name }} + path: target/${{ matrix.rust-target }}/release/${{ matrix.installer-name }} + + - name: Upload Satellite Binaries as Artifacts + uses: actions/upload-artifact@v4 + if: success() + with: + name: satellites-${{ matrix.rust-target }} + path: | + target/${{ matrix.rust-target }}/release/andromeda-run-${{ matrix.rust-target }}* + target/${{ matrix.rust-target }}/release/andromeda-compile-${{ matrix.rust-target }}* + target/${{ matrix.rust-target }}/release/andromeda-fmt-${{ matrix.rust-target }}* + target/${{ matrix.rust-target }}/release/andromeda-lint-${{ matrix.rust-target }}* + target/${{ matrix.rust-target }}/release/andromeda-check-${{ matrix.rust-target }}* + target/${{ matrix.rust-target }}/release/andromeda-bundle-${{ matrix.rust-target }}* + + release: + name: Create Release + needs: build runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + steps: - uses: actions/checkout@v4 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@nightly - with: - toolchain: nightly-2025-09-05 - components: rustfmt, clippy, llvm-tools-preview, rustc-dev - - name: Cache on ${{ github.ref_name }} - uses: Swatinem/rust-cache@v2 + + - name: Download all artifacts + uses: actions/download-artifact@v4 with: - shared-key: warm - - name: Check formatting - run: cargo fmt --check - - name: Clippy + path: ./artifacts + + - name: List all artifacts run: | - cargo +nightly-2025-09-05 clippy --all-targets -- -D warnings - cargo +nightly-2025-09-05 clippy --all-targets --all-features -- -D warnings + echo "=== All artifacts ready for release ===" + find ./artifacts -type f -name "andromeda-*" | sort + echo "=======================================" - build: - name: Build & Test - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: - [ - "macos-14", - "macos-latest", - "ubuntu-24.04", - "ubuntu-latest", - "windows-latest", - ] - steps: - - uses: actions/checkout@v4 - - uses: oxc-project/setup-rust@cd82e1efec7fef815e2c23d296756f31c7cdc03d # v1.0.0 + - name: Create Draft Release + uses: svenstaro/upload-release-action@v2 with: - cache-key: warm - save-cache: ${{ github.ref_name == 'main' }} - - name: Check - run: cargo check --all-targets - - name: Build - run: cargo build --tests --bins --examples - - name: Test - run: cargo test - env: - RUST_BACKTRACE: 1 - # TODO: Enable Web Platform Tests - # wpt: - # name: Web Platform Tests - # runs-on: macos-latest - # timeout-minutes: 50 - # steps: - # - uses: actions/checkout@v4 - # - name: Install Rust toolchain - # uses: dtolnay/rust-toolchain@master - # with: - # toolchain: "nightly-2025-09-05" - # - name: Cache on ${{ github.ref_name }} - # uses: Swatinem/rust-cache@v2 - # with: - # shared-key: wpt - # save-if: ${{ github.ref == 'refs/heads/main' }} - # - name: Checkout WPT submodule - # run: git submodule update --init --recursive - # - name: Build andromeda cli - # run: cargo build -p andromeda --release - # - name: Build WPT runner - # run: cargo build --bin wpt_test_runner --release - # - name: Run WPT tests - # run: cargo run --bin wpt --release -- --wpt-dir tests/wpt --ci-mode - # - name: Generate test report - # if: always() - # run: cargo run --bin wpt_test_runner --release -- report --detailed - # - name: Validate test expectations - # if: always() - # run: cargo run --bin wpt_test_runner --release -- validate-expectations --wpt-dir tests/wpt + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ./artifacts/*/andromeda-* + file_glob: true + draft: true + tag: latest + overwrite: true + body: | + ## Andromeda Release + + ### Installation + + **Main CLI:** + - Download `andromeda-{platform}-{arch}` for your platform + - Make executable and add to PATH + + **Installer:** + - Download `andromeda-installer-{platform}-{arch}` for your platform + - Run `andromeda-installer` to install main CLI + - Run `andromeda-installer satellite ` to install satellites + + **Satellites (specialized binaries):** + - `andromeda-run` - Execute JS/TS files + - `andromeda-compile` - Compile to executables + - `andromeda-fmt` - Format code + - `andromeda-lint` - Lint code + - `andromeda-check` - Type-check TypeScript + - `andromeda-bundle` - Bundle and minify + + ### Platforms + - Linux (x86_64, ARM64) + - macOS (Intel, Apple Silicon) + - Windows (x86_64, ARM64) + + See [SATELLITES.md](https://github.com/tryandromeda/andromeda/blob/main/cli/SATELLITES.md) for detailed satellite documentation. diff --git a/cli/src/bin/installer.rs b/cli/src/bin/installer.rs index 4fa6e52..e6e4ddd 100644 --- a/cli/src/bin/installer.rs +++ b/cli/src/bin/installer.rs @@ -2,7 +2,7 @@ // If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. use andromeda::{CliError, CliResult}; -use clap::{Args, Parser as ClapParser}; +use clap::{Args, Parser as ClapParser, Subcommand}; use serde::Deserialize; use std::env; use std::fs; @@ -14,11 +14,43 @@ use std::process::Command; #[command(name = "andromeda-installer")] #[command(about = "Andromeda Installation Tool", long_about = None)] struct Cli { + #[command(subcommand)] + command: Option, + #[command(flatten)] install_args: InstallArgs, } -#[derive(Debug, Args)] +#[derive(Debug, Subcommand)] +enum InstallerCommand { + /// Install Andromeda satellites (specialized binaries) + /// + /// Satellites are lightweight, single-purpose binaries optimized for specific tasks. + /// + /// Available satellites: + /// - run: Execute JavaScript/TypeScript files + /// - compile: Compile JS/TS into standalone executables + /// - fmt: Format JavaScript/TypeScript files + /// - lint: Lint JavaScript/TypeScript files + /// - check: Type-check TypeScript files + /// - bundle: Bundle and minify JS/TS files + /// - all: Install all satellites + /// + /// Examples: + /// andromeda-installer satellite run + /// andromeda-installer satellite fmt lint check + /// andromeda-installer satellite all --force + Satellite { + /// Satellites to install (run, compile, fmt, lint, check, bundle, or 'all') + #[arg(required = true)] + satellites: Vec, + + #[command(flatten)] + install_args: InstallArgs, + }, +} + +#[derive(Debug, Args, Clone)] struct InstallArgs { /// Installation directory (default: %USERPROFILE%\.local\bin) #[arg(short = 'd', long)] @@ -59,9 +91,18 @@ struct GitHubAsset { const REPO_OWNER: &str = "tryandromeda"; const REPO_NAME: &str = "andromeda"; +const AVAILABLE_SATELLITES: &[&str] = &["run", "compile", "fmt", "lint", "check", "bundle"]; + fn main() -> CliResult<()> { let cli = Cli::parse(); - install_andromeda(cli.install_args) + + match cli.command { + Some(InstallerCommand::Satellite { + satellites, + install_args, + }) => install_satellites(satellites, install_args), + None => install_andromeda(cli.install_args), + } } fn install_andromeda(args: InstallArgs) -> CliResult<()> { @@ -310,12 +351,211 @@ fn print_manual_path_instructions(install_dir: &Path) { } // Utility functions for colored output +fn install_satellites(satellites: Vec, args: InstallArgs) -> CliResult<()> { + print_satellite_header(); + + // Determine installation directory + let install_dir = args.install_dir.clone().unwrap_or_else(|| { + let user_profile = env::var("USERPROFILE") + .unwrap_or_else(|_| env::var("HOME").unwrap_or_else(|_| ".".to_string())); + PathBuf::from(user_profile).join(".local").join("bin") + }); + + if args.verbose { + print_info(&format!( + "Installation directory: {}", + install_dir.display() + )); + } + + // Create installation directory + if !install_dir.exists() { + print_info("Creating installation directory..."); + fs::create_dir_all(&install_dir).map_err(CliError::Io)?; + } + + // Parse satellite list + let satellites_to_install: Vec = if satellites.len() == 1 && satellites[0] == "all" { + AVAILABLE_SATELLITES.iter().map(|s| s.to_string()).collect() + } else { + // Validate satellite names + for sat in &satellites { + if !AVAILABLE_SATELLITES.contains(&sat.as_str()) { + print_error(&format!( + "Unknown satellite: '{}'\n\nAvailable satellites:\n - {}\n - all (installs all satellites)", + sat, + AVAILABLE_SATELLITES.join("\n - ") + )); + return Err(CliError::Config(format!("Unknown satellite: {}", sat))); + } + } + satellites + }; + + // Get version to install + let version = if let Some(ref v) = args.version { + v.clone() + } else { + print_info("Fetching latest release information..."); + get_latest_version(args.verbose)? + }; + + print_info(&format!( + "Installing {} satellite(s) from version {}", + satellites_to_install.len(), + version + )); + + // Detect platform + let rust_target = detect_rust_target()?; + if args.verbose { + print_info(&format!("Detected platform target: {}", rust_target)); + } + + let mut success_count = 0; + let mut failed_satellites = Vec::new(); + + for satellite in &satellites_to_install { + print_info(&format!("Installing andromeda-{} satellite...", satellite)); + + match install_single_satellite(satellite, &version, &rust_target, &install_dir, &args) { + Ok(_) => { + print_success(&format!("✓ andromeda-{} installed successfully", satellite)); + success_count += 1; + } + Err(e) => { + print_warning(&format!( + "✗ Failed to install andromeda-{}: {}", + satellite, e + )); + failed_satellites.push(satellite.clone()); + } + } + } + + println!(); + if success_count == satellites_to_install.len() { + print_success(&format!( + "All {} satellite(s) installed successfully!", + success_count + )); + } else { + print_warning(&format!( + "{} of {} satellite(s) installed successfully", + success_count, + satellites_to_install.len() + )); + if !failed_satellites.is_empty() { + print_warning(&format!( + "Failed satellites: {}", + failed_satellites.join(", ") + )); + } + } + + if !args.skip_path && success_count > 0 { + configure_path(&install_dir, args.verbose)?; + print_info( + "You may need to restart your terminal or run 'refreshenv' for PATH changes to take effect.", + ); + } + + Ok(()) +} + +fn install_single_satellite( + satellite: &str, + version: &str, + rust_target: &str, + install_dir: &Path, + args: &InstallArgs, +) -> CliResult<()> { + let binary_name = format!("andromeda-{}", satellite); + let extension = if cfg!(windows) { ".exe" } else { "" }; + let binary_path = install_dir.join(format!("{}{}", binary_name, extension)); + + // Check if already installed + if binary_path.exists() && !args.force { + return Err(CliError::Config( + "already installed (use --force to reinstall)".to_string(), + )); + } + + // Build download URL for satellite + let asset_name = format!("andromeda-{}-{}{}", satellite, rust_target, extension); + let download_url = format!( + "https://github.com/{}/{}/releases/download/{}/{}", + REPO_OWNER, REPO_NAME, version, asset_name + ); + + if args.verbose { + print_info(&format!("Download URL: {}", download_url)); + } + + // Download binary + let binary_data = download_file(&download_url, args.verbose)?; + + // Install binary + fs::write(&binary_path, binary_data).map_err(CliError::Io)?; + + // Make executable on Unix systems + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&binary_path) + .map_err(CliError::Io)? + .permissions(); + perms.set_mode(0o755); + fs::set_permissions(&binary_path, perms).map_err(CliError::Io)?; + } + + // Verify installation + if let Ok(output) = Command::new(&binary_path).arg("--version").output() + && !output.status.success() + { + return Err(CliError::Config("verification failed".to_string())); + } + + Ok(()) +} + +fn detect_rust_target() -> CliResult { + let os = env::consts::OS; + let arch = env::consts::ARCH; + + let target = match (os, arch) { + ("windows", "x86_64") => "x86_64-pc-windows-msvc", + ("windows", "aarch64") => "aarch64-pc-windows-msvc", + ("linux", "x86_64") => "x86_64-unknown-linux-gnu", + ("linux", "aarch64") => "aarch64-unknown-linux-gnu", + ("macos", "x86_64") => "x86_64-apple-darwin", + ("macos", "aarch64") => "aarch64-apple-darwin", + _ => { + return Err(CliError::Config(format!( + "Unsupported platform: {} {}", + os, arch + ))); + } + }; + + Ok(target.to_string()) +} + fn print_header() { println!("🚀 Andromeda Installation Tool"); println!("════════════════════════════════"); println!(); } +fn print_satellite_header() { + println!("đŸ›°ī¸ Andromeda Satellite Installation Tool"); + println!("══════════════════════════════════════════"); + println!(); + println!("Satellites are specialized, lightweight binaries for specific tasks."); + println!("Available: run, compile, fmt, lint, check, bundle"); + println!(); +} + fn print_info(message: &str) { println!("â„šī¸ {message}"); }