[go: up one dir, main page]
More Web Proxy on the site http://driver.im/

crit/
lib.rs

1//! crit provides predicates for conveniently managing multiple cross target builds.
2
3extern 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
15/// CRIT_ARTIFACT_ROOT denotes the directory housing crit internal files during porting.
16pub static CRIT_ARTIFACT_ROOT: &str = ".crit";
17
18lazy_static::lazy_static! {
19    /// RUSTUP_TARGET_PATTERN matches Rust target triples from rustup target list output.
20    pub static ref RUSTUP_TARGET_PATTERN: regex::Regex = regex::Regex::new(r"(\S+)").unwrap();
21
22    /// DEFAULT_TARGET_EXCLUSION_PATTERN collects patterns for problematic target triples,
23    /// such as bare metal targets that may lack support for the `std` package,
24    /// or targets without community supported cross images.
25    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    /// DEFAULT_FEATURE_EXCLUSION_PATTERN collects patterns for problematic binary features,
51    /// such as internal development programs.
52    pub static ref DEFAULT_FEATURE_EXCLUSION_PATTERN: regex::Regex = regex::Regex::new(
53        &[
54            "letmeout",
55        ].join("|")
56    ).unwrap();
57
58    /// BUILD_MODES enumerates cargo's major build modes.
59    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    /// BINARY_FILE_EXTENSIONS enumerates potential cargo build binary file extensions.
69    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
80/// get_targets queries rustup for the list of available Rust target triples.
81pub 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            // work around rustup writing error messages to stdout
92            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
116/// format_targets renders a target table.
117pub 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
146/// get_applications queries Cargo.toml for the list of binary application names.
147pub 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
210/// TargetConfig models a cross build operation.
211pub struct TargetConfig<'a> {
212    /// target denotes a Rust target triple.
213    pub target: &'a str,
214
215    /// cross_dir_pathbuf denotes the cross notion of target directory.
216    pub cross_dir_pathbuf: &'a path::PathBuf,
217
218    /// bin_dir_pathbuf denotes the location of a destination directory
219    /// for copying artifacts into a recursive archive friendly
220    /// subdirectory tree.
221    pub bin_dir_pathbuf: &'a path::PathBuf,
222
223    /// cross_args denotes any passthrough arguments to forward to cross.
224    pub cross_args: &'a Vec<String>,
225
226    /// applications denotes the names of cargo binaries
227    /// expected to be produced during a cross/cargo build.
228    pub applications: &'a Vec<String>,
229}
230
231impl TargetConfig<'_> {
232    /// build executes a cross build.
233    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
291/// clean_containers removes leftover cross Docker containers.
292pub 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    // cross default image prefix
350    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
402/// clean_artifact_root removes CRIT_ARTIFACT_ROOT directory.
403pub 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
412/// clean removes:
413///
414/// * cross Docker containers
415/// * CRIT_ARTIFACT_ROOT directory
416///
417pub fn clean(artifact_root_path: &path::Path) {
418    _ = clean_containers();
419    _ = clean_artifact_root(artifact_root_path);
420}