1extern crate lazy_static;
4extern crate pad;
5extern crate regex;
6extern crate toml;
7
8use pad::PadStr;
9use std::collections;
10use std::fmt::Write;
11use std::fs;
12use std::path;
13use std::process;
14
15pub static CRIT_ARTIFACT_ROOT: &str = ".crit";
17
18lazy_static::lazy_static! {
19 pub static ref RUSTUP_TARGET_PATTERN: regex::Regex = regex::Regex::new(r"(\S+)").unwrap();
21
22 pub static ref DEFAULT_TARGET_EXCLUSION_PATTERN: regex::Regex = regex::Regex::new(
26 &[
27 "android",
28 "cuda",
29 "emscripten",
30 "fortanix",
31 "fuchsia",
32 "gnullvm",
33 "gnux32",
34 "ios",
35 "loongarch",
36 "msvc",
37 "none-eabi",
38 "ohos",
39 "pc-solaris",
40 "powerpc64le-unknown-linux-musl",
41 "redox",
42 "riscv64gc-unknown-linux-musl",
43 "sparcv9-sun-solaris",
44 "uefi",
45 "unknown-none",
46 "wasm",
47 ].join("|")
48 ).unwrap();
49
50 pub static ref DEFAULT_FEATURE_EXCLUSION_PATTERN: regex::Regex = regex::Regex::new(
53 &[
54 "letmeout",
55 ].join("|")
56 ).unwrap();
57
58 pub static ref BUILD_MODES: Vec<String> = [
60 "debug",
61 "release",
62 ]
63 .iter()
64 .map(|e| e.to_string())
65 .collect();
66
67
68 pub static ref BINARY_FILE_EXTENSIONS: Vec<String> = [
70 "",
71 "exe",
72 "js",
73 "wasm",
74 ]
75 .iter()
76 .map(|e| e.to_string())
77 .collect();
78}
79
80pub fn get_targets(
82 target_exclusion_pattern: regex::Regex,
83) -> Result<collections::BTreeMap<String, bool>, String> {
84 process::Command::new("rustup")
85 .args(["target", "list"])
86 .stdout(process::Stdio::piped())
87 .stderr(process::Stdio::piped())
88 .output()
89 .map_err(|_| "unable to run rustup".to_string())
90 .and_then(|output| match output.status.success() {
91 false => Err("error: unable to query rustup target list".to_string()),
93 _ => String::from_utf8(output.stdout)
94 .map_err(|_| "error: unable to decode rustup stdout stream".to_string()),
95 })
96 .map(|text| {
97 text.lines()
98 .filter(|line| RUSTUP_TARGET_PATTERN.is_match(line))
99 .map(|line| {
100 RUSTUP_TARGET_PATTERN
101 .captures(line)
102 .and_then(|e| e.get(1))
103 .map(|e| e.as_str())
104 .unwrap()
105 })
106 .map(|target| {
107 (
108 target.to_string(),
109 !target_exclusion_pattern.is_match(target),
110 )
111 })
112 .collect()
113 })
114}
115
116pub fn format_targets(targets: collections::BTreeMap<String, bool>) -> Result<String, String> {
118 let target_col_header: String = "TARGET".to_string();
119 let target_col_header_len: usize = target_col_header.len();
120
121 let mut target_col_values: Vec<&String> = targets.keys().collect();
122 target_col_values.push(&target_col_header);
123
124 let max_target_len: usize = target_col_values
125 .iter()
126 .map(|e| e.len())
127 .max()
128 .unwrap_or(target_col_header_len);
129
130 let mut buf: String = String::new();
131 write!(
132 buf,
133 "{} ENABLED",
134 target_col_header.pad_to_width(max_target_len)
135 )
136 .map_err(|_| "error: unable to render target table format header".to_string())?;
137
138 for (target, enabled) in targets {
139 write!(buf, "\n{} {}", target.pad_to_width(max_target_len), enabled)
140 .map_err(|_| "error: unable to render target table format row".to_string())?;
141 }
142
143 Ok(buf)
144}
145
146pub fn get_applications(feature_exclusion_pattern: regex::Regex) -> Result<Vec<String>, String> {
148 let bin_sections_result: Result<Vec<toml::Value>, String> = fs::read_to_string("Cargo.toml")
149 .map_err(|_| "error: unable to read Cargo.toml".to_string())
150 .and_then(|e| e.parse::<toml::Table>().map_err(|err| err.to_string()))
151 .and_then(|e| {
152 e.get("bin")
153 .ok_or("error: no binaries declared in Cargo.toml".to_string())
154 .cloned()
155 })
156 .and_then(|e| {
157 e.as_array()
158 .ok_or("error: binary section not an array in Cargo.toml".to_string())
159 .cloned()
160 });
161
162 let bin_sections: Vec<toml::Value> = bin_sections_result?;
163
164 let name_options: Vec<Option<&toml::Value>> = bin_sections
165 .iter()
166 .filter(|e| {
167 let feature_values_result: Option<&Vec<toml::Value>> =
168 e.get("required-features").and_then(|e2| e2.as_array());
169
170 if feature_values_result.is_none() {
171 return true;
172 }
173
174 let feature_values: &Vec<toml::Value> = feature_values_result.unwrap();
175
176 let feature_options: Vec<Option<&str>> =
177 feature_values.iter().map(|e2| e2.as_str()).collect();
178
179 feature_options.iter().any(|e| match e {
180 Some(feature) => feature_exclusion_pattern.is_match(feature),
181 None => false,
182 })
183 })
184 .map(|e| e.get("name"))
185 .collect();
186
187 if name_options.contains(&None) {
188 return Err("error: binary missing name field in Cargo.toml".to_string());
189 }
190
191 let name_str_results: Vec<Option<&str>> = name_options
192 .iter()
193 .map(|e| {
194 let e2 = e.unwrap();
195 e2.as_str()
196 })
197 .collect();
198
199 if name_str_results.contains(&None) {
200 return Err("error: binary name not a string in Cargo.toml".to_string());
201 }
202
203 Ok(name_str_results
204 .iter()
205 .map(|e| e.unwrap())
206 .map(|e| e.to_string())
207 .collect())
208}
209
210pub struct TargetConfig<'a> {
212 pub target: &'a str,
214
215 pub cross_dir_pathbuf: &'a path::PathBuf,
217
218 pub bin_dir_pathbuf: &'a path::PathBuf,
222
223 pub cross_args: &'a Vec<String>,
225
226 pub applications: &'a Vec<String>,
229}
230
231impl TargetConfig<'_> {
232 pub fn build(&self) -> Result<(), String> {
234 let target_dir_pathbuf: path::PathBuf = self.cross_dir_pathbuf.join(self.target);
235 let target_dir_str: &str = &target_dir_pathbuf.display().to_string();
236
237 let cross_output_result: Result<process::Output, String> = process::Command::new("cross")
238 .args([
239 "build",
240 "--target-dir",
241 target_dir_str,
242 "--target",
243 self.target,
244 ])
245 .args(self.cross_args.clone())
246 .stdout(process::Stdio::piped())
247 .stderr(process::Stdio::piped())
248 .output()
249 .map_err(|err| err.to_string());
250
251 let cross_output: process::Output = cross_output_result?;
252
253 if !cross_output.status.success() {
254 let cross_stderr: String =
255 String::from_utf8(cross_output.stderr).map_err(|err| err.to_string())?;
256
257 return Err(cross_stderr);
258 }
259
260 for application in self.applications {
261 let dest_dir_pathbuf: path::PathBuf = self.bin_dir_pathbuf.join(self.target);
262 let dest_dir_str: &str = &dest_dir_pathbuf.display().to_string();
263
264 fs::create_dir_all(dest_dir_str).map_err(|err| err.to_string())?;
265
266 for extension in BINARY_FILE_EXTENSIONS.iter() {
267 for mode in BUILD_MODES.iter() {
268 let mut source_pathbuf: path::PathBuf = target_dir_pathbuf
269 .join(self.target)
270 .join(mode)
271 .join(application);
272 source_pathbuf.set_extension(extension);
273
274 if source_pathbuf.exists() {
275 let source_str: &str = &source_pathbuf.display().to_string();
276
277 let mut dest_pathbuf: path::PathBuf = dest_dir_pathbuf.join(application);
278 dest_pathbuf.set_extension(extension);
279 let dest_str: &str = &dest_pathbuf.display().to_string();
280
281 fs::copy(source_str, dest_str).map_err(|err| err.to_string())?;
282 }
283 }
284 }
285 }
286
287 Ok(())
288 }
289}
290
291pub fn clean_containers() -> Result<(), String> {
293 let cross_toml_path: &path::Path = path::Path::new("Cross.toml");
294
295 if !cross_toml_path.exists() {
296 return Ok(());
297 }
298
299 let cross_config: toml::Table = fs::read_to_string("Cross.toml")
300 .map_err(|_| "error: unable to read Cross.toml".to_string())
301 .and_then(|e| e.parse::<toml::Table>().map_err(|err| err.to_string()))?;
302
303 if !cross_config.contains_key("target") {
304 return Ok(());
305 }
306
307 let blank_table: toml::Value = toml::Value::Table(toml::Table::new());
308
309 let targets_result: Result<&toml::Table, String> = cross_config
310 .get("target")
311 .unwrap_or(&blank_table)
312 .as_table()
313 .ok_or("target section not a table in Cross.toml".to_string());
314
315 let targets: &toml::Table = targets_result?;
316
317 let target_options: Vec<Option<&toml::Table>> = targets
318 .iter()
319 .map(|(_, target)| target.as_table())
320 .collect();
321
322 if target_options.iter().any(|e| e.is_none()) {
323 return Err("error: target entry not a table in Cross.toml".to_string());
324 }
325
326 let image_options: Vec<Option<String>> = target_options
327 .iter()
328 .map(|e| {
329 e.unwrap_or(&toml::Table::new())
330 .get("image")
331 .unwrap_or(&toml::Value::String(String::new()))
332 .as_str()
333 .map(|e2| e2.to_string())
334 })
335 .collect();
336
337 if image_options.iter().any(|e| e.is_none()) {
338 return Err("error: target image not a string in Cross.toml".to_string());
339 }
340
341 let mut images: Vec<String> = image_options
342 .iter()
343 .map(|e| {
344 let blank_string = String::new();
345 e.clone().unwrap_or(blank_string)
346 })
347 .collect();
348
349 images.push("ghcr.io/cross-rs".to_string());
351
352 let docker_ps_output: process::Output = process::Command::new("docker")
353 .args(["ps", "-a"])
354 .output()
355 .map_err(|_| "error: unable to run docker process list".to_string())?;
356
357 if !docker_ps_output.status.success() {
358 let docker_ps_stderr = String::from_utf8(docker_ps_output.stderr)
359 .map_err(|_| "error: unable to decode docker process list stderr stream")?;
360
361 return Err(docker_ps_stderr);
362 }
363
364 let docker_ps_stdout: String = String::from_utf8(docker_ps_output.stdout)
365 .map_err(|_| "error: unable to decode docker process list stdout stream")?;
366
367 for line in docker_ps_stdout.lines() {
368 let pattern: String = format!("([[:xdigit:]]{{12}})\\s+({})", images.join("|"));
369
370 let re: regex::Regex = regex::Regex::new(&pattern).map_err(|_| {
371 "image name introduced invalid Rust regular expression syntax".to_string()
372 })?;
373
374 if !re.is_match(line) {
375 continue;
376 }
377
378 let container_id: &str = re
379 .captures(line)
380 .and_then(|e| e.get(1))
381 .map(|e| e.as_str())
382 .ok_or("error: container id not a string in docker process list output".to_string())?;
383
384 let docker_rm_output: process::Output = process::Command::new("docker")
385 .args(["rm", "-f", container_id])
386 .output()
387 .map_err(|_| "error: unable to run docker container removal".to_string())?;
388
389 if !docker_rm_output.status.success() {
390 let docker_rm_stderr: String =
391 String::from_utf8(docker_rm_output.stderr).map_err(|_| {
392 "error: unable to decode docker container removal stderr stream".to_string()
393 })?;
394
395 return Err(docker_rm_stderr);
396 }
397 }
398
399 Ok(())
400}
401
402pub fn clean_artifact_root(artifact_root_path: &path::Path) -> Result<(), String> {
404 if !artifact_root_path.exists() {
405 return Ok(());
406 }
407
408 fs::remove_dir_all(CRIT_ARTIFACT_ROOT)
409 .map_err(|_| "error: unable to remove crit artifact root directory".to_string())
410}
411
412pub fn clean(artifact_root_path: &path::Path) {
418 _ = clean_containers();
419 _ = clean_artifact_root(artifact_root_path);
420}