1
use std::path::Path;
2
use std::process::Command;
3

            
4
2
fn main() {
5
2
    let output = Command::new("git")
6
2
        .args(["rev-parse", "--short", "HEAD"])
7
2
        .output()
8
2
        .expect("Failed to get git revision");
9

            
10
2
    let revision = String::from_utf8(output.stdout)
11
2
        .expect("Invalid UTF-8 in git output")
12
2
        .trim()
13
2
        .to_owned();
14

            
15
2
    println!("cargo:rustc-env=GIT_REVISION={revision}");
16
2
    println!("cargo:rerun-if-changed=.git/HEAD");
17

            
18
2
    tangle_entity_registry();
19
2
}
20

            
21
/// Tangles `doc/scripting/entity_registry.org` →
22
/// `src/compiler/context/entity_registry.rs`. Mirrors the
23
/// `rpc/build.rs` + `scripting/format/build.rs` precedent: the org
24
/// file holds the per-entity field-layout table; the babel block
25
/// emits Rust during `cargo build`. Adding a new entity kind =
26
/// editing one row in the org; cargo regenerates.
27
2
fn tangle_entity_registry() {
28
2
    let org_path = "../../doc/scripting/entity_registry.org";
29
2
    println!("cargo:rerun-if-changed={org_path}");
30

            
31
    // Two tangle blocks share the org file: one emits the
32
    // ENTITY_SPECS const consumed by `new_skeleton` /
33
    // `register_entity_allocators`; the other emits the typed-entity
34
    // accessor natives. Both walk the same per-field rows so the
35
    // struct layout, allocator signature, and accessor surface stay
36
    // in lockstep — adding a field is one row, cargo regenerates
37
    // every consumer.
38
2
    run_emacs_block(org_path, "emit-rust-specs");
39
2
    run_emacs_block(org_path, "emit-rust-accessors");
40
2
    run_emacs_block(org_path, "emit-rust-decode-layout");
41

            
42
    // Builtin symbol-table name lists tangle from a separate org
43
    // (the builtin reference doc holds the canonical operator /
44
    // special-form / native registry).
45
2
    let builtins_org = "../../doc/scripting/builtin_reference.org";
46
2
    println!("cargo:rerun-if-changed={builtins_org}");
47
2
    run_emacs_block(builtins_org, "emit-builtin-names");
48

            
49
8
    for path in [
50
2
        "src/compiler/context/entity_registry.rs",
51
2
        "src/compiler/native/typed_entity.rs",
52
2
        "src/runtime/entity_layout.rs",
53
2
        "src/runtime/symbol/builtins_generated.rs",
54
8
    ] {
55
8
        rustfmt_generated(path);
56
8
    }
57
2
}
58

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

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

            
88
8
fn run_emacs_block(org_path: &str, block_name: &str) {
89
8
    let status = Command::new("emacs")
90
8
        .args([
91
8
            "-q",
92
8
            "--batch",
93
8
            org_path,
94
8
            "--eval",
95
8
            "(setq org-confirm-babel-evaluate nil create-lockfiles nil)",
96
8
            "--eval",
97
8
            &format!("(org-babel-goto-named-src-block {block_name:?})"),
98
8
            "--eval",
99
8
            "(org-babel-execute-src-block)",
100
8
            "-f",
101
8
            "kill-emacs",
102
8
        ])
103
8
        .status();
104

            
105
8
    match status {
106
8
        Ok(s) if s.success() => {}
107
        Ok(s) => panic!(
108
            "emacs --batch tangle of {org_path} block {block_name:?} \
109
             exited with {s} — regen failed"
110
        ),
111
        Err(e) => panic!(
112
            "failed to spawn emacs for {org_path} block {block_name:?} \
113
             tangle: {e}. cargo build requires emacs on PATH"
114
        ),
115
    }
116
8
}