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)]
198struct ArgsImpl {
200 manifest_path: Option<PathBuf>,
202
203 target_dir: Option<PathBuf>,
205
206 target: Option<String>,
208
209 host: Option<String>,
212
213 with_guest_capi: bool,
216
217 c_sysroot_dir: Option<PathBuf>,
219
220 env: HashMap<OsString, OsString>,
222
223 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 .allow_unstable()
329 .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}