revive_llvm_builder/
utils.rs

1//! The LLVM builder utilities.
2
3use std::fs::File;
4use std::path::Path;
5use std::path::PathBuf;
6use std::process::Command;
7use std::process::Stdio;
8use std::time::Duration;
9
10use anyhow::Context;
11use path_slash::PathBufExt;
12
13/// The LLVM host repository URL.
14pub const LLVM_HOST_SOURCE_URL: &str = "https://github.com/llvm/llvm-project";
15
16/// The LLVM host repository tag.
17pub const LLVM_HOST_SOURCE_TAG: &str = "llvmorg-18.1.8";
18
19/// The minimum required XCode version.
20pub const XCODE_MIN_VERSION: u32 = 11;
21
22/// The XCode version 15.
23pub const XCODE_VERSION_15: u32 = 15;
24
25/// The number of download retries if failed.
26pub const DOWNLOAD_RETRIES: u16 = 16;
27
28/// The number of parallel download requests.
29pub const DOWNLOAD_PARALLEL_REQUESTS: u16 = 1;
30
31/// The download timeout in seconds.
32pub const DOWNLOAD_TIMEOUT_SECONDS: u64 = 300;
33
34/// The musl snapshots URL.
35pub const MUSL_SNAPSHOTS_URL: &str = "https://git.musl-libc.org/cgit/musl/snapshot";
36
37/// The emscripten SDK git URL.
38pub const EMSDK_SOURCE_URL: &str = "https://github.com/emscripten-core/emsdk.git";
39
40/// The emscripten SDK version.
41pub const EMSDK_VERSION: &str = "4.0.9";
42
43/// The subprocess runner.
44///
45/// Checks the status and prints `stderr`.
46pub fn command(command: &mut Command, description: &str) -> anyhow::Result<()> {
47    log::debug!("executing '{command:?}' ({description})");
48
49    if std::env::var("DRY_RUN").is_ok() {
50        log::warn!("Only a dry run; not executing the command.");
51        return Ok(());
52    }
53
54    let status = command
55        .status()
56        .map_err(|error| anyhow::anyhow!("{} process: {}", description, error))?;
57
58    if !status.success() {
59        log::error!("the command '{command:?}' failed!");
60        anyhow::bail!("{} failed", description);
61    }
62
63    Ok(())
64}
65
66/// Download a file from the URL to the path.
67pub fn download(url: &str, path: &str) -> anyhow::Result<()> {
68    log::trace!("downloading '{url}' into '{path}'");
69
70    let mut downloader = downloader::Downloader::builder()
71        .download_folder(Path::new(path))
72        .parallel_requests(DOWNLOAD_PARALLEL_REQUESTS)
73        .retries(DOWNLOAD_RETRIES)
74        .timeout(Duration::from_secs(DOWNLOAD_TIMEOUT_SECONDS))
75        .build()?;
76    while let Err(error) = downloader.download(&[downloader::Download::new(url)]) {
77        log::error!("MUSL download from `{url}` failed: {error}");
78    }
79    Ok(())
80}
81
82/// Unpack a tarball.
83pub fn unpack_tar(filename: PathBuf, path: &str) -> anyhow::Result<()> {
84    let tar_gz = File::open(filename)?;
85    let tar = flate2::read::GzDecoder::new(tar_gz);
86    let mut archive = tar::Archive::new(tar);
87    archive.unpack(path)?;
88    Ok(())
89}
90
91/// The `musl` downloading sequence.
92pub fn download_musl(name: &str) -> anyhow::Result<()> {
93    log::info!("downloading musl {name}");
94    let tar_file_name = format!("{name}.tar.gz");
95    let url = format!("{MUSL_SNAPSHOTS_URL}/{tar_file_name}");
96    let target_path = crate::llvm_path::DIRECTORY_LLVM_TARGET
97        .get()
98        .unwrap()
99        .to_string_lossy();
100    download(url.as_str(), &target_path)?;
101    let musl_tarball = crate::LLVMPath::musl_source(tar_file_name.as_str())?;
102    unpack_tar(musl_tarball, &target_path)?;
103    Ok(())
104}
105
106/// Call ninja to build the LLVM.
107pub fn ninja(build_dir: &Path) -> anyhow::Result<()> {
108    let mut ninja = Command::new("ninja");
109    ninja.args(["-C", build_dir.to_string_lossy().as_ref()]);
110    if std::env::var("DRY_RUN").is_ok() {
111        ninja.arg("-n");
112    }
113    command(ninja.arg("install"), "Running ninja install")?;
114    Ok(())
115}
116
117/// Create an absolute path, appending it to the current working directory.
118pub fn absolute_path<P: AsRef<Path>>(path: P) -> anyhow::Result<PathBuf> {
119    let mut full_path = std::env::current_dir()?;
120    full_path.push(path);
121    Ok(full_path)
122}
123
124///
125/// Converts a Windows path into a Unix path.
126///
127pub fn path_windows_to_unix<P: AsRef<Path> + PathBufExt>(path: P) -> anyhow::Result<PathBuf> {
128    path.to_slash()
129        .map(|pathbuf| PathBuf::from(pathbuf.to_string()))
130        .ok_or_else(|| anyhow::anyhow!("Windows-to-Unix path conversion error"))
131}
132
133/// Checks if the tool exists in the system.
134pub fn check_presence(name: &str) -> anyhow::Result<()> {
135    which::which(name).with_context(|| format!("Tool `{name}` is missing. Please install"))?;
136    Ok(())
137}
138
139/// Identify XCode version using `pkgutil`.
140pub fn get_xcode_version() -> anyhow::Result<u32> {
141    let pkgutil = Command::new("pkgutil")
142        .args(["--pkg-info", "com.apple.pkg.CLTools_Executables"])
143        .stdout(Stdio::piped())
144        .spawn()
145        .map_err(|error| anyhow::anyhow!("`pkgutil` process: {}", error))?;
146    let grep_version = Command::new("grep")
147        .arg("version")
148        .stdin(Stdio::from(pkgutil.stdout.expect(
149            "Failed to identify XCode version - XCode or CLI tools are not installed",
150        )))
151        .output()
152        .map_err(|error| anyhow::anyhow!("`grep` process: {}", error))?;
153    let version_string = String::from_utf8(grep_version.stdout)?;
154    let version_regex = regex::Regex::new(r"version: (\d+)\..*")?;
155    let captures = version_regex
156        .captures(version_string.as_str())
157        .ok_or(anyhow::anyhow!(
158            "Failed to parse XCode version: {version_string}"
159        ))?;
160    let xcode_version: u32 = captures
161        .get(1)
162        .expect("Always has a major version")
163        .as_str()
164        .parse()
165        .map_err(|error| anyhow::anyhow!("Failed to parse XCode version: {error}"))?;
166    Ok(xcode_version)
167}
168
169/// Install the Emscripten SDK.
170pub fn install_emsdk() -> anyhow::Result<()> {
171    log::info!("installing emsdk v{EMSDK_VERSION}");
172
173    let emsdk_source_path = PathBuf::from(crate::LLVMPath::DIRECTORY_EMSDK_SOURCE);
174
175    if emsdk_source_path.exists() {
176        log::warn!(
177            "emsdk source path {emsdk_source_path:?} already exists.
178            Skipping the emsdk installation, delete the source path for re-installation"
179        );
180        return Ok(());
181    }
182
183    crate::utils::command(
184        Command::new("git")
185            .arg("clone")
186            .arg(crate::utils::EMSDK_SOURCE_URL)
187            .arg(emsdk_source_path.to_string_lossy().as_ref()),
188        "Emscripten SDK repository cloning",
189    )?;
190
191    crate::utils::command(
192        Command::new("git")
193            .arg("checkout")
194            .arg(format!("tags/{}", crate::utils::EMSDK_VERSION))
195            .current_dir(&emsdk_source_path),
196        "Emscripten SDK repository version checkout",
197    )?;
198
199    crate::utils::command(
200        Command::new("./emsdk")
201            .arg("install")
202            .arg(EMSDK_VERSION)
203            .current_dir(&emsdk_source_path),
204        "Emscripten SDK installation",
205    )?;
206
207    crate::utils::command(
208        Command::new("./emsdk")
209            .arg("activate")
210            .arg(EMSDK_VERSION)
211            .current_dir(&emsdk_source_path),
212        "Emscripten SDK activation",
213    )?;
214
215    log::warn!(
216        "run 'source {}emsdk_env.sh' to finish the emsdk installation",
217        emsdk_source_path.display()
218    );
219
220    Ok(())
221}
222
223/// The LLVM target directory default path.
224pub fn directory_target_llvm(target_env: crate::target_env::TargetEnv) -> PathBuf {
225    crate::llvm_path::DIRECTORY_LLVM_TARGET
226        .get_or_init(|| PathBuf::from(format!("./target-llvm/{target_env}/")))
227        .clone()
228}