1
//! Tangle the canonical native fn registry from
2
//! =doc/scripting/native_reference.org= into
3
//! `src/natives/generated_specs.rs`. The org file is the source of
4
//! truth — this script is the build-time bridge cargo invokes
5
//! before compiling the crate.
6
//!
7
//! Mirrors the `scripting/format/build.rs` precedent: `emacs
8
//! --batch` evaluates the named babel block, which writes the Rust
9
//! file at a fixed path. `cargo build` is the only command needed;
10
//! editing the org and re-running `cargo build` regenerates the
11
//! tangled source (`cargo:rerun-if-changed=` on the org file
12
//! drives the incremental rebuild).
13

            
14
use std::path::Path;
15
use std::process::Command;
16

            
17
4
fn main() {
18
4
    let org_path = "../doc/scripting/native_reference.org";
19
4
    println!("cargo:rerun-if-changed={org_path}");
20

            
21
4
    let status = Command::new("emacs")
22
4
        .args([
23
4
            "-q",
24
4
            "--batch",
25
4
            org_path,
26
4
            "--eval",
27
4
            "(setq org-confirm-babel-evaluate nil create-lockfiles nil)",
28
4
            "--eval",
29
4
            "(org-babel-goto-named-src-block \"emit-rust\")",
30
4
            "--eval",
31
4
            "(org-babel-execute-src-block)",
32
4
            "-f",
33
4
            "kill-emacs",
34
4
        ])
35
4
        .status();
36

            
37
4
    match status {
38
4
        Ok(s) if s.success() => {}
39
        Ok(s) => panic!(
40
            "emacs --batch tangle of native_reference.org exited with {s} \
41
             — regen of rpc/src/natives/generated_specs.rs failed"
42
        ),
43
        Err(e) => panic!(
44
            "failed to spawn emacs for native_reference.org tangle: {e}. \
45
             cargo build requires emacs on PATH (same as finance/build.rs)"
46
        ),
47
    }
48

            
49
    // The babel block writes raw Rust without consulting rustfmt; run
50
    // rustfmt here so the file's whitespace matches the rest of the
51
    // tree. Without this, `cargo fmt --all --check` flags the tangled
52
    // file and the pre-commit hook refuses every commit.
53
4
    rustfmt_generated("src/natives/generated_specs.rs");
54
4
}
55

            
56
/// Formats a generated source so it matches `cargo fmt --all --check`.
57
/// rustfmt is resolved from the active toolchain's sysroot (where
58
/// `cargo fmt` finds it), not bare `$PATH`: minimal CI images expose
59
/// rustfmt only in the sysroot bin, so `Command::new("rustfmt")` would
60
/// spawn-fail there and ship the file unformatted, breaking the later
61
/// fmt check. A genuine failure is surfaced as a build warning, not
62
/// silently swallowed.
63
4
fn rustfmt_generated(path: &str) {
64
4
    match rustfmt_command().arg("--edition=2024").arg(path).status() {
65
4
        Ok(s) if s.success() => {}
66
        Ok(s) => println!("cargo:warning=rustfmt exited {s} on {path}"),
67
        Err(e) => println!("cargo:warning=could not run rustfmt on {path}: {e}"),
68
    }
69
4
}
70

            
71
4
fn rustfmt_command() -> Command {
72
    // Probe the compiler Cargo set for this build (`$RUSTC`), not bare
73
    // `rustc`, so the sysroot matches the active toolchain.
74
4
    let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_owned());
75
4
    let sysroot_rustfmt = Command::new(rustc)
76
4
        .args(["--print", "sysroot"])
77
4
        .output()
78
4
        .ok()
79
4
        .filter(|out| out.status.success())
80
4
        .map(|out| Path::new(String::from_utf8_lossy(&out.stdout).trim()).join("bin/rustfmt"))
81
4
        .filter(|path| path.exists());
82
4
    sysroot_rustfmt.map_or_else(|| Command::new("rustfmt"), Command::new)
83
4
}