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 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)]
202struct ArgsImpl {
204 manifest_path: Option<PathBuf>,
206
207 target_dir: Option<PathBuf>,
209
210 target: Option<String>,
212
213 host: Option<String>,
216
217 with_guest_capi: bool,
220
221 c_sysroot_dir: Option<PathBuf>,
223
224 env: HashMap<OsString, OsString>,
226
227 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 .allow_unstable()
333 .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}