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

            
5
8
fn build_rust_wasm(manifest_path: &str, name: &str) {
6
8
    let status = Command::new("cargo")
7
8
        .args([
8
8
            "build",
9
8
            "--target",
10
8
            "wasm32-unknown-unknown",
11
8
            "--release",
12
8
            "--manifest-path",
13
8
            manifest_path,
14
8
        ])
15
8
        .env_remove("RUSTFLAGS")
16
8
        .env_remove("CARGO_ENCODED_RUSTFLAGS")
17
8
        .status()
18
8
        .unwrap_or_else(|_| panic!("Failed to build {name} WASM"));
19

            
20
8
    assert!(status.success(), "{name} WASM build failed");
21
8
}
22

            
23
2
fn compile_nomiscript(nms_path: &Path, wasm_output_dir: &Path) {
24
2
    let stem = nms_path.file_stem().unwrap().to_str().unwrap();
25
2
    let wasm_name = stem.replace('-', "_");
26
2
    let wasm_dest = wasm_output_dir.join(format!("{wasm_name}_nms.wasm"));
27

            
28
2
    let source = fs::read_to_string(nms_path)
29
2
        .unwrap_or_else(|_| panic!("Failed to read {}", nms_path.display()));
30
2
    let program = nomiscript::Reader::parse(&source)
31
2
        .unwrap_or_else(|e| panic!("Failed to parse {}: {e}", nms_path.display()));
32
2
    let mut symbols = nomiscript::SymbolTable::with_builtins();
33
2
    let mut compiler = nomiscript::Compiler::new();
34
2
    let wasm = compiler
35
2
        .compile(&program, &mut symbols)
36
2
        .unwrap_or_else(|e| panic!("Failed to compile {}: {e}", nms_path.display()));
37
2
    fs::write(&wasm_dest, &wasm)
38
2
        .unwrap_or_else(|_| panic!("Failed to write {}", wasm_dest.display()));
39
2
    println!("cargo:warning=Built and installed {wasm_name}_nms.wasm");
40
2
}
41

            
42
4
fn main() {
43
4
    if std::env::var("MIRI_SYSROOT").is_ok() {
44
        return;
45
4
    }
46

            
47
4
    let manifest_dir = env!("CARGO_MANIFEST_DIR");
48
4
    let scripts_dir = Path::new(manifest_dir)
49
4
        .parent()
50
4
        .unwrap()
51
4
        .join("doc/scripts");
52
4
    let wasm_output_dir = Path::new(manifest_dir)
53
4
        .parent()
54
4
        .unwrap()
55
4
        .join("web/static/wasm");
56

            
57
4
    if !scripts_dir.exists() {
58
        return;
59
4
    }
60

            
61
4
    fs::create_dir_all(&wasm_output_dir).ok();
62

            
63
4
    println!("cargo:rerun-if-changed={}", scripts_dir.display());
64

            
65
4
    let entries: Vec<_> = fs::read_dir(&scripts_dir)
66
4
        .expect("Failed to read doc/scripts directory")
67
4
        .filter_map(Result::ok)
68
4
        .collect();
69

            
70
4
    let org_files: Vec<_> = entries
71
4
        .iter()
72
14
        .filter(|e| e.path().extension().is_some_and(|ext| ext == "org"))
73
4
        .collect();
74

            
75
12
    for entry in &org_files {
76
12
        let org_path = entry.path();
77
12
        println!("cargo:rerun-if-changed={}", org_path.display());
78

            
79
12
        fs::create_dir_all(scripts_dir.join("src")).ok();
80

            
81
12
        let status = Command::new("emacs")
82
12
            .args([
83
12
                "-q",
84
12
                "--batch",
85
12
                "--eval",
86
12
                "(setq org-confirm-babel-evaluate nil)",
87
12
                "--eval",
88
12
                &format!(
89
12
                    "(progn (find-file \"{}\") (org-babel-tangle))",
90
12
                    org_path.display()
91
12
                ),
92
12
            ])
93
12
            .current_dir(&scripts_dir)
94
12
            .status()
95
12
            .expect("Failed to run emacs org-babel-tangle");
96

            
97
12
        assert!(
98
12
            status.success(),
99
            "org-babel-tangle failed for {}",
100
            org_path.display()
101
        );
102

            
103
12
        let cargo_toml = scripts_dir.join("Cargo.toml");
104
12
        if !cargo_toml.exists() {
105
4
            eprintln!(
106
                "Warning: No Cargo.toml generated from {}",
107
4
                org_path.display()
108
            );
109
4
            continue;
110
8
        }
111

            
112
8
        let cargo_content =
113
8
            fs::read_to_string(&cargo_toml).expect("Failed to read generated Cargo.toml");
114
8
        let lib_name = cargo_content
115
8
            .lines()
116
16
            .find(|line| line.trim().starts_with("name = "))
117
8
            .and_then(|line| {
118
8
                let start = line.find('"')? + 1;
119
8
                let end = line.rfind('"')?;
120
8
                Some(&line[start..end])
121
8
            });
122

            
123
8
        let Some(lib_name) = lib_name else {
124
            eprintln!(
125
                "Warning: Could not find lib name in Cargo.toml from {}",
126
                org_path.display()
127
            );
128
            continue;
129
        };
130

            
131
8
        let wasm_name = lib_name.replace('-', "_");
132

            
133
8
        build_rust_wasm(cargo_toml.to_str().unwrap(), lib_name);
134

            
135
8
        let wasm_source = scripts_dir
136
8
            .join("target/wasm32-unknown-unknown/release")
137
8
            .join(format!("{wasm_name}.wasm"));
138

            
139
8
        if wasm_source.exists() {
140
8
            let wasm_dest = wasm_output_dir.join(format!("{wasm_name}.wasm"));
141
8
            fs::copy(&wasm_source, &wasm_dest).unwrap_or_else(|_| {
142
                panic!(
143
                    "Failed to copy {} to {}",
144
                    wasm_source.display(),
145
                    wasm_dest.display()
146
                )
147
            });
148
8
            println!("cargo:warning=Built and installed {wasm_name}.wasm");
149
        } else {
150
            panic!("WASM file not found: {}", wasm_source.display());
151
        }
152

            
153
8
        fs::remove_file(&cargo_toml).ok();
154
8
        fs::remove_file(scripts_dir.join("Cargo.lock")).ok();
155
8
        fs::remove_dir_all(scripts_dir.join("src")).ok();
156
8
        fs::remove_dir_all(scripts_dir.join("target")).ok();
157
    }
158

            
159
4
    let nms_files: Vec<_> = entries
160
4
        .iter()
161
14
        .filter(|e| e.path().extension().is_some_and(|ext| ext == "nms"))
162
4
        .collect();
163

            
164
4
    for entry in &nms_files {
165
2
        let nms_path = entry.path();
166
2
        println!("cargo:rerun-if-changed={}", nms_path.display());
167
2
        compile_nomiscript(&nms_path, &wasm_output_dir);
168
2
    }
169
4
}