alef 0.83.1

Opinionated polyglot binding generator for Rust libraries
Documentation
//! Root-level Cargo scaffold files: the `rust-toolchain.toml` seed and the
//! `.cargo/config.toml` renderer for a configured `[scaffold.cargo]` block.

use crate::core::backend::GeneratedFile;
use crate::core::config::{Language, ScaffoldCargo, ScaffoldCargoEnvValue};
use crate::scaffold::template_env;

/// The `rust-toolchain.toml` seed. Split out of [`scaffold`] for the same CWD reason as
/// `wasm_cargo_config_file` (`languages::wasm`) — alef's own repo root carries a
/// `rust-toolchain.toml`, so every
/// test that reaches this through `scaffold` sees the file suppressed and asserts nothing. ~keep
pub(crate) fn rust_toolchain_file(languages: &[Language]) -> GeneratedFile {
    let targets = if languages.contains(&Language::Wasm) {
        "targets = [\"wasm32-unknown-unknown\"]\n"
    } else {
        ""
    };
    GeneratedFile {
        path: std::path::PathBuf::from("rust-toolchain.toml"),
        content: format!(
            "[toolchain]\nchannel = \"1.95\"\ncomponents = [\"rust-src\", \"rustfmt\", \"clippy\"]\n{targets}"
        ),
        // Was `false`, which meant `ensure_generated_header` never ran and the file reached disk
        // with no marker: alef wrote every byte of it, `finalize_hashes` skipped it for want of a
        // marker to inject after, and poly's hash-keyed skip therefore reformatted it on every
        // run. Gated on non-existence, so flipping this can only affect a create. ~keep
        generated_header: true,
    }
}

/// Render the canonical workspace `.cargo/config.toml` from a `[scaffold.cargo]`
/// configuration block.
///
/// The output is deterministic (same config → byte-identical output) and includes
/// the `auto-generated by alef` marker so `finalize_hashes` will stamp the
/// `alef:hash:` line during the scaffold pipeline.
///
/// Section order is fixed: header comment → `[build]` → `[net]` →
/// `[registries.crates-io]` → `[target.*]` blocks (in declaration order:
/// macOS dynamic_lookup, Windows MSVC x64+i686, aarch64-linux-gnu, x86_64-linux-musl,
/// wasm32) → optional `[env]`. `inject_hash_line` will insert the hash comment
/// directly after the marker line.
pub fn render_cargo_config(cargo: &ScaffoldCargo) -> String {
    let mut out = String::new();
    out.push_str("# This file is auto-generated by alef. DO NOT EDIT.\n");
    out.push_str("# Re-generate with: alef scaffold\n");
    out.push('\n');
    out.push_str("[build]\nincremental = true\n");
    if cargo.build_jobs > 0 {
        out.push_str(&format!("jobs = {}\n", cargo.build_jobs));
    }
    if let Some(wrapper) = cargo.rustc_wrapper.as_deref() {
        out.push_str(&format!("rustc-wrapper = \"{}\"\n", escape_toml_string(wrapper)));
    }
    out.push('\n');
    out.push_str("[net]\ngit-fetch-with-cli = true\n\n");
    out.push_str("[registries.crates-io]\nprotocol = \"sparse\"\n");

    let t = &cargo.targets;
    if t.macos_dynamic_lookup {
        out.push_str(
            "\n# Required for PyO3 / ext-php-rs cdylibs: Python and Zend C-API symbols are\n\
             # resolved at runtime when the host loads the extension, not at link time.\n\
             # macOS ld is strict and rejects unresolved symbols by default.\n\
             [target.'cfg(target_os = \"macos\")']\n\
             rustflags = [\"-C\", \"link-arg=-Wl,-undefined,dynamic_lookup\"]\n",
        );
    }
    if t.x86_64_pc_windows_msvc {
        out.push_str("\n[target.x86_64-pc-windows-msvc]\nlinker = \"rust-lld\"\n");
    }
    if t.i686_pc_windows_msvc {
        out.push_str("\n[target.i686-pc-windows-msvc]\nlinker = \"rust-lld\"\n");
    }
    if t.aarch64_unknown_linux_gnu {
        out.push_str("\n[target.aarch64-unknown-linux-gnu]\nlinker = \"aarch64-linux-gnu-gcc\"\n");
    }
    if t.x86_64_unknown_linux_musl {
        out.push_str("\n[target.x86_64-unknown-linux-musl]\nlinker = \"musl-gcc\"\n");
    }
    if t.wasm32_unknown_unknown {
        out.push_str(
            "\n[target.wasm32-unknown-unknown]\n\
             rustflags = [\"-C\", \"target-feature=+bulk-memory\", \"--cfg\", \"getrandom_backend=\\\"wasm_js\\\"\", \"-C\", \"link-arg=--allow-multiple-definition\"]\n",
        );
    }

    if !cargo.env.is_empty() {
        out.push_str("\n[env]\n");
        let mut keys: Vec<&String> = cargo.env.keys().collect();
        keys.sort();
        for key in keys {
            let value = &cargo.env[key];
            match value {
                ScaffoldCargoEnvValue::Plain(s) => {
                    out.push_str(&template_env::render(
                        "cargo_env_plain.jinja",
                        minijinja::context! { key => key, value => escape_toml_string(s) },
                    ));
                }
                ScaffoldCargoEnvValue::Structured { value, relative } => {
                    out.push_str(&template_env::render(
                        "cargo_env_structured.jinja",
                        minijinja::context! {
                            key => key,
                            value => escape_toml_string(value),
                            relative => if *relative { "true" } else { "false" },
                        },
                    ));
                }
            }
        }
    }

    out
}

/// Escape a string for TOML basic-string syntax: backslash + double-quote only.
/// (Tabs/newlines are preserved as-is — typical Cargo config values don't contain them.)
fn escape_toml_string(s: &str) -> String {
    s.replace('\\', "\\\\").replace('"', "\\\"")
}