|
| 1 | +use std::collections::hash_map::DefaultHasher; |
| 2 | +use std::hash::{Hash, Hasher}; |
| 3 | +use std::path::{Path, PathBuf}; |
| 4 | +use std::process::Command; |
| 5 | + |
1 | 6 | fn main() { |
2 | | - let grammar_file = std::path::Path::new("grammar.js"); |
3 | | - let src_dir = std::path::Path::new("src"); |
4 | | - let parser_path = src_dir.join("parser.c"); |
| 7 | + let manifest_dir = |
| 8 | + PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("Missing CARGO_MANIFEST_DIR")); |
| 9 | + let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("Missing OUT_DIR")); |
| 10 | + let grammar_file = manifest_dir.join("grammar.js"); |
| 11 | + let config_file = manifest_dir.join("tree-sitter.json"); |
| 12 | + let src_dir = manifest_dir.join("src"); |
| 13 | + let scanner_path = src_dir.join("scanner.c"); |
| 14 | + let generated_dir = out_dir.join("generated"); |
| 15 | + let parser_path = generated_dir.join("parser.c"); |
| 16 | + let node_types_path = generated_dir.join("node-types.json"); |
| 17 | + let stamp_path = generated_dir.join(".stamp"); |
| 18 | + |
| 19 | + println!("cargo:rerun-if-changed={}", grammar_file.display()); |
| 20 | + println!("cargo:rerun-if-changed={}", config_file.display()); |
| 21 | + println!("cargo:rerun-if-changed={}", scanner_path.display()); |
| 22 | + println!( |
| 23 | + "cargo:rerun-if-changed={}", |
| 24 | + manifest_dir.join("build.rs").display() |
| 25 | + ); |
5 | 26 |
|
6 | | - // Detect Emscripten target for WASM builds |
| 27 | + // Detect Emscripten target for WASM builds. |
7 | 28 | let target = std::env::var("TARGET").unwrap_or_default(); |
8 | 29 | let is_emscripten = target.contains("emscripten"); |
9 | 30 |
|
10 | | - // regenerate parser if grammar.js changes |
11 | | - println!("cargo:rerun-if-changed={}", grammar_file.to_str().unwrap()); |
12 | | - |
13 | | - // generate parser if it does not exist. |
14 | | - if !parser_path.exists() || is_file_newer(grammar_file, parser_path.as_path()) { |
15 | | - let output = std::process::Command::new("tree-sitter") |
16 | | - .arg("generate") |
17 | | - .output(); |
18 | | - |
19 | | - match output { |
20 | | - Ok(result) if result.status.success() => { |
21 | | - println!("cargo:warning=Successfully generated parser from grammar.js"); |
22 | | - } |
23 | | - Ok(result) => { |
24 | | - panic!( |
25 | | - "Failed to generate parser: {}", |
26 | | - String::from_utf8_lossy(&result.stderr) |
27 | | - ); |
28 | | - } |
29 | | - Err(_) => { |
30 | | - panic!("tree-sitter CLI not found. Please install it with: `just install`"); |
31 | | - } |
32 | | - } |
| 31 | + std::fs::create_dir_all(&generated_dir).expect("Failed to create generated output directory"); |
| 32 | + |
| 33 | + let stamp = compute_stamp([&grammar_file, &config_file]); |
| 34 | + let should_regenerate = |
| 35 | + !parser_path.exists() || !node_types_path.exists() || read_stamp(&stamp_path) != stamp; |
| 36 | + |
| 37 | + if should_regenerate { |
| 38 | + generate_grammar(&grammar_file, &config_file, &generated_dir); |
| 39 | + std::fs::write(&stamp_path, &stamp).expect("Failed to write grammar stamp"); |
33 | 40 | } |
34 | 41 |
|
35 | 42 | let mut c_config = cc::Build::new(); |
36 | 43 |
|
37 | | - // Use Emscripten compiler for WASM builds |
| 44 | + // Use Emscripten compiler for WASM builds. |
38 | 45 | if is_emscripten { |
39 | 46 | c_config.compiler("emcc").archiver("emar"); |
40 | 47 | } |
41 | 48 |
|
42 | | - c_config.std("c11").include(src_dir); |
| 49 | + // Generated parser.c includes tree_sitter headers from generated_dir/tree_sitter. |
| 50 | + // scanner.c still lives in src/, so both include roots are required. |
| 51 | + c_config |
| 52 | + .std("c11") |
| 53 | + .include(&generated_dir) |
| 54 | + .include(&src_dir); |
43 | 55 |
|
44 | 56 | #[cfg(target_env = "msvc")] |
45 | 57 | c_config.flag("-utf-8"); |
46 | 58 |
|
47 | 59 | c_config.file(&parser_path); |
48 | | - println!("cargo:rerun-if-changed={}", parser_path.to_str().unwrap()); |
49 | | - |
50 | | - let scanner_path = src_dir.join("scanner.c"); |
51 | 60 | if scanner_path.exists() { |
52 | 61 | c_config.file(&scanner_path); |
53 | | - println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap()); |
54 | 62 | } |
55 | 63 |
|
56 | 64 | c_config.compile("tree_sitter_pgls"); |
57 | 65 | } |
58 | 66 |
|
59 | | -fn is_file_newer(file1: &std::path::Path, file2: &std::path::Path) -> bool { |
60 | | - if !file1.exists() || !file2.exists() { |
61 | | - return true; |
| 67 | +fn compute_stamp(files: [&Path; 2]) -> String { |
| 68 | + let mut hasher = DefaultHasher::new(); |
| 69 | + |
| 70 | + for file in files { |
| 71 | + file.as_os_str().hash(&mut hasher); |
| 72 | + let contents = std::fs::read(file).unwrap_or_else(|error| { |
| 73 | + panic!("Failed to read {}: {error}", file.display()); |
| 74 | + }); |
| 75 | + contents.hash(&mut hasher); |
62 | 76 | } |
63 | 77 |
|
64 | | - let modified1 = file1.metadata().unwrap().modified().unwrap(); |
65 | | - let modified2 = file2.metadata().unwrap().modified().unwrap(); |
| 78 | + format!("{:016x}", hasher.finish()) |
| 79 | +} |
66 | 80 |
|
67 | | - modified1 > modified2 |
| 81 | +fn read_stamp(stamp_path: &Path) -> String { |
| 82 | + std::fs::read_to_string(stamp_path) |
| 83 | + .map(|value| value.trim().to_owned()) |
| 84 | + .unwrap_or_default() |
| 85 | +} |
| 86 | + |
| 87 | +fn generate_grammar(grammar_file: &Path, config_file: &Path, generated_dir: &Path) { |
| 88 | + // tree-sitter generate updates tree-sitter.json in its working directory. |
| 89 | + // Use an isolated temp workdir under OUT_DIR to avoid mutating repository files. |
| 90 | + let generator_workdir = generated_dir.join("tree-sitter-workdir"); |
| 91 | + let work_grammar = generator_workdir.join("grammar.js"); |
| 92 | + let work_config = generator_workdir.join("tree-sitter.json"); |
| 93 | + |
| 94 | + let _ = std::fs::remove_dir_all(&generator_workdir); |
| 95 | + std::fs::create_dir_all(&generator_workdir) |
| 96 | + .expect("Failed to create temporary tree-sitter generator workdir"); |
| 97 | + std::fs::copy(grammar_file, &work_grammar).unwrap_or_else(|error| { |
| 98 | + panic!( |
| 99 | + "Failed to copy {} into generator workdir: {error}", |
| 100 | + grammar_file.display() |
| 101 | + ); |
| 102 | + }); |
| 103 | + std::fs::copy(config_file, &work_config).unwrap_or_else(|error| { |
| 104 | + panic!( |
| 105 | + "Failed to copy {} into generator workdir: {error}", |
| 106 | + config_file.display() |
| 107 | + ); |
| 108 | + }); |
| 109 | + |
| 110 | + let output = Command::new("tree-sitter") |
| 111 | + .arg("generate") |
| 112 | + .arg("grammar.js") |
| 113 | + .arg("--output") |
| 114 | + .arg(generated_dir) |
| 115 | + .current_dir(&generator_workdir) |
| 116 | + .output(); |
| 117 | + |
| 118 | + let _ = std::fs::remove_dir_all(&generator_workdir); |
| 119 | + |
| 120 | + match output { |
| 121 | + Ok(result) if result.status.success() => {} |
| 122 | + Ok(result) => { |
| 123 | + panic!( |
| 124 | + "Failed to generate tree-sitter grammar.\nstdout:\n{}\nstderr:\n{}", |
| 125 | + String::from_utf8_lossy(&result.stdout), |
| 126 | + String::from_utf8_lossy(&result.stderr) |
| 127 | + ); |
| 128 | + } |
| 129 | + Err(error) => { |
| 130 | + panic!( |
| 131 | + "tree-sitter CLI not found ({error}). Please install it with: `just install-tools`" |
| 132 | + ); |
| 133 | + } |
| 134 | + } |
68 | 135 | } |
0 commit comments