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

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

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

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

            
18
4
    tangle_entity_registry();
19
4
}
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
4
fn tangle_entity_registry() {
28
4
    let org_path = "../../doc/scripting/entity_registry.org";
29
4
    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
4
    run_emacs_block(org_path, "emit-rust-specs");
39
4
    run_emacs_block(org_path, "emit-rust-accessors");
40
4
    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
4
    let builtins_org = "../../doc/scripting/builtin_reference.org";
46
4
    println!("cargo:rerun-if-changed={builtins_org}");
47
4
    run_emacs_block(builtins_org, "emit-builtin-names");
48

            
49
16
    for path in [
50
4
        "src/compiler/context/entity_registry.rs",
51
4
        "src/compiler/native/typed_entity.rs",
52
4
        "src/runtime/entity_layout.rs",
53
4
        "src/runtime/symbol/builtins_generated.rs",
54
16
    ] {
55
16
        rustfmt_generated(path);
56
16
    }
57
4
}
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
16
fn rustfmt_generated(path: &str) {
67
16
    match rustfmt_command().arg("--edition=2024").arg(path).status() {
68
16
        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
16
}
73

            
74
16
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
16
    let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_owned());
78
16
    let sysroot_rustfmt = Command::new(rustc)
79
16
        .args(["--print", "sysroot"])
80
16
        .output()
81
16
        .ok()
82
16
        .filter(|out| out.status.success())
83
16
        .map(|out| Path::new(String::from_utf8_lossy(&out.stdout).trim()).join("bin/rustfmt"))
84
16
        .filter(|path| path.exists());
85
16
    sysroot_rustfmt.map_or_else(|| Command::new("rustfmt"), Command::new)
86
16
}
87

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

            
105
16
    match status {
106
16
        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
16
}