scarb_metadata/command/
metadata_command.rs

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/// Error thrown while trying to read `scarb metadata`.
13#[derive(Error, Debug)]
14#[non_exhaustive]
15pub enum MetadataCommandError {
16    /// `scarb metadata` command did not produce any metadata
17    #[error("`scarb metadata` command did not produce any metadata")]
18    NotFound {
19        /// Captured standard output if any.
20        stdout: String,
21    },
22
23    /// Failed to read `scarb metadata` output.
24    #[error("failed to read `scarb metadata` output")]
25    Io(#[from] io::Error),
26
27    /// Failed to deserialize `scarb metadata` output.
28    #[error("failed to deserialize `scarb metadata` output")]
29    Json(#[from] serde_json::Error),
30
31    /// Error during execution of `scarb metadata`.
32    #[error("`scarb metadata` exited with error\n\nstdout:\n{stdout}\nstderr:\n{stderr}")]
33    ScarbError {
34        /// Captured standard output if any.
35        stdout: String,
36        /// Captured standard error if any.
37        stderr: String,
38    },
39}
40
41impl MetadataCommandError {
42    /// Check if this is [`MetadataCommandError::NotFound`].
43    pub const fn did_not_found(&self) -> bool {
44        matches!(self, Self::NotFound { .. })
45    }
46}
47
48/// A builder for `scarb metadata` command invocation.
49///
50/// In detail, this will always execute `scarb --json metadata --format-version N`, where `N`
51/// matches metadata version understandable by this crate version.
52#[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    /// Creates a default `scarb metadata` command, which will look for `scarb` in `$PATH` and
62    /// for `Scarb.toml` in the current directory or its ancestors.
63    pub fn new() -> Self {
64        Self::default()
65    }
66
67    /// Path to `scarb` executable.
68    ///
69    /// If not set, this will use the `$SCARB` environment variable, and if that is not set, it
70    /// will simply be `scarb` and the system will look it up in `$PATH`.
71    pub fn scarb_path(&mut self, path: impl Into<PathBuf>) -> &mut Self {
72        self.inner.scarb_path(path);
73        self
74    }
75
76    /// Path to `Scarb.toml`.
77    ///
78    /// If not set, this will look for `Scarb.toml` in the current directory or its ancestors.
79    pub fn manifest_path(&mut self, path: impl Into<PathBuf>) -> &mut Self {
80        self.inner.manifest_path(path);
81        self
82    }
83
84    /// Current directory of the `scarb metadata` process.
85    pub fn current_dir(&mut self, path: impl Into<PathBuf>) -> &mut Self {
86        self.inner.current_dir(path);
87        self
88    }
89
90    /// Output information only about workspace members and don't fetch dependencies.
91    pub fn no_deps(&mut self) -> &mut Self {
92        self.no_deps = true;
93        self
94    }
95
96    /// Defines profile to use for `scarb metadata` command.
97    pub fn profile(&mut self, profile: impl AsRef<OsStr>) -> &mut Self {
98        self.env("SCARB_PROFILE", profile)
99    }
100
101    /// Defines profile to use for `scarb metadata` command as "dev".
102    pub fn dev(&mut self) -> &mut Self {
103        self.profile("dev")
104    }
105
106    /// Defines profile to use for `scarb metadata` command as "release".
107    pub fn release(&mut self) -> &mut Self {
108        self.profile("release")
109    }
110
111    /// Inserts or updates an environment variable mapping.
112    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    /// Adds or updates multiple environment variable mappings.
118    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    /// Removes an environment variable mapping.
129    pub fn env_remove(&mut self, key: impl AsRef<OsStr>) -> &mut Self {
130        self.inner.env_remove(key);
131        self
132    }
133
134    /// Clears the entire environment map for the child process.
135    pub fn env_clear(&mut self) -> &mut Self {
136        self.inner.env_clear();
137        self
138    }
139
140    /// Inherit standard error, i.e. show Scarb errors in this process's standard error.
141    pub fn inherit_stderr(&mut self) -> &mut Self {
142        self.inner.inherit_stderr();
143        self
144    }
145
146    /// Inherit standard output, i.e. show Scarb output in this process's standard output.
147    pub fn inherit_stdout(&mut self) -> &mut Self {
148        // we can not just use self.inner.inherit_stdout()
149        // because it will make output.stdout empty
150        self.inherit_stdout = true;
151        self
152    }
153
154    /// Set output format to JSON.
155    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    /// Runs configured `scarb metadata` and returns parsed `Metadata`.
174    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                // if we parsed successfully dont print lines consumed for printing
187                .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    /// lines from `scarb metadata` output that were consumed for parsing [`Metadata`]
222    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    // depending on usage of --json flag scarb returns either one line json
242    // or pretty printed one which starts with "{" and ends with "}" on single lines
243    //
244    // singleline json's
245    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    // multiline json's
255    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}