infinity-msfs 0.3.3

Build/packaging/util CLI for infinity-msfs projects.
use crate::{cli::PackageArgs, config::InfinityMsfsToml, runner::CliRunner, util};
use anyhow::{Result, bail};
use console::style;
use infinity_build_package::{PackageOverrides, SimPackage, build_one, locate_fspackagetool};
use infinity_build_sdk as sdk;
use std::time::Instant;

pub fn run_package(args: PackageArgs) -> Result<()> {
    if !cfg!(target_os = "windows") {
        bail!(
            "`infinity-msfs package` is Windows-only.\n\
             fspackagetool.exe drives the MSFS 2024 sim binary to compile assets,\n\
             and that binary only ships for Windows."
        );
    }

    sdk::ensure_sdk()?;

    let root = util::find_project_root()?;
    let cfg_path = util::config_path(&root);
    if !cfg_path.exists() {
        bail!(
            "no infinity-msfs.toml found at {}; run `infinity-msfs package` from a project root",
            cfg_path.display()
        );
    }

    let cfg = InfinityMsfsToml::load(&cfg_path)?;
    if cfg.sim_packages.is_empty() {
        bail!(
            "no [[sim_packages]] entries in {}.\n\
             Add at least one entry pointing at a project .xml.",
            cfg_path.display()
        );
    }

    let selected: Vec<&SimPackage> = if args.only.is_empty() {
        cfg.sim_packages.iter().collect()
    } else {
        let chosen: Vec<&SimPackage> = cfg
            .sim_packages
            .iter()
            .filter(|p| args.only.iter().any(|n| n == &p.name))
            .collect();
        if chosen.is_empty() {
            bail!(
                "no [[sim_packages]] entry matched filter {:?} (available: {})",
                args.only,
                cfg.sim_packages
                    .iter()
                    .map(|p| p.name.as_str())
                    .collect::<Vec<_>>()
                    .join(", ")
            );
        }
        chosen
    };

    let tool = locate_fspackagetool()?;
    let runner = CliRunner {
        verbose: args.verbose,
    };
    let overrides = PackageOverrides {
        force_rebuild: args.rebuild,
        mirror_output: args.mirror,
        prefer_steam: args.force_steam,
        marketplace_dir: args.marketplace.clone(),
    };

    println!(
        "{} {} {} {} {}",
        style("Packaging").cyan().bold(),
        style(selected.len()).bold(),
        if selected.len() == 1 {
            "project"
        } else {
            "projects"
        },
        style("via").dim(),
        style(tool.display()).dim(),
    );

    let started = Instant::now();
    let mut succeeded = 0usize;
    let mut failed: Vec<String> = Vec::new();

    for entry in &selected {
        let entry_started = Instant::now();
        match build_one(&runner, &root, &tool, entry, &overrides) {
            Ok(()) => {
                succeeded += 1;
                println!(
                    "{} {} {}",
                    style("").green().bold(),
                    style(&entry.name).bold(),
                    style(format!("({:.1?})", entry_started.elapsed())).dim(),
                );
            }
            Err(e) => {
                failed.push(entry.name.clone());
                eprintln!(
                    "{} {} {}",
                    style("").red().bold(),
                    style(&entry.name).bold(),
                    style(format!("{e:#}")).dim(),
                );
            }
        }
    }

    println!(
        "{} packaged {}/{} in {:.1?}{}",
        style("Done").green().bold(),
        style(succeeded).bold(),
        style(selected.len()).bold(),
        started.elapsed(),
        if failed.is_empty() {
            String::new()
        } else {
            format!(" — failed: {}", failed.join(", "))
        },
    );

    if !failed.is_empty() {
        bail!("{} package(s) failed", failed.len());
    }
    Ok(())
}