Skip to content

Commit 9efd454

Browse files
fix(grammar): build grammar on CI (#698)
1 parent 01727fa commit 9efd454

12 files changed

Lines changed: 136 additions & 1228173 deletions

File tree

.github/workflows/pull_request.yml

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,12 @@ jobs:
132132
~/.cargo/bin/tree-sitter
133133
~/.cargo/bin/sqlx
134134
~/.cargo/bin/cargo-sqlx
135-
key: ${{ runner.os }}-tree-sitter-${{ hashFiles('rust-toolchain.toml') }}
135+
target/**/build/pgls_treesitter_grammar-*/out/generated
136+
key: ${{ runner.os }}-tree-sitter-${{ hashFiles('.tree-sitter-cli-version', 'crates/pgls_treesitter_grammar/grammar.js', 'crates/pgls_treesitter_grammar/tree-sitter.json', 'rust-toolchain.toml', 'Cargo.lock') }}
136137

137138
- name: Setup tree-sitter
138-
run: command -v tree-sitter || cargo install tree-sitter-cli
139+
run: command -v tree-sitter || cargo install tree-sitter-cli --version "$(cat .tree-sitter-cli-version)" --locked
140+
shell: bash
139141

140142
- name: Setup sqlx-cli
141143
run: command -v sqlx ||cargo install sqlx-cli
@@ -199,10 +201,12 @@ jobs:
199201
with:
200202
path: |
201203
~/.cargo/bin/tree-sitter
202-
key: ${{ runner.os }}-tree-sitter-${{ hashFiles('rust-toolchain.toml') }}
204+
target/**/build/pgls_treesitter_grammar-*/out/generated
205+
key: ${{ runner.os }}-tree-sitter-${{ hashFiles('.tree-sitter-cli-version', 'crates/pgls_treesitter_grammar/grammar.js', 'crates/pgls_treesitter_grammar/tree-sitter.json', 'rust-toolchain.toml', 'Cargo.lock') }}
203206

204-
- name: Setup Postgres
205-
run: command -v tree-sitter || cargo install tree-sitter-cli
207+
- name: Setup tree-sitter
208+
run: command -v tree-sitter || cargo install tree-sitter-cli --version "$(cat .tree-sitter-cli-version)" --locked
209+
shell: bash
206210

207211
- name: Run tests
208212
run: cargo test --workspace
@@ -236,9 +240,11 @@ jobs:
236240
with:
237241
path: |
238242
~/.cargo/bin/tree-sitter
239-
key: ${{ runner.os }}-tree-sitter-${{ hashFiles('rust-toolchain.toml') }}
243+
target/**/build/pgls_treesitter_grammar-*/out/generated
244+
key: ${{ runner.os }}-tree-sitter-${{ hashFiles('.tree-sitter-cli-version', 'crates/pgls_treesitter_grammar/grammar.js', 'crates/pgls_treesitter_grammar/tree-sitter.json', 'rust-toolchain.toml', 'Cargo.lock') }}
240245
- name: setup tree-sitter
241-
run: command -v tree-sitter || cargo install tree-sitter-cli
246+
run: command -v tree-sitter || cargo install tree-sitter-cli --version "$(cat .tree-sitter-cli-version)" --locked
247+
shell: bash
242248
- name: Build main binary
243249
run: cargo build -p pgls_cli --release
244250
- name: Setup Bun
@@ -324,9 +330,11 @@ jobs:
324330
with:
325331
path: |
326332
~/.cargo/bin/tree-sitter
327-
key: ${{ runner.os }}-tree-sitter-${{ hashFiles('rust-toolchain.toml') }}
333+
target/**/build/pgls_treesitter_grammar-*/out/generated
334+
key: ${{ runner.os }}-tree-sitter-${{ hashFiles('.tree-sitter-cli-version', 'crates/pgls_treesitter_grammar/grammar.js', 'crates/pgls_treesitter_grammar/tree-sitter.json', 'rust-toolchain.toml', 'Cargo.lock') }}
328335
- name: setup tree-sitter
329-
run: command -v tree-sitter || cargo install tree-sitter-cli
336+
run: command -v tree-sitter || cargo install tree-sitter-cli --version "$(cat .tree-sitter-cli-version)" --locked
337+
shell: bash
330338
- name: Ensure RustFMT on nightly toolchain
331339
run: rustup component add rustfmt --toolchain nightly
332340
- name: Setup Bun

.github/workflows/release.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,12 @@ jobs:
120120
with:
121121
path: |
122122
~/.cargo/bin/tree-sitter
123-
key: ${{ runner.os }}-tree-sitter-${{ hashFiles('rust-toolchain.toml') }}
123+
target/**/build/pgls_treesitter_grammar-*/out/generated
124+
key: ${{ runner.os }}-tree-sitter-${{ hashFiles('.tree-sitter-cli-version', 'crates/pgls_treesitter_grammar/grammar.js', 'crates/pgls_treesitter_grammar/tree-sitter.json', 'rust-toolchain.toml', 'Cargo.lock') }}
124125

125126
- name: Setup tree-sitter
126-
run: command -v tree-sitter || cargo install tree-sitter-cli
127+
run: command -v tree-sitter || cargo install tree-sitter-cli --version "$(cat .tree-sitter-cli-version)" --locked
128+
shell: bash
127129

128130
- name: Setup Postgres
129131
uses: ./.github/actions/setup-postgres

.tree-sitter-cli-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0.25.9

crates/pgls_treesitter_grammar/Cargo.toml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 106 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,135 @@
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+
16
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+
);
526

6-
// Detect Emscripten target for WASM builds
27+
// Detect Emscripten target for WASM builds.
728
let target = std::env::var("TARGET").unwrap_or_default();
829
let is_emscripten = target.contains("emscripten");
930

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");
3340
}
3441

3542
let mut c_config = cc::Build::new();
3643

37-
// Use Emscripten compiler for WASM builds
44+
// Use Emscripten compiler for WASM builds.
3845
if is_emscripten {
3946
c_config.compiler("emcc").archiver("emar");
4047
}
4148

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);
4355

4456
#[cfg(target_env = "msvc")]
4557
c_config.flag("-utf-8");
4658

4759
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");
5160
if scanner_path.exists() {
5261
c_config.file(&scanner_path);
53-
println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap());
5462
}
5563

5664
c_config.compile("tree_sitter_pgls");
5765
}
5866

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);
6276
}
6377

64-
let modified1 = file1.metadata().unwrap().modified().unwrap();
65-
let modified2 = file2.metadata().unwrap().modified().unwrap();
78+
format!("{:016x}", hasher.finish())
79+
}
6680

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+
}
68135
}

0 commit comments

Comments
 (0)