Skip to main content

moon_config/
toolchains_config.rs

1use crate::patterns::{merge_iter, merge_plugin_partials};
2use crate::toolchain::*;
3use crate::{config_enum, config_struct};
4use miette::IntoDiagnostic;
5use moon_common::{Id, IdExt};
6use rustc_hash::FxHashMap;
7use schematic::{Config, Schematic, validate};
8use serde_json::Value;
9use std::collections::BTreeMap;
10use std::env;
11use version_spec::UnresolvedVersionSpec;
12use warpgate_api::PluginLocator;
13
14config_enum!(
15    /// The strategy in which to inherit a version from `.prototools`.
16    #[derive(Schematic)]
17    #[serde(untagged)]
18    pub enum ToolchainPluginVersionFrom {
19        Enabled(bool),
20        Id(String),
21    }
22);
23
24impl Default for ToolchainPluginVersionFrom {
25    fn default() -> Self {
26        Self::Enabled(true)
27    }
28}
29
30config_struct!(
31    /// Configures an individual toolchain.
32    #[derive(Config)]
33    #[config(allow_unknown_fields)]
34    pub struct ToolchainPluginConfig {
35        /// Inherit aliases (name derived from a manifest) for all
36        /// projects associated with this toolchain.
37        /// @since 2.1.0
38        #[setting(default = true)]
39        pub inherit_aliases: bool,
40
41        /// Run the `InstallDependencies` actions for each running task
42        /// when changes to lockfiles and manifests are detected.
43        /// @since 2.1.0
44        #[setting(default = true)]
45        pub install_dependencies: bool,
46
47        /// Location of the WASM plugin to use.
48        #[serde(default, skip_serializing_if = "Option::is_none")]
49        pub plugin: Option<PluginLocator>,
50
51        /// The version of the toolchain to download and install.
52        #[serde(default, skip_serializing_if = "Option::is_none")]
53        pub version: Option<UnresolvedVersionSpec>,
54
55        /// Inherit the version from the root `.prototools`.
56        /// When true, matches using the same identifier, otherwise a
57        /// string can be provided for a custom identifier.
58        pub version_from_prototools: ToolchainPluginVersionFrom,
59
60        /// Arbitrary configuration that'll be passed to the WASM plugin.
61        #[setting(flatten, merge = merge_iter)]
62        pub config: BTreeMap<String, Value>,
63    }
64);
65
66impl ToolchainPluginConfig {
67    pub fn to_json(&self) -> Value {
68        let mut data = Value::Object(self.config.clone().into_iter().collect());
69
70        if let Some(version) = &self.version {
71            data["version"] = Value::String(version.to_string());
72        }
73
74        data
75    }
76}
77
78config_struct!(
79    /// Configures all toolchains.
80    /// Docs: https://moonrepo.dev/docs/config/toolchain
81    #[derive(Config)]
82    #[config(allow_unknown_fields)]
83    pub struct ToolchainsConfig {
84        #[setting(default = "./cache/schemas/toolchains.json", rename = "$schema")]
85        pub schema: String,
86
87        /// Extends one or many toolchain configuration files.
88        /// Supports a relative file path or a secure URL.
89        /// @since 1.12.0
90        #[setting(extend, validate = validate::extends_from)]
91        #[serde(default, skip_serializing_if = "Option::is_none")]
92        pub extends: Option<schematic::ExtendsFrom>,
93
94        /// Configures moon itself.
95        #[setting(nested)]
96        pub moon: MoonConfig,
97
98        /// Configures how moon integrates with proto.
99        #[setting(nested)]
100        pub proto: ProtoConfig,
101
102        /// Configures and integrates toolchains into the system using
103        /// a unique identifier.
104        #[setting(flatten, nested, merge = merge_plugin_partials)]
105        pub plugins: FxHashMap<Id, ToolchainPluginConfig>,
106    }
107);
108
109impl ToolchainsConfig {
110    pub fn get_enabled(&self) -> Vec<Id> {
111        let mut tools = self.plugins.keys().cloned().collect::<Vec<_>>();
112        tools.push(Id::raw("system"));
113        tools
114    }
115
116    pub fn get_plugin_config(&self, id: impl AsRef<str>) -> Option<&ToolchainPluginConfig> {
117        let (stable_id, unstable_id) = Id::stable_and_unstable(id);
118
119        self.plugins
120            .get(&stable_id)
121            .or_else(|| self.plugins.get(&unstable_id))
122    }
123
124    pub fn inherit_versions_from_env_vars(&mut self) -> miette::Result<()> {
125        for (id, config) in &mut self.plugins {
126            if let Ok(version) = env::var(format!("MOON_{}_VERSION", id.to_env_var())) {
127                config.version = Some(UnresolvedVersionSpec::parse(version).into_diagnostic()?);
128            }
129        }
130
131        Ok(())
132    }
133}
134
135#[cfg(feature = "proto")]
136impl ToolchainsConfig {
137    pub fn requires_proto(&self) -> bool {
138        for config in self.plugins.values() {
139            if config.version.is_some() {
140                return true;
141            }
142        }
143
144        false
145    }
146
147    pub fn get_plugin_locator(id: &Id) -> Option<proto_core::PluginLocator> {
148        use proto_core::warpgate::find_debug_locator_with_url_fallback as locate;
149
150        match id.as_str() {
151            "bun" => Some(locate("bun_toolchain", "1.0.2")),
152            "deno" => Some(locate("deno_toolchain", "1.0.3")),
153            "go" => Some(locate("go_toolchain", "1.1.1")),
154            "javascript" => Some(locate("javascript_toolchain", "1.0.6")),
155            "node" => Some(locate("node_toolchain", "1.0.2")),
156            "npm" => Some(locate("node_depman_toolchain", "1.0.2")),
157            "pnpm" => Some(locate("node_depman_toolchain", "1.0.2")),
158            "rust" => Some(locate("rust_toolchain", "1.0.5")),
159            "system" => Some(locate("system_toolchain", "1.0.2")),
160            "typescript" => Some(locate("typescript_toolchain", "1.1.1")),
161            "unstable_python" => Some(locate("python_toolchain", "0.1.5")),
162            "unstable_pip" => Some(locate("python_pip_toolchain", "0.1.2")),
163            "unstable_uv" => Some(locate("python_uv_toolchain", "0.1.2")),
164            "yarn" => Some(locate("node_depman_toolchain", "1.0.2")),
165            _ => None,
166        }
167    }
168
169    pub fn inherit_defaults(
170        &mut self,
171        proto_config: &proto_core::ProtoConfig,
172    ) -> miette::Result<()> {
173        self.inherit_proto_versions_for_plugins(proto_config)?;
174        self.inherit_default_plugins();
175        self.inherit_plugin_locators()?;
176
177        Ok(())
178    }
179
180    pub fn inherit_proto_versions_for_plugins(
181        &mut self,
182        proto_config: &proto_core::ProtoConfig,
183    ) -> miette::Result<()> {
184        use moon_common::color;
185        use proto_core::ToolContext;
186        use tracing::trace;
187
188        for (id, config) in &mut self.plugins {
189            if config.version.is_some() {
190                continue;
191            }
192
193            let proto_id = match &config.version_from_prototools {
194                ToolchainPluginVersionFrom::Enabled(enabled) => {
195                    if *enabled {
196                        id.as_str().strip_prefix("unstable_").unwrap_or(id.as_str())
197                    } else {
198                        continue;
199                    }
200                }
201                ToolchainPluginVersionFrom::Id(custom_id) => custom_id,
202            };
203            let proto_context = ToolContext::parse(proto_id).unwrap();
204
205            if let Some(version) = proto_config.versions.get(&proto_context) {
206                trace!(
207                    "Inheriting {} version {} from .prototools",
208                    color::id(id),
209                    version
210                );
211
212                config.version = Some(version.req.to_owned());
213            }
214        }
215
216        Ok(())
217    }
218
219    pub fn inherit_default_plugins(&mut self) {
220        self.plugins.entry(Id::raw("system")).or_default();
221    }
222
223    pub fn inherit_test_plugins(&mut self) -> miette::Result<()> {
224        for id in [
225            "tc-tier1",
226            "tc-tier2",
227            "tc-tier2-reqs",
228            "tc-tier2-setup-env",
229            "tc-tier3",
230            "tc-tier3-reqs",
231        ] {
232            self.plugins.entry(Id::raw(id)).or_default();
233        }
234
235        Ok(())
236    }
237
238    pub fn inherit_test_builtin_plugins(&mut self) -> miette::Result<()> {
239        // We don't need all package managers
240        for id in [
241            "bun",
242            "deno",
243            "go",
244            "javascript",
245            "node",
246            "npm",
247            "rust",
248            "system",
249            "typescript",
250            "unstable_python",
251            "unstable_pip",
252        ] {
253            self.plugins.entry(Id::raw(id)).or_default();
254        }
255
256        Ok(())
257    }
258
259    pub fn inherit_plugin_locators(&mut self) -> miette::Result<()> {
260        use schematic::{ConfigError, Path, PathSegment, ValidateError, ValidatorError};
261
262        for (id, config) in self.plugins.iter_mut() {
263            if config.plugin.is_some() {
264                continue;
265            }
266
267            match id.as_str() {
268                "bun" | "deno" | "go" | "javascript" | "node" | "npm" | "pnpm" | "rust"
269                | "system" | "typescript" | "unstable_python" | "unstable_pip" | "unstable_uv"
270                | "yarn" => {
271                    config.plugin = Self::get_plugin_locator(id);
272                }
273                #[cfg(debug_assertions)]
274                "tc-tier1" | "tc-tier2" | "tc-tier2-reqs" | "tc-tier2-setup-env" | "tc-tier3"
275                | "tc-tier3-reqs" => {
276                    use proto_core::warpgate::find_debug_locator;
277
278                    config.plugin = Some(
279                        find_debug_locator(&id.replace("-", "_"))
280                            .expect("Development plugins missing, build with `just build-wasm`!"),
281                    );
282
283                    if id.contains("tc-tier3") {
284                        config.version = UnresolvedVersionSpec::parse("1.2.3").ok();
285                    }
286                }
287                other => {
288                    return Err(ConfigError::Validator {
289                        location: ".moon/toolchains.*".into(),
290                        error: Box::new(ValidatorError {
291                            errors: vec![ValidateError {
292                                message:
293                                    "a locator is required for plugins; accepts file paths and URLs"
294                                        .into(),
295                                path: Path::new(vec![
296                                    PathSegment::Key(other.to_string()),
297                                    PathSegment::Key("plugin".into()),
298                                ]),
299                            }],
300                        }),
301                        help: None,
302                    }
303                    .into());
304                }
305            };
306        }
307
308        Ok(())
309    }
310}