Skip to content

Commit df702e5

Browse files
committed
Add repo-tweak crate for local templating
1 parent 867cfb3 commit df702e5

File tree

3 files changed

+330
-21
lines changed

3 files changed

+330
-21
lines changed

itest/repo-tweak/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[package]
2+
name = "repo-tweak"
3+
version = "0.1.0"
4+
edition = "2021"
5+
publish = false
6+
7+
[dependencies]

itest/repo-tweak/src/main.rs

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
/*
2+
* Copyright (c) godot-rust; Bromeon and contributors.
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
use std::collections::HashMap;
9+
use std::fs::File;
10+
use std::io::Write;
11+
use std::path::Path;
12+
13+
#[rustfmt::skip]
14+
pub const GODOT_LATEST_PATCH_VERSIONS: &[&str] = &[
15+
"4.0.4",
16+
"4.1.4",
17+
"4.2.2",
18+
"4.3.0",
19+
];
20+
21+
// ----------------------------------------------------------------------------------------------------------------------------------------------
22+
23+
fn main() {
24+
let workspace_dir = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../.."));
25+
sync_versions_recursive(workspace_dir, true);
26+
}
27+
28+
fn sync_versions_recursive(parent_dir: &Path, top_level: bool) {
29+
// Iterate recursively
30+
for dir in parent_dir.read_dir().expect("read workspace dir") {
31+
let dir = dir.expect("read dir entry");
32+
let path = dir.path();
33+
34+
if path.is_dir() {
35+
// Only recurse into `godot` and `godot-*` crates.
36+
if !top_level
37+
|| path
38+
.file_name()
39+
.unwrap()
40+
.to_str()
41+
.unwrap()
42+
.starts_with("godot")
43+
{
44+
sync_versions_recursive(&path, false);
45+
}
46+
} else {
47+
// Is a file.
48+
if !matches!(path.extension(), Some(ext) if ext == "rs" || ext == "toml") {
49+
continue;
50+
}
51+
52+
// println!("Check: {}", path.display());
53+
54+
// Replace parts
55+
let content = std::fs::read_to_string(&path).expect("read file");
56+
57+
let keys = ["repeat", "fmt", "pre", "post"];
58+
let ranges = find_repeated_ranges(&content, "[version-sync] [[", "]]", &keys);
59+
60+
let mut last_pos = 0;
61+
if !ranges.is_empty() {
62+
println!("-> Replace: {}", path.display());
63+
// continue;
64+
65+
let mut file = File::create(path).expect("create file");
66+
67+
for m in ranges {
68+
println!(" -> Found {m:?}");
69+
70+
file.write_all(content[last_pos..m.start].as_bytes())
71+
.expect("write file (before start)");
72+
73+
// Note: m.start..m.end is discarded and replaced with newly generated lines.
74+
let (replaced, pre, post) = substitute_template(&m.key_values);
75+
if let Some(pre) = pre {
76+
write_newline(&mut file);
77+
file.write_all(pre.as_bytes()).expect("write file (pre)");
78+
}
79+
80+
for line in replaced {
81+
// Write newline before.
82+
write_newline(&mut file);
83+
file.write_all(line.as_bytes())
84+
.expect("write file (generated line)");
85+
}
86+
87+
if let Some(post) = post {
88+
write_newline(&mut file);
89+
file.write_all(post.as_bytes()).expect("write file (post)");
90+
}
91+
92+
file.write_all(content[m.end..m.after_end].as_bytes())
93+
.expect("write file (after end)");
94+
95+
last_pos = m.after_end;
96+
}
97+
98+
file.write_all(content[last_pos..].as_bytes())
99+
.expect("write to file (end)");
100+
}
101+
}
102+
}
103+
}
104+
105+
fn write_newline(file: &mut File) {
106+
file.write_all(b"\n").expect("write file (newline)")
107+
}
108+
109+
/// For a given template, generate lines of content to be filled.
110+
fn substitute_template(
111+
key_values: &HashMap<String, String>,
112+
) -> (Vec<String>, Option<String>, Option<String>) {
113+
let template = key_values
114+
.get("fmt")
115+
.expect("version-sync: missing required [fmt] key");
116+
let template = apply_char_substitutions(template);
117+
118+
let versions_max = latest_patch_versions();
119+
let mut applicable_versions = vec![];
120+
121+
let parts = key_values
122+
.get("repeat")
123+
.expect("version-sync: missing required [repeat] key")
124+
.split('+');
125+
for part in parts {
126+
let current_minor = versions_max[versions_max.len() - 2].0;
127+
128+
let filter: Box<dyn Fn(u8, u8) -> bool> = match part {
129+
"past" => Box::new(|m, _p| m < current_minor),
130+
"current" => Box::new(|m, _p| m == current_minor),
131+
"future" => Box::new(|m, _p| m > current_minor),
132+
"current.minor" => Box::new(|m, p| m == current_minor && p == 0),
133+
134+
other => {
135+
panic!("version-sync: invalid value '{other}' for [repeat] key")
136+
}
137+
};
138+
139+
for (minor, highest_patch) in versions_max.iter().copied() {
140+
for patch in 0..=highest_patch {
141+
if filter(minor, patch) {
142+
applicable_versions.push((minor, patch));
143+
}
144+
}
145+
}
146+
}
147+
148+
// Apply variable substitutions.
149+
let substituted = applicable_versions
150+
.into_iter()
151+
.map(|(minor, patch)| {
152+
if patch == 0 {
153+
template
154+
.replace("$dotVersion", &format!("4.{minor}"))
155+
.replace("$kebabVersion", &format!("4-{minor}"))
156+
.replace("$snakeVersion", &format!("4_{minor}"))
157+
.replace("$triple", &format!("(4, {minor}, 0)"))
158+
} else {
159+
template
160+
.replace("$dotVersion", &format!("4.{minor}.{patch}"))
161+
.replace("$kebabVersion", &format!("4-{minor}-{patch}"))
162+
.replace("$snakeVersion", &format!("4_{minor}_{patch}"))
163+
.replace("$triple", &format!("(4, {minor}, {patch})"))
164+
}
165+
})
166+
.collect();
167+
168+
// Pre/post are needed because e.g. within #[cfg], no comments are possible.
169+
let pre = key_values.get("pre").map(|s| apply_char_substitutions(s));
170+
let post = key_values.get("post").map(|s| apply_char_substitutions(s));
171+
172+
(substituted, pre, post)
173+
}
174+
175+
fn apply_char_substitutions(s: &str) -> String {
176+
s.replace("\\t", "\t").replace("\\n", "\n")
177+
}
178+
179+
fn find_repeated_ranges(entire: &str, start_pat: &str, end_pat: &str, keys: &[&str]) -> Vec<Match> {
180+
let mut search_start = 0;
181+
let mut found = vec![];
182+
183+
while let Some(start) = entire[search_start..].find(start_pat) {
184+
let before_start = search_start + start;
185+
let start = before_start + start_pat.len();
186+
187+
let mut key_values = HashMap::new();
188+
189+
let Some(end) = entire[start..].find(end_pat) else {
190+
panic!("unmatched start pattern '{start_pat}' without end");
191+
};
192+
193+
let end = start + end;
194+
// Rewind until previous newline.
195+
let end = entire[..end].rfind('\n').unwrap_or(end);
196+
let after_end = end + end_pat.len();
197+
let within = &entire[start..end];
198+
199+
let mut after_keys = start;
200+
for key in keys {
201+
let key_fmt = format!("[{key}] ");
202+
203+
println!(" Find '{key_fmt}' -> {:?}", within.find(&key_fmt));
204+
205+
let Some(pos) = within.find(&key_fmt) else {
206+
continue;
207+
};
208+
209+
let pos = pos + key_fmt.len();
210+
211+
// Read until end of line -> that's the value.
212+
let eol = within[pos..]
213+
.find(&['\n', '\r'])
214+
.expect("unterminated line for key");
215+
216+
let value = &within[pos..pos + eol];
217+
key_values.insert(key.to_string(), value.to_string());
218+
219+
after_keys = after_keys.max(start + pos + eol);
220+
}
221+
222+
println!("Found {start}..{end}");
223+
found.push(Match {
224+
before_start,
225+
start: after_keys,
226+
end,
227+
after_end,
228+
key_values,
229+
});
230+
search_start = after_end;
231+
}
232+
233+
found
234+
}
235+
236+
#[derive(Debug)]
237+
struct Match {
238+
/// Position before pattern start marker.
239+
before_start: usize,
240+
/// Position at the beginning of the repetition (after marker + keys).
241+
start: usize,
242+
/// Position 1 past the end of the repetition.
243+
end: usize,
244+
/// Position after the end pattern marker.
245+
after_end: usize,
246+
/// Extra keys following the start pattern marker.
247+
key_values: HashMap<String, String>,
248+
}
249+
250+
fn latest_patch_versions() -> Vec<(u8, u8)> {
251+
GODOT_LATEST_PATCH_VERSIONS
252+
.iter()
253+
.map(|v| {
254+
let mut parts = v.split('.');
255+
let _major: u8 = parts.next().unwrap().parse().unwrap();
256+
let minor = parts.next().unwrap().parse().unwrap();
257+
let patch = parts.next().unwrap().parse().unwrap();
258+
(minor, patch)
259+
})
260+
.collect()
261+
}

itest/rust/build.rs

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
use proc_macro2::TokenStream;
99
use quote::{format_ident, quote};
10+
use std::collections::HashMap;
1011
use std::io::Write;
1112
use std::path::Path;
1213

@@ -560,7 +561,7 @@ fn write_gdscript_code(
560561
// let (mut last_start, mut prev_end) = (0, 0);
561562
let mut last = 0;
562563

563-
let ranges = find_repeated_ranges(&template);
564+
let ranges = find_repeated_ranges(&template, "#(", "#)", &[]);
564565
for m in ranges {
565566
file.write_all(template[last..m.before_start].as_bytes())?;
566567

@@ -600,39 +601,79 @@ fn replace_parts(
600601
Ok(())
601602
}
602603

603-
fn find_repeated_ranges(entire: &str) -> Vec<Match> {
604-
const START_PAT: &str = "#(";
605-
const END_PAT: &str = "#)";
606-
604+
// FIXME deduplicate
605+
fn find_repeated_ranges(entire: &str, start_pat: &str, end_pat: &str, keys: &[&str]) -> Vec<Match> {
607606
let mut search_start = 0;
608607
let mut found = vec![];
609-
while let Some(start) = entire[search_start..].find(START_PAT) {
608+
609+
while let Some(start) = entire[search_start..].find(start_pat) {
610610
let before_start = search_start + start;
611-
let start = before_start + START_PAT.len();
612-
if let Some(end) = entire[start..].find(END_PAT) {
613-
let end = start + end;
614-
let after_end = end + END_PAT.len();
615-
616-
println!("Found {start}..{end}");
617-
found.push(Match {
618-
before_start,
619-
start,
620-
end,
621-
after_end,
622-
});
623-
search_start = after_end;
624-
} else {
625-
panic!("unmatched start pattern without end");
611+
let start = before_start + start_pat.len();
612+
613+
let mut key_values = HashMap::new();
614+
615+
let Some(end) = entire[start..].find(end_pat) else {
616+
panic!("unmatched start pattern '{start_pat}' without end");
617+
};
618+
619+
let end = start + end;
620+
// Rewind until previous newline.
621+
let end = entire[..end].rfind('\n').unwrap_or(end);
622+
623+
let after_end = end + end_pat.len();
624+
625+
let within = &entire[start..end];
626+
println!("Within: <<{within}>>");
627+
628+
let mut after_keys = start;
629+
for key in keys {
630+
let key_fmt = format!("[{key}] ");
631+
632+
println!(" Find '{key_fmt}' -> {:?}", within.find(&key_fmt));
633+
634+
let Some(pos) = within.find(&key_fmt) else {
635+
continue;
636+
};
637+
638+
let pos = pos + key_fmt.len();
639+
640+
// Read until end of line -> that's the value.
641+
let eol = within[pos..]
642+
.find(&['\n', '\r'])
643+
.expect("unterminated line for key");
644+
645+
let value = &within[pos..pos + eol];
646+
key_values.insert(key.to_string(), value.to_string());
647+
648+
after_keys = after_keys.max(start + pos + eol);
626649
}
650+
651+
println!("Found {start}..{end}");
652+
found.push(Match {
653+
before_start,
654+
start: after_keys,
655+
end,
656+
after_end,
657+
key_values,
658+
});
659+
search_start = after_end;
627660
}
628661

629662
found
630663
}
631664

665+
// FIXME deduplicate
632666
#[derive(Debug)]
633667
struct Match {
668+
/// Position before pattern start marker.
634669
before_start: usize,
670+
/// Position at the beginning of the repetition (after marker + keys).
635671
start: usize,
672+
/// Position 1 past the end of the repetition.
636673
end: usize,
674+
/// Position after the end pattern marker.
637675
after_end: usize,
676+
/// Extra keys following the start pattern marker.
677+
#[allow(dead_code)]
678+
key_values: HashMap<String, String>,
638679
}

0 commit comments

Comments
 (0)