run-rs 0.6.31

Run a subset of Rust as an interpreted script
//! Renders the manifest `rust check` and `rust build` compile a script against. The entries come
//! from this crate's own `[dependencies]`, so the checker can never drift from the versions the
//! bridges are built with. A `workspace = true` entry is filled in from the workspace manifest.
//! The published crate has none of those, cargo inlines them on publish.

use std::collections::BTreeMap;
use std::path::Path;

use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct Manifest {
    package: Package,
    #[serde(default)]
    dependencies: BTreeMap<String, Dep>,
    #[serde(default)]
    target: BTreeMap<String, TargetDeps>,
}

#[derive(Deserialize)]
struct Package {
    metadata: Metadata,
}

#[derive(Deserialize)]
struct Metadata {
    rustscript: Rustscript,
}

#[derive(Deserialize)]
struct Rustscript {
    #[serde(rename = "script-crates")]
    script_crates: Vec<String>,
}

#[derive(Deserialize, Serialize, Default)]
struct TargetDeps {
    #[serde(default)]
    dependencies: BTreeMap<String, Dep>,
}

#[derive(Deserialize)]
struct WorkspaceManifest {
    workspace: Workspace,
}

#[derive(Deserialize)]
struct Workspace {
    dependencies: BTreeMap<String, Dep>,
}

/// `name = "1.0"` or the table form.
#[derive(Deserialize, Serialize, Clone)]
#[serde(untagged)]
enum Dep {
    Version(String),
    Table(DepTable),
}

#[derive(Deserialize, Serialize, Clone, Default)]
struct DepTable {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    version: Option<String>,
    #[serde(default, skip_serializing)]
    workspace: bool,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    features: Vec<String>,
    #[serde(
        default,
        rename = "default-features",
        skip_serializing_if = "Option::is_none"
    )]
    default_features: Option<bool>,
}

#[derive(Serialize)]
struct ScriptManifest {
    dependencies: BTreeMap<String, Dep>,
    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
    target: BTreeMap<String, TargetDeps>,
}

pub struct Generated {
    /// The `[dependencies]` tables, TOML text.
    pub manifest: String,
    /// `SCRIPT_CRATES`, the crate names as a script spells them in `use`.
    pub crates_rs: String,
}

pub fn generate(manifest_dir: &Path) -> Generated {
    let manifest = read_manifest(manifest_dir);
    let workspace = read_workspace(manifest_dir);
    let wanted = &manifest.package.metadata.rustscript.script_crates;

    let mut dependencies = BTreeMap::new();
    let mut target: BTreeMap<String, TargetDeps> = BTreeMap::new();
    for name in wanted {
        if let Some(dep) = manifest.dependencies.get(name) {
            dependencies.insert(name.clone(), resolve(name, dep, workspace.as_ref()));
            continue;
        }
        let found = manifest
            .target
            .iter()
            .find_map(|(cfg, deps)| deps.dependencies.get(name).map(|dep| (cfg, dep)));
        let Some((cfg, dep)) = found else {
            panic!("script crate `{name}` is not a dependency of run-rs");
        };
        target
            .entry(cfg.clone())
            .or_default()
            .dependencies
            .insert(name.clone(), resolve(name, dep, workspace.as_ref()));
    }

    let manifest = toml::to_string(&ScriptManifest {
        dependencies,
        target,
    })
    .expect("the script manifest serializes");
    let names: Vec<String> = wanted
        .iter()
        .map(|name| format!("{:?}", name.replace('-', "_")))
        .collect();
    let crates_rs = format!(
        "// Generated by build.rs from Cargo.toml. Do not edit.\n\n\
         /// The crates a script may use without declaring anything.\n\
         pub const SCRIPT_CRATES: &[&str] = &[{}];\n",
        names.join(", ")
    );
    Generated {
        manifest,
        crates_rs,
    }
}

fn read_manifest(manifest_dir: &Path) -> Manifest {
    let path = manifest_dir.join("Cargo.toml");
    let text = std::fs::read_to_string(&path)
        .unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
    toml::from_str(&text).unwrap_or_else(|e| panic!("cannot parse {}: {e}", path.display()))
}

/// Present only in the repository. The published crate needs no lookups.
fn read_workspace(manifest_dir: &Path) -> Option<BTreeMap<String, Dep>> {
    let path = manifest_dir.join("../../Cargo.toml");
    let text = std::fs::read_to_string(&path).ok()?;
    let parsed: WorkspaceManifest =
        toml::from_str(&text).unwrap_or_else(|e| panic!("cannot parse {}: {e}", path.display()));
    Some(parsed.workspace.dependencies)
}

/// A `workspace = true` entry takes the workspace version and features, plus the features the
/// crate adds on top, which is what cargo does for the build itself.
fn resolve(name: &str, dep: &Dep, workspace: Option<&BTreeMap<String, Dep>>) -> Dep {
    let Dep::Table(table) = dep else {
        return dep.clone();
    };
    if !table.workspace {
        return dep.clone();
    }
    let base = workspace
        .and_then(|deps| deps.get(name))
        .unwrap_or_else(|| {
            panic!("`{name}` inherits from the workspace but the workspace manifest has no entry")
        });
    let mut merged = match base {
        Dep::Version(version) => DepTable {
            version: Some(version.clone()),
            ..DepTable::default()
        },
        Dep::Table(base) => base.clone(),
    };
    merged.workspace = false;
    merged.features.extend(table.features.iter().cloned());
    if table.default_features.is_some() {
        merged.default_features = table.default_features;
    }
    if merged.features.is_empty() && merged.default_features.is_none() {
        return Dep::Version(merged.version.unwrap_or_default());
    }
    Dep::Table(merged)
}