Skip to main content

cargo_hyperlight/
cli.rs

1use std::collections::HashMap;
2use std::convert::Infallible;
3use std::env;
4use std::env::consts::ARCH;
5use std::ffi::{OsStr, OsString};
6use std::fmt::Debug;
7use std::path::PathBuf;
8
9use anyhow::{Context, Result};
10use const_format::formatcp;
11use os_str_bytes::OsStrBytesExt as _;
12
13use crate::cargo_cmd::{CargoCmd as _, cargo_cmd};
14use crate::toolchain;
15
16pub struct Args {
17    pub manifest_path: Option<PathBuf>,
18    pub target_dir: PathBuf,
19    pub target: String,
20    pub host: String,
21    pub with_guest_capi: bool,
22    pub c_sysroot_dir: Option<PathBuf>,
23    pub env: HashMap<OsString, OsString>,
24    pub current_dir: PathBuf,
25    pub clang: Option<PathBuf>,
26    pub ar: Option<PathBuf>,
27    /// Whether rustc needs `-Zunstable-options` to load the custom target
28    /// specification. Detected by [`Args::prepare_sysroot`].
29    pub unstable_target_spec: bool,
30}
31
32pub trait WarningLevel {
33    type Error;
34    fn warning<T: Debug>(
35        &self,
36        msg: &str,
37        err: impl Into<anyhow::Error>,
38        default: T,
39    ) -> Result<T, Self::Error>;
40}
41
42pub struct Warning;
43
44#[doc(hidden)]
45pub mod warning {
46    pub struct WarningIgnore;
47    pub struct WarningWarn;
48    #[allow(dead_code)]
49    pub struct WarningError;
50}
51
52impl Warning {
53    pub const IGNORE: warning::WarningIgnore = warning::WarningIgnore;
54    pub const WARN: warning::WarningWarn = warning::WarningWarn;
55    #[allow(dead_code)]
56    pub const ERROR: warning::WarningError = warning::WarningError;
57}
58
59impl WarningLevel for warning::WarningIgnore {
60    type Error = Infallible;
61    fn warning<T: Debug>(
62        &self,
63        _msg: &str,
64        _err: impl Into<anyhow::Error>,
65        default: T,
66    ) -> Result<T, Self::Error> {
67        Ok(default)
68    }
69}
70
71impl WarningLevel for warning::WarningWarn {
72    type Error = Infallible;
73    fn warning<T: Debug>(
74        &self,
75        msg: &str,
76        err: impl Into<anyhow::Error>,
77        default: T,
78    ) -> Result<T, Self::Error> {
79        warning(msg);
80        warning(format!("{:?}", err.into()));
81        warning(format!("using {default:?}"));
82        Ok(default)
83    }
84}
85
86impl WarningLevel for warning::WarningError {
87    type Error = anyhow::Error;
88    fn warning<T: Debug>(
89        &self,
90        msg: &str,
91        err: impl Into<anyhow::Error>,
92        _default: T,
93    ) -> Result<T, Self::Error> {
94        Err(err.into()).context(msg.to_string())
95    }
96}
97
98impl Args {
99    pub fn parse<W: WarningLevel>(
100        args: impl IntoIterator<Item = impl Into<OsString> + Clone>,
101        env: impl IntoIterator<Item = (impl Into<OsString>, impl Into<OsString>)>,
102        cwd: Option<impl Into<PathBuf>>,
103        warn: W,
104    ) -> Result<Args, W::Error> {
105        let mut args = ArgsImpl::parse_args(args);
106        args.env = env.into_iter().map(|(k, v)| (k.into(), v.into())).collect();
107        let cwd = match cwd {
108            Some(cwd) => cwd.into(),
109            None => match env::current_dir() {
110                Ok(cwd) => cwd,
111                Err(err) => {
112                    warn.warning("Could not get current directory", err, PathBuf::from("."))?
113                }
114            },
115        };
116        args.current_dir = cwd.clone();
117        Args::try_from_with_defaults(warn, args)
118    }
119}
120
121fn warning(msg: impl AsRef<str>) {
122    eprintln!(
123        "{}{}{}",
124        console::style("warning").yellow().bold(),
125        console::style(": ").bold(),
126        console::style(msg.as_ref()).bold(),
127    );
128}
129
130impl TryFrom<ArgsImpl> for Args {
131    type Error = anyhow::Error;
132
133    fn try_from(value: ArgsImpl) -> Result<Self> {
134        Args::try_from_with_defaults(Warning::ERROR, value)
135    }
136}
137
138impl Args {
139    fn try_from_with_defaults<W: WarningLevel>(warn: W, value: ArgsImpl) -> Result<Self, W::Error> {
140        let manifest_path = value.manifest_path;
141
142        let target_dir = match value.target_dir {
143            Some(dir) => dir,
144            None => match resolve_target_dir(&manifest_path, &value.env, &value.current_dir) {
145                Ok(dir) => dir,
146                Err(err) => warn.warning(
147                    "could not resolve target directory",
148                    err,
149                    value.current_dir.join("target"),
150                )?,
151            },
152        };
153
154        let target = match value.target {
155            Some(triplet) => triplet,
156            None => match resolve_target(&value.env, &value.current_dir) {
157                Ok(triplet) => triplet,
158                Err(err) => warn.warning(
159                    "could not resolve target triple",
160                    err,
161                    DEFAULT_TARGET.to_string(),
162                )?,
163            },
164        };
165
166        let target = if target.ends_with("-hyperlight-none") {
167            target
168        } else {
169            let (arch, _) = target.split_once('-').unwrap_or((&target, ""));
170            warn.warning(
171                "requested target is not a hyperlight target",
172                anyhow::anyhow!("invalid hyperlight target: {target}"),
173                format!("{arch}-hyperlight-none"),
174            )?
175        };
176
177        let target_dir = value.current_dir.join(target_dir);
178
179        let host = value
180            .host
181            .unwrap_or(env!("CARGO_HYPERLIGHT_HOST_TRIPLE").to_string());
182
183        Ok(Args {
184            manifest_path,
185            target_dir,
186            target,
187            host,
188            with_guest_capi: value.with_guest_capi,
189            c_sysroot_dir: value.c_sysroot_dir,
190            env: value.env,
191            current_dir: value.current_dir,
192            clang: toolchain::find_cc().ok(),
193            ar: toolchain::find_ar().ok(),
194            unstable_target_spec: false,
195        })
196    }
197}
198
199const DEFAULT_TARGET: &str = const { formatcp!("{ARCH}-hyperlight-none") };
200
201#[derive(Default)]
202//#[command(disable_help_subcommand = true)]
203struct ArgsImpl {
204    /// Path to Cargo.toml
205    manifest_path: Option<PathBuf>,
206
207    /// Directory for all generated artifacts
208    target_dir: Option<PathBuf>,
209
210    /// Target triple to build for
211    target: Option<String>,
212
213    /// Target triple to use for host utilities/wrappers, enabling a
214    /// building a distributable C sysroot for Canadian cross usecases
215    host: Option<String>,
216
217    /// Whether to include hyperlight-guest-capi headers and libs in
218    /// the built sysroot, used for building distributable C sysroots
219    with_guest_capi: bool,
220
221    /// When building a C sysroot, the target C sysroot directory
222    c_sysroot_dir: Option<PathBuf>,
223
224    /// Environment variables to set
225    env: HashMap<OsString, OsString>,
226
227    /// Current working directory
228    pub current_dir: PathBuf,
229}
230
231fn parse_flag(flag: &str, arg: &OsStr) -> Option<bool> {
232    let value = arg.strip_prefix(flag)?;
233    if value.is_empty() {
234        Some(true)
235    } else {
236        let lower = value.strip_prefix("=")?.to_ascii_lowercase();
237        if lower == "false" || lower == "0" {
238            Some(false)
239        } else {
240            Some(true)
241        }
242    }
243}
244
245fn parse_arg(
246    flag: &str,
247    arg: &OsStr,
248    args: &mut impl Iterator<Item = OsString>,
249) -> Option<OsString> {
250    let value = arg.strip_prefix(flag)?;
251    if value.is_empty() {
252        args.next()
253    } else {
254        value.strip_prefix("=").map(OsStr::to_os_string)
255    }
256}
257
258impl ArgsImpl {
259    pub fn parse_args(args: impl IntoIterator<Item = impl Into<OsString> + Clone>) -> Self {
260        let mut this = Self::default();
261        let mut args = args.into_iter().map(Into::into);
262
263        while let Some(arg) = args.next() {
264            if arg == "--" {
265                break;
266            }
267            if let Some(path) = parse_arg("--manifest-path", &arg, &mut args) {
268                this.manifest_path = Some(PathBuf::from(path));
269                continue;
270            }
271            if let Some(dir) = parse_arg("--target-dir", &arg, &mut args) {
272                this.target_dir = Some(PathBuf::from(dir));
273                continue;
274            }
275            if let Some(triplet) = parse_arg("--target", &arg, &mut args) {
276                this.target = Some(triplet.to_string_lossy().to_string());
277                continue;
278            }
279            if let Some(host) = parse_arg("--host", &arg, &mut args) {
280                this.host = Some(host.to_string_lossy().to_string());
281            }
282            if let Some(capi) = parse_flag("--with-guest-capi", &arg) {
283                this.with_guest_capi = capi;
284            }
285            if let Some(dir) = parse_arg("--c-sysroot-dir", &arg, &mut args) {
286                this.c_sysroot_dir = Some(PathBuf::from(dir));
287            }
288        }
289        this
290    }
291}
292
293#[derive(serde::Deserialize)]
294struct CargoMetadata {
295    target_directory: PathBuf,
296}
297
298fn resolve_target_dir(
299    manifest_path: &Option<PathBuf>,
300    env: &HashMap<OsString, OsString>,
301    cwd: &PathBuf,
302) -> Result<PathBuf> {
303    let output = cargo_cmd()?
304        .env_clear()
305        .envs(env.iter())
306        .current_dir(cwd)
307        .arg("metadata")
308        .manifest_path(manifest_path)
309        .arg("--format-version=1")
310        .arg("--no-deps")
311        .checked_output()
312        .context("Failed to get cargo metadata")?;
313
314    let metadata: CargoMetadata =
315        serde_json::from_slice(&output.stdout).context("Failed to parse cargo metadata")?;
316
317    Ok(metadata.target_directory)
318}
319
320fn resolve_target(env: &HashMap<OsString, OsString>, cwd: &PathBuf) -> Result<String> {
321    let output = cargo_cmd()?
322        .env_clear()
323        .envs(env.iter())
324        .current_dir(cwd)
325        .arg("config")
326        .arg("get")
327        .arg("--quiet")
328        .arg("--format=json-value")
329        .arg("-Zunstable-options")
330        .arg("build.target")
331        // cargo config is an unstable feature
332        .allow_unstable()
333        // use output instead of checked_output
334        // as cargo will error if build.target is not set
335        .output()
336        .context("Failed to get cargo config")?;
337
338    let target = String::from_utf8_lossy(&output.stdout);
339    let target = target.trim();
340    let target = target.trim_matches(|c| c == '"' || c == '\'');
341
342    if target.is_empty() {
343        Ok(DEFAULT_TARGET.into())
344    } else {
345        Ok(target.into())
346    }
347}