Skip to main content

cargo_hyperlight/
toolchain.rs

1use std::path::{Path, PathBuf};
2
3use anyhow::{Context, Result, ensure};
4use proc_macro2::TokenStream;
5use quote::{TokenStreamExt, quote};
6use regex::Regex;
7
8use crate::cargo_cmd::{CargoCmd, cargo_cmd};
9use crate::cli::Args;
10use crate::sysroot::CargoBuildMessage;
11use crate::{toolchain_flags, util};
12
13#[derive(serde::Deserialize)]
14struct CargoMetadata {
15    packages: Vec<CargoMetadataPackage>,
16}
17
18#[derive(serde::Deserialize)]
19struct CargoMetadataPackage {
20    name: String,
21    manifest_path: PathBuf,
22    #[allow(dead_code)]
23    // we can use this if we ever change the include paths to be copied
24    version: semver::Version,
25}
26
27struct PackageDirectories {
28    hyperlight_libc: Option<PathBuf>,
29    hyperlight_guest_bin: Option<PathBuf>,
30    hyperlight_guest_capi: Option<PathBuf>,
31}
32impl PackageDirectories {
33    fn libc(&self) -> Result<PathBuf> {
34        self.hyperlight_libc
35            .as_ref()
36            .or(self.hyperlight_guest_bin.as_ref())
37            .cloned()
38            .context(
39                "Could not find hyperlight-libc or hyperlight-guest-bin package in cargo metadata",
40            )
41    }
42    fn guest_capi(&self) -> Result<PathBuf> {
43        self.hyperlight_guest_capi
44            .clone()
45            .context("Could not find hyperlight-guest-capi package in cargo metadata")
46    }
47}
48
49fn find_package_dir(metadata: &CargoMetadata, name: &str) -> Result<Option<PathBuf>> {
50    metadata
51        .packages
52        .iter()
53        .find(|x| x.name == name)
54        .map(|pkg| {
55            pkg.manifest_path
56                .parent()
57                .with_context(|| format!("Failed to get directory for {name}"))
58                .map(|x| x.to_path_buf())
59        })
60        .transpose()
61}
62
63fn find_package_dirs(args: &Args) -> Result<PackageDirectories> {
64    let metadata = cargo_cmd()?
65        .env_clear()
66        .envs(args.env.iter())
67        .current_dir(&args.current_dir)
68        .arg("metadata")
69        .manifest_path(&args.manifest_path)
70        .arg("--format-version=1")
71        .append_rustflags("--cfg=hyperlight")
72        .append_rustflags("--check-cfg=cfg(hyperlight)")
73        .checked_output()
74        .context("Failed to get cargo metadata")?;
75
76    let metadata = serde_json::from_slice::<CargoMetadata>(&metadata.stdout)
77        .context("Failed to parse cargo metadata")?;
78
79    Ok(PackageDirectories {
80        hyperlight_libc: find_package_dir(&metadata, "hyperlight-libc")?,
81        hyperlight_guest_bin: find_package_dir(&metadata, "hyperlight-guest-bin")?,
82        hyperlight_guest_capi: if args.with_guest_capi {
83            find_package_dir(&metadata, "hyperlight_guest_capi")?
84        } else {
85            None
86        },
87    })
88}
89
90fn copy_includes(src_dirs: impl Iterator<Item: AsRef<Path>>, dst_dir: &Path) -> Result<()> {
91    util::union_glob(src_dirs, dst_dir, "**/*.h")
92}
93
94fn build_guest_capi(args: &Args, capi_dir: &Path) -> Result<()> {
95    use crate::CargoCommandExt;
96    let output = cargo_cmd()?
97        .env_clear()
98        .envs(args.env.iter())
99        .arg("build")
100        .manifest_path(&Some(capi_dir.join("Cargo.toml")))
101        .target_dir(args.build_dir())
102        .arg("--message-format=json")
103        .env_remove("RUSTC_WORKSPACE_WRAPPER")
104        .populate_from_args(args, true)
105        .output()
106        .context("Failed to build capi cargo project")?;
107    ensure!(
108        output.status.success(),
109        "Failed to build capi\n{}",
110        String::from_utf8_lossy(&output.stderr)
111    );
112
113    let messages = String::from_utf8_lossy(&output.stdout);
114
115    for message in messages.lines() {
116        let message = serde_json::from_str::<CargoBuildMessage>(message)
117            .context("Failed to parse sysroot build message")?;
118        if message.reason == "compiler-artifact" {
119            let name = message.target.name;
120            if name == "hyperlight_guest_capi" {
121                for file in message.filenames {
122                    let file_name = file.file_name().with_context(|| {
123                        format!(
124                            "Failed to get filename for capi build artifact {}",
125                            file.display()
126                        )
127                    })?;
128                    let dst = args.c_libs_dir().join(file_name);
129                    std::fs::copy(&file, &dst)?;
130                }
131            }
132        }
133    }
134
135    Ok(())
136}
137
138fn path_to_tokens(p: &Path) -> TokenStream {
139    let mut tokens = quote! {
140        let mut x: ::std::path::PathBuf = ::std::path::PathBuf::new();
141    };
142    for x in p.iter() {
143        let s = x.to_string_lossy();
144        tokens.append_all(quote! {
145            x.push(#s);
146        });
147    }
148    tokens.append_all(quote! { x });
149    quote! { { #tokens } }
150}
151
152fn build_wrappers(args: &Args) -> Result<()> {
153    const CARGO_TOML: &str = include_str!("wrapper/_Cargo.toml");
154    const MAIN_RS: &str = include_str!("wrapper/_main.rs");
155    const CLANG_PARSER_RS: &str = include_str!("wrapper/_clang_parser.rs");
156    const FLAGS_RS: &str = include_str!("toolchain_flags.rs");
157
158    let wrapper_src_dir = args.wrapper_src_dir();
159    std::fs::create_dir_all(&wrapper_src_dir)
160        .context("Failed to create wrapper source directory")?;
161    std::fs::write(wrapper_src_dir.join("Cargo.toml"), CARGO_TOML)?;
162    let wrapper_src_src_dir = wrapper_src_dir.join("src");
163    std::fs::create_dir_all(&wrapper_src_src_dir)
164        .context("Failed to create wrapper source src directory")?;
165    std::fs::write(wrapper_src_src_dir.join("main.rs"), MAIN_RS)?;
166    std::fs::write(wrapper_src_src_dir.join("clang_parser.rs"), CLANG_PARSER_RS)?;
167    std::fs::write(wrapper_src_src_dir.join("toolchain_flags.rs"), FLAGS_RS)?;
168    let includes_toks = path_to_tokens(args.includes_dir().strip_prefix(args.sysroot_dir())?);
169    let c_libs_toks = path_to_tokens(args.c_libs_dir().strip_prefix(args.sysroot_dir())?);
170    let wrapper_toks = path_to_tokens(args.wrapper_dir().strip_prefix(args.sysroot_dir())?);
171    let target = &args.target;
172    let with_guest_capi = args.with_guest_capi;
173    std::fs::write(
174        wrapper_src_src_dir.join("args.rs"),
175        (quote! {
176            pub(crate) fn args(root: &std::path::Path) -> crate::toolchain_flags::Args {
177                crate::toolchain_flags::Args {
178                    includes_dir: root.join(#includes_toks),
179                    c_libs_dir: root.join(#c_libs_toks),
180                    wrapper_dir: root.join(#wrapper_toks),
181                    target: #target.to_string(),
182                    with_guest_capi: #with_guest_capi,
183                }
184            }
185        })
186        .to_string(),
187    )?;
188
189    let output = cargo_cmd()?
190        .env_clear()
191        .envs(args.env.iter())
192        .current_dir(&args.current_dir)
193        .arg("build")
194        .target(&args.host)
195        .manifest_path(&Some(wrapper_src_dir.join("Cargo.toml")))
196        .target_dir(args.build_dir())
197        .arg("--release")
198        .arg("--message-format=json")
199        .env_remove("RUSTC_WORKSPACE_WRAPPER")
200        .output()
201        .context("Failed to build wrapper cargo project")?;
202    ensure!(
203        output.status.success(),
204        "Failed to build wrapper\n{}",
205        String::from_utf8_lossy(&output.stderr)
206    );
207
208    let messages = String::from_utf8_lossy(&output.stdout);
209
210    for message in messages.lines() {
211        let message = serde_json::from_str::<CargoBuildMessage>(message)
212            .context("Failed to parse wrapper build message")?;
213        if message.reason == "compiler-artifact" {
214            let name = message.target.name;
215            if name == "hyperlight-sysroot-wrappers" {
216                let files: Vec<_> = message
217                    .filenames
218                    .iter()
219                    .filter(|x| x.extension() != Some("pdb".as_ref()))
220                    .collect();
221                ensure!(
222                    files.len() == 1,
223                    "hyperlight-sysroot-wrappers produced wrong number of binaries",
224                );
225                let target_uses_exe = message.filenames[0].extension() == Some("exe".as_ref());
226                let dir = args.wrapper_dir();
227                let bin_name = |n| {
228                    let mut p = dir.join(n);
229                    if target_uses_exe {
230                        p.set_extension("exe");
231                    }
232                    p
233                };
234                std::fs::create_dir_all(&dir).context("Failed to create wrapper bin directory")?;
235                std::fs::copy(&message.filenames[0], bin_name("hyperlight-config"))?;
236                std::fs::copy(&message.filenames[0], bin_name("clang"))?;
237                std::fs::copy(
238                    &message.filenames[0],
239                    bin_name(&format!("{}-clang", target)),
240                )?;
241            }
242        }
243    }
244    Ok(())
245}
246
247pub fn prepare(args: &Args) -> Result<()> {
248    let package_dirs = find_package_dirs(args)?;
249    let libc_dir = package_dirs.libc()?;
250
251    let include_dst_dir = args.includes_dir();
252
253    std::fs::create_dir_all(&include_dst_dir)
254        .context("Failed to create sysroot include directory")?;
255
256    // Detect which libc variant is present: picolibc or legacy musl
257    let mut include_dirs: Vec<&str> = vec![
258        // directories for musl
259        "third_party/printf/",
260        "third_party/musl/include",
261        "third_party/musl/arch/generic",
262        "third_party/musl/src/internal",
263        // directories for picolibc
264        "third_party/picolibc/libc/include",
265        "third_party/picolibc/libc/stdio",
266        "include",
267    ];
268    if !args.target.starts_with("aarch64") {
269        include_dirs.push("third_party/musl/arch/x86_64");
270    }
271
272    let capi_dir = args
273        .with_guest_capi
274        .then(|| {
275            let d = package_dirs.guest_capi()?;
276            build_guest_capi(args, &d)?;
277            Ok::<_, anyhow::Error>(d)
278        })
279        .transpose()?;
280
281    let include_dirs = include_dirs
282        .into_iter()
283        .map(|dir| libc_dir.join(dir))
284        .chain(capi_dir.map(|x| x.join("include")));
285    copy_includes(include_dirs, &include_dst_dir)?;
286
287    build_wrappers(args)?;
288
289    Ok(())
290}
291
292impl From<&Args> for toolchain_flags::Args {
293    fn from(args: &Args) -> toolchain_flags::Args {
294        toolchain_flags::Args {
295            includes_dir: args.includes_dir(),
296            c_libs_dir: args.c_libs_dir(),
297            wrapper_dir: args.wrapper_dir(),
298            target: args.target.clone(),
299            with_guest_capi: args.with_guest_capi,
300        }
301    }
302}
303
304pub fn cflags(args: &Args, bootstrap: bool) -> toolchain_flags::Flags {
305    toolchain_flags::cflags(&args.into(), bootstrap)
306}
307pub fn ldflags(args: &Args) -> toolchain_flags::Flags {
308    toolchain_flags::ldflags(&args.into())
309}
310pub fn libs(args: &Args) -> toolchain_flags::Flags {
311    toolchain_flags::libs(&args.into())
312}
313
314pub fn find_cc() -> Result<PathBuf> {
315    if let Ok(path) = which::which("clang") {
316        return Ok(path);
317    }
318    // try with postfixed version clang, e.g., clang-20
319    let re = Regex::new(r"clang-\d+").unwrap();
320    which::which_re(&re)
321        .context("Could not find 'clang' in PATH")?
322        .next()
323        .context("Could not find 'clang' in PATH")
324}
325
326pub fn find_ar() -> Result<PathBuf> {
327    #[cfg(not(target_os = "macos"))]
328    let ar = which::which("ar");
329    let llvm_ar = which::which("llvm-ar");
330    // The system archiver on macOS can't deal with ELFs, so check
331    // `llvm-ar` first there (but stillfall back to `ar` when it's not
332    // available, since the correct LLVM ar is named `ar` in some
333    // environments, like when building in Nix);
334    #[cfg(target_os = "macos")]
335    let preferred_ar = llvm_ar.or(ar);
336    #[cfg(not(target_os = "macos"))]
337    let preferred_ar = ar.or(llvm_ar);
338    if let Ok(ar) = preferred_ar {
339        return Ok(ar);
340    }
341
342    // try with postfixed version llvm-ar, e.g., llvm-ar-20
343    let re = Regex::new(r"llvm-ar-\d+").unwrap();
344    which::which_re(&re)
345        .context("Could not find 'ar' or 'llvm-ar' in PATH")?
346        .next()
347        .context("Could not find 'ar' or 'llvm-ar' in PATH")
348}