-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuild.rs
More file actions
155 lines (143 loc) · 5.36 KB
/
Copy pathbuild.rs
File metadata and controls
155 lines (143 loc) · 5.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
struct Guide {
topic: String,
title: String,
body: String,
path: PathBuf,
}
fn main() {
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
let guide_dir = manifest_dir.join("docs/guides");
println!("cargo:rerun-if-changed={}", guide_dir.display());
let mut guides = discover_guides(&guide_dir);
guides.sort_by(|left, right| left.topic.cmp(&right.topic));
validate_guides(&guides);
let generated = render_topics(&guides);
let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
fs::write(out_dir.join("guide_topics.rs"), generated)
.expect("failed to write generated guide topic table");
embed_profiles(
&manifest_dir.join("guard-profiles"),
&out_dir.join("guard_profiles.rs"),
"PACKAGED_GUARD_PROFILES",
);
embed_profiles(
&manifest_dir.join("ignore-profiles"),
&out_dir.join("ignore_profiles.rs"),
"PACKAGED_IGNORE_PROFILES",
);
}
/// Render one directory of TOML profiles as a `&[(filename, body)]` table the
/// crate `include!`s. Both packaged profile families — guard command policy and
/// tool-ignore targets — ship this way, so a new profile is a new file and
/// nothing else.
fn embed_profiles(profile_dir: &Path, out_file: &Path, table_name: &str) {
println!("cargo:rerun-if-changed={}", profile_dir.display());
let mut profiles: Vec<PathBuf> = fs::read_dir(profile_dir)
.unwrap_or_else(|err| panic!("failed to read {}: {err}", profile_dir.display()))
.map(|entry| entry.expect("failed to read profile entry").path())
.filter(|path| path.extension().and_then(|value| value.to_str()) == Some("toml"))
.collect();
profiles.sort();
assert!(
!profiles.is_empty(),
"{} must contain a TOML profile",
profile_dir.display()
);
let mut generated = format!("const {table_name}: &[(&str, &str)] = &[\n");
for path in profiles {
let name = path
.file_name()
.and_then(|value| value.to_str())
.unwrap_or_else(|| panic!("profile path is not UTF-8: {}", path.display()));
let body = fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("failed to read {}: {err}", path.display()));
generated.push_str(&format!(" ({name:?}, {body:?}),\n"));
}
generated.push_str("];\n");
fs::write(out_file, generated)
.unwrap_or_else(|err| panic!("failed to write {}: {err}", out_file.display()));
}
fn discover_guides(guide_dir: &Path) -> Vec<Guide> {
fs::read_dir(guide_dir)
.unwrap_or_else(|err| panic!("failed to read {}: {err}", guide_dir.display()))
.filter_map(|entry| {
let path = entry.expect("failed to read guide directory entry").path();
(path.extension().and_then(|value| value.to_str()) == Some("md")).then_some(path)
})
.map(|path| {
let topic = path
.file_stem()
.and_then(|value| value.to_str())
.unwrap_or_else(|| panic!("guide path is not UTF-8: {}", path.display()))
.to_string();
let body = fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("failed to read {}: {err}", path.display()));
let title = body
.lines()
.next()
.and_then(|line| line.strip_prefix("# "))
.filter(|title| !title.is_empty())
.unwrap_or_else(|| panic!("{} must start with a non-empty H1", path.display()))
.to_string();
Guide {
topic,
title,
body,
path,
}
})
.collect()
}
fn validate_guides(guides: &[Guide]) {
assert!(
!guides.is_empty(),
"docs/guides must contain a Markdown guide"
);
for guide in guides {
assert!(
is_kebab_case(&guide.topic),
"guide filename stem must be ASCII kebab-case: {}",
guide.path.display()
);
}
for pair in guides.windows(2) {
assert_ne!(
pair[0].topic, pair[1].topic,
"duplicate guide topic {}",
pair[0].topic
);
}
let width = guides.iter().map(|guide| guide.topic.len()).max().unwrap();
for guide in guides {
let row_len = 2 + width + 1 + guide.title.chars().count();
assert!(
row_len <= 80,
"guide listing row exceeds 80 columns for topic {}",
guide.topic
);
}
}
fn is_kebab_case(value: &str) -> bool {
!value.is_empty()
&& !value.starts_with('-')
&& !value.ends_with('-')
&& !value.contains("--")
&& value
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
}
fn render_topics(guides: &[Guide]) -> String {
let mut generated = String::from("const TOPICS: &[Topic] = &[\n");
for guide in guides {
generated.push_str(" Topic {\n");
generated.push_str(&format!(" name: {:?},\n", guide.topic));
generated.push_str(&format!(" summary: {:?},\n", guide.title));
generated.push_str(&format!(" body: {:?},\n", guide.body));
generated.push_str(" },\n");
}
generated.push_str("];\n");
generated
}