cargo-forward-flags 0.1.0

Generate Cargo feature forwarding declarations
use crate::{discovery, generate, manifest};
use anyhow::{Context, Result, bail};
use clap::Parser;
use std::{env, ffi::OsString, fs, path::PathBuf};

#[derive(Debug, Parser)]
#[command(
    name = "cargo-forward-flags",
    version,
    about = "Generate Cargo feature forwarding declarations",
    long_about = "Discovers dependency features with Cargo metadata and maintains only the text between forwarding markers in Cargo.toml. Place blocks in the top-level [features] table.\n\nSyntax:\n  # <forward dependency>\n  # <forward dependency prefix=\"foo-\">\n  # <forward dependency exclude=\"std,alloc\">\n  # <forward dependency prefix=\"foo-\" exclude=\"std,alloc\">\n  # </forward>\n\nThe dependency is its manifest key, including renamed dependencies. Attribute order is flexible. The default feature is always omitted. Optional dependencies use conditional dependency?/feature forwarding.",
    after_help = "Use `cargo forward-flags` or invoke `cargo-forward-flags` directly."
)]
struct Cli {
    /// Operate on this Cargo.toml (defaults to ./Cargo.toml)
    #[arg(long, value_name = "PATH")]
    manifest_path: Option<PathBuf>,

    /// Exit unsuccessfully if generated content is outdated; do not write
    #[arg(long, conflicts_with = "dry_run")]
    check: bool,

    /// Print a diff without modifying the manifest
    #[arg(long)]
    dry_run: bool,
}

pub fn run() -> Result<()> {
    let mut args: Vec<OsString> = env::args_os().skip(1).collect();
    // Cargo's external-subcommand convention may include the subcommand name as argv[1].
    if args.first().is_some_and(|arg| arg == "forward-flags") {
        args.remove(0);
    }
    let cli = Cli::parse_from(std::iter::once(OsString::from("cargo-forward-flags")).chain(args));

    let manifest_path = cli
        .manifest_path
        .unwrap_or_else(|| PathBuf::from("Cargo.toml"));
    let manifest_path = fs::canonicalize(&manifest_path)
        .with_context(|| format!("could not find manifest `{}`", manifest_path.display()))?;
    let original = fs::read_to_string(&manifest_path)
        .with_context(|| format!("could not read manifest `{}`", manifest_path.display()))?;
    let parsed = manifest::parse_manifest(&original).with_context(|| {
        format!(
            "could not parse forwarding directives in `{}`",
            manifest_path.display()
        )
    })?;
    let directives: Vec<_> = parsed
        .blocks
        .iter()
        .map(|block| block.directive.clone())
        .collect();
    let dependencies = discovery::discover(&manifest_path, &directives)?;
    let generated = generate::generate(&directives, &dependencies, &parsed.manual_features)?;
    let updated = manifest::rewrite(&original, &parsed, &generated)?;

    if updated == original {
        if cli.dry_run {
            println!("No generated features need updating.");
        }
        return Ok(());
    }
    if cli.check {
        print!(
            "{}",
            manifest::diff(&manifest_path, &parsed, &generated, &original)
        );
        bail!(
            "generated forwarding content is outdated in `{}`; run `cargo forward-flags` to update it",
            manifest_path.display()
        );
    }
    if cli.dry_run {
        print!(
            "{}",
            manifest::diff(&manifest_path, &parsed, &generated, &original)
        );
        return Ok(());
    }

    manifest::write_atomically(&manifest_path, &original, &updated)?;
    eprintln!("Updated {}", manifest_path.display());
    Ok(())
}