1use std::ffi::OsStr;
2use std::io;
3use std::ops::RangeInclusive;
4use std::path::PathBuf;
5use std::process::Command;
6
7use thiserror::Error;
8
9use crate::command::internal_command::InternalScarbCommandBuilder;
10use crate::{Metadata, VersionPin};
11
12#[derive(Error, Debug)]
14#[non_exhaustive]
15pub enum MetadataCommandError {
16 #[error("`scarb metadata` command did not produce any metadata")]
18 NotFound {
19 stdout: String,
21 },
22
23 #[error("failed to read `scarb metadata` output")]
25 Io(#[from] io::Error),
26
27 #[error("failed to deserialize `scarb metadata` output")]
29 Json(#[from] serde_json::Error),
30
31 #[error("`scarb metadata` exited with error\n\nstdout:\n{stdout}\nstderr:\n{stderr}")]
33 ScarbError {
34 stdout: String,
36 stderr: String,
38 },
39}
40
41impl MetadataCommandError {
42 pub const fn did_not_found(&self) -> bool {
44 matches!(self, Self::NotFound { .. })
45 }
46}
47
48#[derive(Clone, Debug, Default)]
53pub struct MetadataCommand {
54 inner: InternalScarbCommandBuilder,
55 no_deps: bool,
56 inherit_stdout: bool,
57 json: bool,
58}
59
60impl MetadataCommand {
61 pub fn new() -> Self {
64 Self::default()
65 }
66
67 pub fn scarb_path(&mut self, path: impl Into<PathBuf>) -> &mut Self {
72 self.inner.scarb_path(path);
73 self
74 }
75
76 pub fn manifest_path(&mut self, path: impl Into<PathBuf>) -> &mut Self {
80 self.inner.manifest_path(path);
81 self
82 }
83
84 pub fn current_dir(&mut self, path: impl Into<PathBuf>) -> &mut Self {
86 self.inner.current_dir(path);
87 self
88 }
89
90 pub fn no_deps(&mut self) -> &mut Self {
92 self.no_deps = true;
93 self
94 }
95
96 pub fn profile(&mut self, profile: impl AsRef<OsStr>) -> &mut Self {
98 self.env("SCARB_PROFILE", profile)
99 }
100
101 pub fn dev(&mut self) -> &mut Self {
103 self.profile("dev")
104 }
105
106 pub fn release(&mut self) -> &mut Self {
108 self.profile("release")
109 }
110
111 pub fn env(&mut self, key: impl AsRef<OsStr>, val: impl AsRef<OsStr>) -> &mut Self {
113 self.inner.env(key, val);
114 self
115 }
116
117 pub fn envs<I, K, V>(&mut self, vars: I) -> &mut Self
119 where
120 I: IntoIterator<Item = (K, V)>,
121 K: AsRef<OsStr>,
122 V: AsRef<OsStr>,
123 {
124 self.inner.envs(vars);
125 self
126 }
127
128 pub fn env_remove(&mut self, key: impl AsRef<OsStr>) -> &mut Self {
130 self.inner.env_remove(key);
131 self
132 }
133
134 pub fn env_clear(&mut self) -> &mut Self {
136 self.inner.env_clear();
137 self
138 }
139
140 pub fn inherit_stderr(&mut self) -> &mut Self {
142 self.inner.inherit_stderr();
143 self
144 }
145
146 pub fn inherit_stdout(&mut self) -> &mut Self {
148 self.inherit_stdout = true;
151 self
152 }
153
154 pub fn json(&mut self) -> &mut Self {
156 self.json = true;
157 self
158 }
159
160 fn scarb_command(&self) -> Command {
161 let mut builder = self.inner.clone();
162 if self.json {
163 builder.json();
164 }
165 builder.args(["metadata", "--format-version"]);
166 builder.arg(VersionPin.numeric().to_string());
167 if self.no_deps {
168 builder.arg("--no-deps");
169 }
170 builder.command()
171 }
172
173 pub fn exec(&self) -> Result<Metadata, MetadataCommandError> {
175 let mut cmd = self.scarb_command();
176
177 let output = cmd.output()?;
178
179 let stdout_string = String::from_utf8_lossy(&output.stdout).to_string();
180
181 if output.status.success() {
182 let parse_result = parse_stream(stdout_string.clone());
183
184 let data = parse_result
185 .as_ref()
186 .map(|parse_result| {
188 stdout_string
189 .split('\n')
190 .enumerate()
191 .filter(|(n, _)| !parse_result.used_lines.contains(n))
192 .map(|(_, line)| line)
193 .collect::<Vec<_>>()
194 .join("\n")
195 })
196 .unwrap_or(stdout_string);
197
198 self.print(&data);
199
200 parse_result.map(|result| result.metadata)
201 } else {
202 self.print(&stdout_string);
203
204 Err(MetadataCommandError::ScarbError {
205 stdout: stdout_string,
206 stderr: String::from_utf8_lossy(&output.stderr).into(),
207 })
208 }
209 }
210
211 fn print(&self, data: &str) {
212 if self.inherit_stdout {
213 print!("{data}");
214 }
215 }
216}
217
218#[derive(Debug)]
219struct ParseResult {
220 metadata: Metadata,
221 used_lines: RangeInclusive<usize>,
223}
224
225impl ParseResult {
226 fn new(metadata: Metadata, used_lines: RangeInclusive<usize>) -> Self {
227 Self {
228 metadata,
229 used_lines,
230 }
231 }
232}
233
234fn parse_stream(stdout: String) -> Result<ParseResult, MetadataCommandError> {
235 const OPEN_BRACKET: &str = "{";
236 const CLOSE_BRACKET: &str = "}";
237
238 let mut err = None;
239 let mut lines = stdout.split('\n').map(|line| line.trim_end()).enumerate();
240
241 for (n, line) in lines
246 .clone()
247 .filter(|(_, line)| line.starts_with(OPEN_BRACKET) && line.ends_with(CLOSE_BRACKET))
248 {
249 match serde_json::from_str(line) {
250 Ok(metadata) => return Ok(ParseResult::new(metadata, n..=n)),
251 Err(serde_err) => err = Some(serde_err.into()),
252 }
253 }
254 loop {
256 let json_lines = lines
257 .by_ref()
258 .skip_while(|(_, line)| *line != OPEN_BRACKET)
259 .skip(1)
260 .take_while(|(_, line)| *line != CLOSE_BRACKET);
261
262 let json_lines = json_lines.collect::<Vec<_>>();
263
264 let used_lines = match (json_lines.first(), json_lines.last()) {
265 (Some((first, _)), Some((last, _))) => *first - 1..=*last + 1,
266 _ => break,
267 };
268 let json_string = json_lines
269 .into_iter()
270 .map(|(_, line)| line)
271 .collect::<Vec<_>>()
272 .join("");
273
274 match serde_json::from_str(&format!("{OPEN_BRACKET}{json_string}{CLOSE_BRACKET}")) {
275 Ok(metadata) => return Ok(ParseResult::new(metadata, used_lines)),
276 Err(serde_err) => err = Some(serde_err.into()),
277 }
278 }
279
280 Err(err.unwrap_or(MetadataCommandError::NotFound { stdout }))
281}
282
283#[cfg(test)]
284mod tests {
285 use semver::Version;
286 use std::ffi::OsStr;
287
288 use crate::{
289 CairoVersionInfo, Metadata, MetadataCommand, MetadataCommandError, VersionInfo,
290 WorkspaceMetadata,
291 };
292
293 macro_rules! check_parse_stream {
294 ($input:expr, $expected:pat) => {{
295 #![allow(clippy::redundant_pattern_matching)]
296 let actual = crate::command::metadata_command::parse_stream(
297 $input
298 .to_string()
299 .replace("{meta}", &minimal_metadata_json()),
300 );
301
302 assert!(matches!(actual, $expected));
303
304 let actual = crate::command::metadata_command::parse_stream(
305 $input
306 .to_string()
307 .replace("{meta}", &minimal_metadata_json_pretty()),
308 );
309
310 assert!(matches!(actual, $expected));
311 }};
312 }
313
314 #[test]
315 fn parse_stream_ok() {
316 check_parse_stream!("{meta}", Ok(_));
317 }
318
319 #[test]
320 fn parse_stream_ok_nl() {
321 check_parse_stream!("{meta}\n", Ok(_));
322 }
323
324 #[test]
325 fn parse_stream_trailing_nl() {
326 check_parse_stream!("\n\n\n\n{meta}\n\n\n", Ok(_));
327 }
328
329 #[test]
330 fn parse_stream_ok_random_text_around() {
331 check_parse_stream!("abcde\n{meta}\nghjkl", Ok(_));
332 }
333
334 #[test]
335 fn parse_stream_empty() {
336 check_parse_stream!("", Err(MetadataCommandError::NotFound { .. }));
337 }
338
339 #[test]
340 fn parse_stream_empty_nl() {
341 check_parse_stream!('\n', Err(MetadataCommandError::NotFound { .. }));
342 }
343
344 #[test]
345 fn parse_stream_garbage_message() {
346 check_parse_stream!("{\"foo\":1}", Err(MetadataCommandError::Json(_)));
347 }
348
349 #[test]
350 fn parse_stream_garbage_message_nl() {
351 check_parse_stream!("{\"foo\":1}\n", Err(MetadataCommandError::Json(_)));
352 }
353
354 #[test]
355 fn parse_stream_garbage_messages() {
356 check_parse_stream!(
357 "{\"foo\":1}\n{\"bar\":1}",
358 Err(MetadataCommandError::Json(_))
359 );
360 }
361
362 #[test]
363 fn parse_stream_not_serializable() {
364 check_parse_stream!(
365 "{\"version\":\"x\",\"foo\":1}",
366 Err(MetadataCommandError::Json(_))
367 );
368 }
369
370 #[test]
371 fn parse_stream_version_0() {
372 check_parse_stream!(
373 "{\"version\":0,\"foo\":1}",
374 Err(MetadataCommandError::Json(_))
375 );
376 }
377
378 #[test]
379 fn parse_stream_impersonator() {
380 check_parse_stream!("{\"version\":0,\"foo\":1}\n{meta}", Ok(_));
381 }
382
383 #[test]
384 fn parse_stream_crlf() {
385 check_parse_stream!(
386 "{\"foo\":1}\r\n{\"foo\":1}\r\n{meta}\r\n{\"foo\":1}\r\n",
387 Ok(_)
388 );
389 }
390
391 fn minimal_metadata_json() -> String {
392 serde_json::to_string(&minimal_metadata()).unwrap()
393 }
394
395 fn minimal_metadata_json_pretty() -> String {
396 serde_json::to_string_pretty(&minimal_metadata()).unwrap()
397 }
398
399 fn minimal_metadata() -> Metadata {
400 Metadata {
401 version: Default::default(),
402 app_exe: Default::default(),
403 app_version_info: VersionInfo {
404 version: Version::new(1, 0, 0),
405 commit_info: Default::default(),
406 cairo: CairoVersionInfo {
407 version: Version::new(1, 0, 0),
408 commit_info: Default::default(),
409 extra: Default::default(),
410 },
411 extra: Default::default(),
412 },
413 target_dir: Default::default(),
414 runtime_manifest: Default::default(),
415 workspace: WorkspaceMetadata {
416 manifest_path: Default::default(),
417 root: Default::default(),
418 members: Default::default(),
419 extra: Default::default(),
420 },
421 packages: Default::default(),
422 compilation_units: Default::default(),
423 current_profile: "dev".into(),
424 profiles: vec!["dev".into()],
425 extra: Default::default(),
426 }
427 }
428
429 #[test]
430 fn can_define_profile() {
431 let mut cmd = MetadataCommand::new();
432 cmd.profile("test");
433 assert_profile(cmd, "test");
434
435 let mut cmd = MetadataCommand::new();
436 cmd.dev();
437 assert_profile(cmd, "dev");
438
439 let mut cmd = MetadataCommand::new();
440 cmd.profile("test");
441 cmd.release();
442 assert_profile(cmd, "release");
443 }
444
445 fn assert_profile(cmd: MetadataCommand, profile: impl AsRef<OsStr>) {
446 let cmd = cmd.scarb_command();
447 let (_key, Some(val)) = cmd.get_envs().find(|(k, _)| k == &"SCARB_PROFILE").unwrap() else {
448 panic!("profile not defined")
449 };
450 assert_eq!(val, profile.as_ref());
451 }
452}