rucc-targets 0.9.3

Target and psABI queries for the rucc C compiler, and the generator for docs/TARGETS.md.
//! Generating `docs/TARGETS.md` from the target table.
//!
//! The table lives in `rucc-tuple` and every other copy of it is produced from there. That is
//! `spec/cross-compile/04-target-matrix.md` section 4.7's rule and it exists because a hand maintained copy
//! of a table drifts, and a drifted support table is a promise nobody made.

use std::path::Path;
use std::process::ExitCode;

use rucc_tuple::TARGETS;

/// Write the file, or check that the one on disk matches.
pub(crate) fn run(root: &Path, check_only: bool) -> ExitCode {
    let path = root.join("docs").join("TARGETS.md");
    let wanted = render();

    if check_only {
        let found = std::fs::read_to_string(&path).unwrap_or_default();
        if found == wanted {
            println!("targets: docs/TARGETS.md is up to date");
            return ExitCode::SUCCESS;
        }
        println!("docs/TARGETS.md is out of date, run `cargo xtask targets`");
        return ExitCode::FAILURE;
    }

    // The directory rather than path.parent(), because the path was built from it two lines up
    // and asking for the parent of something we just joined is a question with a known answer.
    if let Err(error) = std::fs::create_dir_all(root.join("docs")) {
        eprintln!("error: {error}");
        return ExitCode::FAILURE;
    }
    match std::fs::write(&path, wanted) {
        Ok(()) => {
            println!("targets: wrote docs/TARGETS.md");
            ExitCode::SUCCESS
        }
        Err(error) => {
            eprintln!("error: {error}");
            ExitCode::FAILURE
        }
    }
}

/// The whole file, as text.
fn render() -> String {
    let mut out = String::new();
    out.push_str("# Targets\n\n");
    out.push_str(
        "Generated by `cargo xtask targets` from the table in `rucc-tuple`. Do not edit this file, edit the table.\n\n",
    );
    out.push_str(
        "The `now` column is what the last reporting run established and the `plan` column is what `spec/cross-compile/15-plan.md` commits to. The tiers are defined in `spec/cross-compile/04-target-matrix.md` section 4.2. The `host` column says whether rucc itself runs there, which is a much cheaper question than whether rucc compiles for it.\n\n",
    );

    out.push_str("| target | now | plan | host | notes |\n");
    out.push_str("|---|---|---|---|---|\n");
    for entry in TARGETS {
        out.push_str(&format!(
            "| `{}` | {} | {} | {} | {} |\n",
            entry.tuple,
            entry.tier.number(),
            entry.planned.number(),
            entry.host,
            entry.note
        ));
    }

    let counts = rucc_tuple::counts_by_planned_tier();
    out.push_str(&format!(
        "\n{} rows. The plan reaches tier 1 on {}, tier 2 on {}, tier 3 on {} and recognizes {}, which is {} targets that compile and link.\n",
        TARGETS.len(),
        counts[0],
        counts[1],
        counts[2],
        counts[3],
        rucc_tuple::planned_working_count()
    ));
    out
}