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 #[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 #[derive(Config)]
33 #[config(allow_unknown_fields)]
34 pub struct ToolchainPluginConfig {
35 #[setting(default = true)]
39 pub inherit_aliases: bool,
40
41 #[setting(default = true)]
45 pub install_dependencies: bool,
46
47 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub plugin: Option<PluginLocator>,
50
51 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub version: Option<UnresolvedVersionSpec>,
54
55 pub version_from_prototools: ToolchainPluginVersionFrom,
59
60 #[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 #[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 #[setting(extend, validate = validate::extends_from)]
91 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub extends: Option<schematic::ExtendsFrom>,
93
94 #[setting(nested)]
96 pub moon: MoonConfig,
97
98 #[setting(nested)]
100 pub proto: ProtoConfig,
101
102 #[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 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}