cargo-winapp 0.1.1

Cargo subcommand for Windows app development with winapp CLI
//! Cargo metadata parsing and build artifact detection

use anyhow::{Context, Result};
use cargo_metadata::MetadataCommand;
use serde::Deserialize;
use std::io::BufRead;
use std::path::PathBuf;
use std::process::{Command, Stdio};

/// Print status message to stderr (only in verbose mode)
#[macro_export]
macro_rules! status {
    ($verbose:expr, $($arg:tt)*) => {
        if $verbose {
            eprintln!($($arg)*);
        }
    };
}

/// Print important status message to stderr (always shown)
#[macro_export]
macro_rules! info {
    ($($arg:tt)*) => {
        eprintln!($($arg)*);
    };
}

/// Project information extracted from Cargo.toml
#[derive(Debug)]
pub struct ProjectInfo {
    #[allow(dead_code)]
    pub name: String,
    pub manifest_dir: PathBuf,
}

/// Load project information from cargo metadata
pub fn load_project_info() -> Result<ProjectInfo> {
    let metadata = MetadataCommand::new()
        .exec()
        .context("Failed to execute cargo metadata")?;

    let root_package = metadata
        .root_package()
        .context("No root package found. Make sure you're in a Cargo project directory.")?;

    Ok(ProjectInfo {
        name: (*root_package.name).clone(),
        manifest_dir: root_package.manifest_path.parent().unwrap().into(),
    })
}

/// Cargo build message for artifact detection
#[derive(Debug, Deserialize)]
struct CargoMessage {
    reason: String,
    #[serde(default)]
    executable: Option<String>,
    #[serde(default)]
    target: Option<CargoTarget>,
}

#[derive(Debug, Deserialize)]
struct CargoTarget {
    #[allow(dead_code)]
    name: String,
    kind: Vec<String>,
}

/// Build result with executable path
pub struct BuildResult {
    pub exe_path: PathBuf,
}

/// Build the project and return the executable path
pub fn build_and_get_exe(
    manifest_dir: &PathBuf,
    cargo_args: &[String],
    verbose: bool,
) -> Result<BuildResult> {
    let mut cmd = Command::new("cargo");
    cmd.arg("build")
        .arg("--message-format=json")
        .current_dir(manifest_dir)
        .stdout(Stdio::piped());

    // Always show cargo build stderr to user (build progress)
    cmd.stderr(Stdio::inherit());

    // passthrough verbose to cargo
    if verbose {
        cmd.arg("--verbose");
    }

    // Add cargo arguments (including --release if present)
    for arg in cargo_args {
        cmd.arg(arg);
    }

    let mut child = cmd.spawn().context("Failed to spawn cargo build")?;

    let stdout = child.stdout.take().context("Failed to capture stdout")?;

    let reader = std::io::BufReader::new(stdout);
    let mut exe_path: Option<PathBuf> = None;

    for line in reader.lines() {
        let line: String = line.context("Failed to read cargo output")?;

        // Try to parse as JSON message
        if let Ok(msg) = serde_json::from_str::<CargoMessage>(&line) {
            // Look for compiler-artifact with executable
            if msg.reason == "compiler-artifact" {
                if let Some(exe) = msg.executable {
                    // Check if this is a bin target
                    if let Some(target) = &msg.target {
                        if target.kind.contains(&"bin".to_string()) {
                            exe_path = Some(PathBuf::from(exe));
                        }
                    }
                }
            }
        }
    }

    let status = child.wait().context("Failed to wait for cargo build")?;

    if !status.success() {
        anyhow::bail!("cargo build failed");
    }

    let exe_path =
        exe_path.context("No executable found in build output. Is this a binary crate?")?;

    Ok(BuildResult { exe_path })
}