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