compiler_llvm_builder/
utils.rs

1//!
2//! The LLVM builder utilities.
3//!
4
5use std::fs::File;
6use std::path::Path;
7use std::path::PathBuf;
8use std::process::Command;
9use std::process::Stdio;
10use std::time::Duration;
11
12use path_slash::PathBufExt;
13
14/// The LLVM host repository URL.
15pub const LLVM_HOST_SOURCE_URL: &str = "https://github.com/llvm/llvm-project";
16
17/// The LLVM host repository tag.
18pub const LLVM_HOST_SOURCE_TAG: &str = "llvmorg-17.0.6";
19
20/// The minimum required XCode version.
21pub const XCODE_MIN_VERSION: u32 = 11;
22
23/// The XCode version 15.
24pub const XCODE_VERSION_15: u32 = 15;
25
26/// The number of download retries if failed.
27pub const DOWNLOAD_RETRIES: u16 = 16;
28
29/// The number of parallel download requests.
30pub const DOWNLOAD_PARALLEL_REQUESTS: u16 = 1;
31
32/// The download timeout in seconds.
33pub const DOWNLOAD_TIMEOUT_SECONDS: u64 = 300;
34
35/// The musl snapshots URL.
36pub const MUSL_SNAPSHOTS_URL: &str = "https://git.musl-libc.org/cgit/musl/snapshot";
37
38///
39/// The subprocess runner.
40///
41/// Checks the status and prints `stderr`.
42///
43pub fn command(command: &mut Command, description: &str) -> anyhow::Result<()> {
44    if std::env::var("VERBOSE").is_ok() {
45        println!("\ndescription: {}; command: {:?}", description, command);
46    }
47    if std::env::var("DRY_RUN").is_ok() {
48        println!("\tOnly a dry run; not executing the command.");
49    } else {
50        let status = command
51            .status()
52            .map_err(|error| anyhow::anyhow!("{} process: {}", description, error))?;
53        if !status.success() {
54            anyhow::bail!("{} failed", description);
55        }
56    }
57    Ok(())
58}
59
60///
61/// Download a file from the URL to the path.
62///
63pub fn download(url: &str, path: &str) -> anyhow::Result<()> {
64    let mut downloader = downloader::Downloader::builder()
65        .download_folder(Path::new(path))
66        .parallel_requests(DOWNLOAD_PARALLEL_REQUESTS)
67        .retries(DOWNLOAD_RETRIES)
68        .timeout(Duration::from_secs(DOWNLOAD_TIMEOUT_SECONDS))
69        .build()?;
70    while let Err(error) = downloader.download(&[downloader::Download::new(url)]) {
71        eprintln!("MUSL download from `{url}` failed: {error}");
72    }
73    Ok(())
74}
75
76///
77/// Unpack a tarball.
78///
79pub fn unpack_tar(filename: PathBuf, path: &str) -> anyhow::Result<()> {
80    let tar_gz = File::open(filename)?;
81    let tar = flate2::read::GzDecoder::new(tar_gz);
82    let mut archive = tar::Archive::new(tar);
83    archive.unpack(path)?;
84    Ok(())
85}
86
87///
88/// The `musl` downloading sequence.
89///
90pub fn download_musl(name: &str) -> anyhow::Result<()> {
91    let tar_file_name = format!("{name}.tar.gz");
92    let url = format!("{}/{tar_file_name}", MUSL_SNAPSHOTS_URL);
93    download(url.as_str(), crate::LLVMPath::DIRECTORY_LLVM_TARGET)?;
94    let musl_tarball = crate::LLVMPath::musl_source(tar_file_name.as_str())?;
95    unpack_tar(musl_tarball, crate::LLVMPath::DIRECTORY_LLVM_TARGET)?;
96    Ok(())
97}
98
99/// Call ninja to build the LLVM.
100pub fn ninja(build_dir: &Path) -> anyhow::Result<()> {
101    let mut ninja = Command::new("ninja");
102    ninja.args(["-C", build_dir.to_string_lossy().as_ref()]);
103    if std::env::var("DRY_RUN").is_ok() {
104        ninja.arg("-n");
105    }
106    command(ninja.arg("install"), "Running ninja install")?;
107    Ok(())
108}
109
110///
111/// Create an absolute path, appending it to the current working directory.
112///
113pub fn absolute_path<P: AsRef<Path>>(path: P) -> anyhow::Result<PathBuf> {
114    let mut full_path = std::env::current_dir()?;
115    full_path.push(path);
116    Ok(full_path)
117}
118
119///
120/// Converts a Windows path into a Unix path.
121///
122pub fn path_windows_to_unix<P: AsRef<Path> + PathBufExt>(path: P) -> anyhow::Result<PathBuf> {
123    path.to_slash()
124        .map(|pathbuf| PathBuf::from(pathbuf.to_string()))
125        .ok_or_else(|| anyhow::anyhow!("Windows-to-Unix path conversion error"))
126}
127
128///
129/// Checks if the tool exists in the system.
130///
131pub fn check_presence(name: &str) -> anyhow::Result<()> {
132    let status = Command::new("which")
133        .arg(name)
134        .status()
135        .map_err(|error| anyhow::anyhow!("`which {}` process: {}", name, error))?;
136    if !status.success() {
137        anyhow::bail!("Tool `{}` is missing. Please install", name);
138    }
139    Ok(())
140}
141
142///
143/// Identify XCode version using `pkgutil`.
144///
145pub fn get_xcode_version() -> anyhow::Result<u32> {
146    let pkgutil = Command::new("pkgutil")
147        .args(["--pkg-info", "com.apple.pkg.CLTools_Executables"])
148        .stdout(Stdio::piped())
149        .spawn()
150        .map_err(|error| anyhow::anyhow!("`pkgutil` process: {}", error))?;
151    let grep_version = Command::new("grep")
152        .arg("version")
153        .stdin(Stdio::from(pkgutil.stdout.expect(
154            "Failed to identify XCode version - XCode or CLI tools are not installed",
155        )))
156        .output()
157        .map_err(|error| anyhow::anyhow!("`grep` process: {}", error))?;
158    let version_string = String::from_utf8(grep_version.stdout)?;
159    let version_regex = regex::Regex::new(r"version: (\d+)\..*")?;
160    let captures = version_regex
161        .captures(version_string.as_str())
162        .ok_or(anyhow::anyhow!(
163            "Failed to parse XCode version: {version_string}"
164        ))?;
165    let xcode_version: u32 = captures
166        .get(1)
167        .expect("Always has a major version")
168        .as_str()
169        .parse()
170        .map_err(|error| anyhow::anyhow!("Failed to parse XCode version: {error}"))?;
171    Ok(xcode_version)
172}