aboutsummaryrefslogtreecommitdiff
path: root/mingling_cli/src/metadata/setup.rs
blob: 198baf1ff568744cd3c7ce50d510ec2014e23265 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
use std::{env::current_dir, path::PathBuf};

use cargo_metadata::{CargoOpt, MetadataCommand};
use mingling::{
    LazyInit, Program,
    macros::program_setup,
    picker::{IntoPicker, value::Flag},
    prelude::*,
    res::ResCurrentDir,
};

/// Name of Cargo manifest file
pub const CARGO_TOML: &str = "Cargo.toml";

use cargo_metadata::Metadata;
use serde::Serialize;

/// Resource holding parsed `cargo metadata` output.
///
/// This is lazily initialized during program setup by calling `cargo metadata`
/// with the appropriate CLI flags (e.g., `--features`, `--all-features`,
/// `--no-default-features`, `--no-deps`, `--message-format`).
///
/// Access the inner [`Metadata`] via [`ResMetadata::data()`], which panics if
/// called before initialization (guaranteed not to happen in normal usage).
#[derive(Default, Clone, Serialize)]
pub struct ResMetadata {
    data: Option<Metadata>,
}

/// Resource indicating whether the output format is JSON.
///
/// Set to `true` when `--message-format json` (or similar) is passed.
/// Used by renderers to decide whether to serialize structs as JSON.
#[derive(Default, Clone, Serialize)]
pub struct ResUsingJson {
    pub using: bool,
}

impl ResMetadata {
    /// Returns a reference to the parsed `cargo metadata`.
    ///
    /// # Panics
    ///
    /// This function does **not** panic in practice, because `ResMetadata` is
    /// always initialized via [`LazyInit`] inside the program setup, and the
    /// initialization either succeeds (setting `data` to `Some(...)`) or fails
    /// by propagating the error from `cmd.exec().unwrap()`. Therefore, by the
    /// time this getter is called, `self.data` is guaranteed to be `Some`.
    pub fn data(&self) -> &Metadata {
        self.data.as_ref().unwrap()
    }
}

#[program_setup]
pub fn cargo_metadata_setup(program: &mut Program<crate::ThisProgram>) {
    let args = program.get_args().to_vec();

    let (
        feature_args,
        manifest_path,
        message_format,
        enable_all_features,
        no_default_features,
        no_deps,
    ) = args
        .pick(&arg![features: Vec<String>])
        .pick(&arg![manifest_path: Option<String>])
        .pick_or(&arg![message_format: String], || "disable".to_string())
        .pick(&arg![all_features: Flag])
        .pick(&arg![no_default_features: Flag])
        .pick(&arg![no_deps: Flag])
        .unwrap();

    // Is Using Json
    program.with_resource(ResUsingJson {
        using: message_format.contains("json"),
    });

    // Current Dir
    let current_dir = current_dir().unwrap();

    // Leak `feature_args` into a static slice so it can be captured by the metadata closure.
    // Since the process does not loop, this memory leak is harmless.
    let feature_args_leaked: &[String] = Box::leak(feature_args.into_boxed_slice());
    let manifest_path_clone = manifest_path.clone();
    let current_dir_clone = current_dir.clone();

    program.with_resource(ResMetadata::lazy_init(move || {
        // Paths
        let metadata_path = match manifest_path_clone {
            Some(ref path_str) => PathBuf::from(path_str),
            None => find_manifest().unwrap(),
        };

        // Cargo Metadata - bind to a longer-lived variable
        let mut cmd = MetadataCommand::new();
        cmd.manifest_path(metadata_path)
            .current_dir(&current_dir_clone);

        if *enable_all_features {
            cmd.features(CargoOpt::AllFeatures);
        }

        if *no_default_features {
            cmd.features(CargoOpt::NoDefaultFeatures);
        }

        if *no_deps {
            cmd.no_deps();
        }

        cmd.features(CargoOpt::SomeFeatures(feature_args_leaked.to_vec()));

        ResMetadata {
            data: Some(cmd.exec().unwrap()),
        }
    }));

    // Current Dir
    program.with_resource(ResCurrentDir::from(current_dir));
}

/// Find `Cargo.toml` by searching upward from `current_dir`.
fn find_manifest() -> Option<PathBuf> {
    let mut dir = current_dir().ok()?;
    loop {
        let candidate = dir.join(CARGO_TOML);
        if candidate.is_file() {
            return Some(candidate);
        }
        if !dir.pop() {
            return None;
        }
    }
}