1
use std::fs;
2
use std::path::{Path, PathBuf};
3
use std::process::Command;
4

            
5
/// Where the nested wasm build writes, and where its output is read back from.
6
/// Pinned explicitly because the nested `cargo` would otherwise inherit an
7
/// ambient `CARGO_TARGET_DIR` and drop the artifact somewhere the caller does
8
/// not look.
9
4
fn wasm_target_dir(scripts_dir: &Path) -> PathBuf {
10
4
    scripts_dir.join("target")
11
4
}
12

            
13
4
fn build_rust_wasm(manifest_path: &str, name: &str, target_dir: &Path) {
14
4
    let status = Command::new("cargo")
15
4
        .args([
16
4
            "build",
17
4
            "--target",
18
4
            "wasm32-unknown-unknown",
19
4
            "--release",
20
4
            "--manifest-path",
21
4
            manifest_path,
22
4
        ])
23
4
        .env("CARGO_TARGET_DIR", target_dir)
24
4
        .env_remove("RUSTFLAGS")
25
4
        .env_remove("CARGO_ENCODED_RUSTFLAGS")
26
4
        // The generated script crates are not part of this workspace's lint
27
4
        // surface. Inheriting clippy's wrapper runs clippy-driver over them —
28
4
        // under `-D warnings` that fails the whole build for warnings in
29
4
        // generated code nobody edits. web/build.rs drops the same pair.
30
4
        .env_remove("RUSTC_WRAPPER")
31
4
        .env_remove("RUSTC_WORKSPACE_WRAPPER")
32
4
        .status()
33
4
        .unwrap_or_else(|_| panic!("Failed to build {name} WASM"));
34

            
35
4
    assert!(status.success(), "{name} WASM build failed");
36
4
}
37

            
38
fn compile_nomiscript(nms_path: &Path, wasm_output_dir: &Path) {
39
    let stem = nms_path.file_stem().unwrap().to_str().unwrap();
40
    let wasm_name = stem.replace('-', "_");
41
    let wasm_dest = wasm_output_dir.join(format!("{wasm_name}_nms.wasm"));
42

            
43
    let source = fs::read_to_string(nms_path)
44
        .unwrap_or_else(|_| panic!("Failed to read {}", nms_path.display()));
45
    let program = nomiscript::Reader::parse(&source)
46
        .unwrap_or_else(|e| panic!("Failed to parse {}: {e}", nms_path.display()));
47
    let mut symbols = nomiscript::SymbolTable::with_builtins();
48
    let mut compiler = nomiscript::Compiler::new();
49
    let wasm = compiler
50
        .compile(&program, &mut symbols)
51
        .unwrap_or_else(|e| panic!("Failed to compile {}: {e}", nms_path.display()));
52
    fs::write(&wasm_dest, &wasm)
53
        .unwrap_or_else(|_| panic!("Failed to write {}", wasm_dest.display()));
54
    println!("cargo:warning=Built and installed {wasm_name}_nms.wasm");
55
}
56

            
57
2
fn main() {
58
2
    if std::env::var("MIRI_SYSROOT").is_ok() {
59
        return;
60
2
    }
61

            
62
2
    let manifest_dir = env!("CARGO_MANIFEST_DIR");
63
2
    let scripts_dir = Path::new(manifest_dir)
64
2
        .parent()
65
2
        .unwrap()
66
2
        .join("doc/scripts");
67
2
    let wasm_output_dir = Path::new(manifest_dir)
68
2
        .parent()
69
2
        .unwrap()
70
2
        .join("web/static/wasm");
71

            
72
2
    if !scripts_dir.exists() {
73
        return;
74
2
    }
75

            
76
2
    fs::create_dir_all(&wasm_output_dir).ok();
77

            
78
2
    println!("cargo:rerun-if-changed={}", scripts_dir.display());
79

            
80
2
    let entries: Vec<_> = fs::read_dir(&scripts_dir)
81
2
        .expect("Failed to read doc/scripts directory")
82
2
        .filter_map(Result::ok)
83
2
        .collect();
84

            
85
2
    let org_files: Vec<_> = entries
86
2
        .iter()
87
8
        .filter(|e| e.path().extension().is_some_and(|ext| ext == "org"))
88
2
        .collect();
89

            
90
8
    for entry in &org_files {
91
8
        let org_path = entry.path();
92
8
        println!("cargo:rerun-if-changed={}", org_path.display());
93

            
94
8
        fs::create_dir_all(scripts_dir.join("src")).ok();
95

            
96
8
        let status = Command::new("emacs")
97
8
            .args([
98
8
                "-q",
99
8
                "--batch",
100
8
                "--eval",
101
8
                "(setq org-confirm-babel-evaluate nil create-lockfiles nil)",
102
8
                "--eval",
103
8
                &format!(
104
8
                    "(progn (find-file \"{}\") (org-babel-tangle))",
105
8
                    org_path.display()
106
8
                ),
107
8
            ])
108
8
            .current_dir(&scripts_dir)
109
8
            .status()
110
8
            .expect("Failed to run emacs org-babel-tangle");
111

            
112
8
        assert!(
113
8
            status.success(),
114
            "org-babel-tangle failed for {}",
115
            org_path.display()
116
        );
117

            
118
8
        let cargo_toml = scripts_dir.join("Cargo.toml");
119
8
        if !cargo_toml.exists() {
120
4
            eprintln!(
121
                "Warning: No Cargo.toml generated from {}",
122
4
                org_path.display()
123
            );
124
4
            continue;
125
4
        }
126

            
127
4
        let cargo_content =
128
4
            fs::read_to_string(&cargo_toml).expect("Failed to read generated Cargo.toml");
129
4
        let lib_name = cargo_content
130
4
            .lines()
131
8
            .find(|line| line.trim().starts_with("name = "))
132
4
            .and_then(|line| {
133
4
                let start = line.find('"')? + 1;
134
4
                let end = line.rfind('"')?;
135
4
                Some(&line[start..end])
136
4
            });
137

            
138
4
        let Some(lib_name) = lib_name else {
139
            eprintln!(
140
                "Warning: Could not find lib name in Cargo.toml from {}",
141
                org_path.display()
142
            );
143
            continue;
144
        };
145

            
146
4
        let wasm_name = lib_name.replace('-', "_");
147

            
148
4
        let target_dir = wasm_target_dir(&scripts_dir);
149
4
        build_rust_wasm(cargo_toml.to_str().unwrap(), lib_name, &target_dir);
150

            
151
4
        let wasm_source = target_dir
152
4
            .join("wasm32-unknown-unknown/release")
153
4
            .join(format!("{wasm_name}.wasm"));
154

            
155
4
        if wasm_source.exists() {
156
4
            let wasm_dest = wasm_output_dir.join(format!("{wasm_name}.wasm"));
157
4
            fs::copy(&wasm_source, &wasm_dest).unwrap_or_else(|_| {
158
                panic!(
159
                    "Failed to copy {} to {}",
160
                    wasm_source.display(),
161
                    wasm_dest.display()
162
                )
163
            });
164
4
            println!("cargo:warning=Built and installed {wasm_name}.wasm");
165
        } else {
166
            panic!("WASM file not found: {}", wasm_source.display());
167
        }
168

            
169
4
        fs::remove_file(&cargo_toml).ok();
170
4
        fs::remove_file(scripts_dir.join("Cargo.lock")).ok();
171
4
        fs::remove_dir_all(scripts_dir.join("src")).ok();
172
4
        fs::remove_dir_all(&target_dir).ok();
173
    }
174

            
175
2
    let nms_files: Vec<_> = entries
176
2
        .iter()
177
8
        .filter(|e| e.path().extension().is_some_and(|ext| ext == "nms"))
178
2
        .collect();
179

            
180
2
    for entry in &nms_files {
181
        let nms_path = entry.path();
182
        println!("cargo:rerun-if-changed={}", nms_path.display());
183
        compile_nomiscript(&nms_path, &wasm_output_dir);
184
    }
185
2
}