cargo_dist/config/v1/mod.rs
1//! The dist 1.0 config format (as opposed to the old v0 format)
2//!
3//! This is the config subsystem!
4//!
5//! # Concepts
6//!
7//! It's responsible for loading, merging, and auto-detecting all the various config
8//! sources. There are two closely related families of types:
9//!
10//! - `...Config` types are the "complete" values that will be passed around to the rest
11//! of the program. All of these types get shoved into the top-level [`Config`][] type.
12//!
13//! - `...Layer` types are "partial" values that are loaded and parsed before being merged
14//! into the final [`Config`][]. Notably the dist(-workspace).toml is loaded as [`TomlLayer`][].
15//!
16//! Nested types like [`WorkspaceInstallerConfig`][] usually have a paired layer ([`InstallerLayer`][]),
17//! with an almost identical definition. The differences usually lie in the Layer having far more
18//! Options, because you don't need to specify it in your oranda.json but we want the rest of our
19//! code to have the final result fully resolved.
20//!
21//!
22//! # The ORIGINAL Big Idea
23//!
24//! These ideas don't hold anymore but they're informative of how we ended up with the
25//! current design. The next section discusses where we ended up and why.
26//!
27//! - a `...Config` type implements [`Default`][] manually to specify default values
28//! - a `...Config` type implements [`ApplyLayer`][] to specify how its `...Layer` gets combined
29//!
30//! Conveniences like [`ApplyValExt::apply_val`][] and [`ApplyOptExt::apply_opt`][]
31//! exist to help merge simple values like `bool <- Option<bool>` where overwriting the entire
32//! value is acceptable.
33//!
34//! [`ApplyBoolLayerExt::apply_bool_layer`][] exists to apply [`BoolOr`][] wrappers
35//! which lets config say things like `homebrew = false` when `HomebrewInstallerConfig`
36//! is actually an entire struct.
37//!
38//!
39//! # The ACTUAL Situation
40//!
41//! Here's how things are different from the original ideal design.
42//!
43//! ## Two Different Output Config Types
44//!
45//! Because we wanted to structurally distinguish "global" and "package-specific" configs
46//! we ended up with two kinds of Config type: `Workspace...Config` and `App...Config`.
47//! For instance [`WorkspaceInstallerConfig`][] and [`AppInstallerConfig`][] both exist.
48//! Sometimes only one exists, because the struct would have no fields (because e.g. all the
49//! relevant subconfig is all global).
50//!
51//!
52//! ## Common Fields
53//!
54//! Some "common" fields are defined to be shared to several related things like
55//! e.g. `ShellInstallerConfig` and `PowershellInstallerConfig`.
56//!
57//! These common fields are defined in `...Common` types. For instance [`CommonInstallerConfig`][]
58//! and [`CommonInstallerLayer`][] specify the shared fields for all installers.
59//!
60//! Notably, as a convenience sugar, these common fields can be specified in the parent
61//! struct and will be automatically "folded" into the subtypes. So you can set
62//! `installers.success_msg = "hello"`` and this will be inherited by
63//! `installers.powershell.success_msg` and `installers.shell.success_msg` and so on.
64//!
65//! In addition to being a sugar it also gives a forward-compat path for making a config
66//! more granular in the future without breaking existing configs. So if success_msg
67//! could only be set once for all installers, we could make it "common" *later*
68//! to allow anyone to customize it without breaking any config from before then.
69//!
70//! ...HOWEVER...
71//!
72//! This is a huge thorn in the whole idea of starting with our final config with Default
73//! and then folding in layers over time.
74//!
75//! This is because the "common" fields need to exist in the Layer types and be preserved
76//! as we fold in all the fields *BUT* we want them to go away in the final Config types
77//! because we want a single source of truth (we don't want code to forget to consult
78//! the inheritance chain). So a bunch of places grew `...ConfigInheritable` types that
79//! represent a hybrid between Config and Layer where the fields are ostensibly final
80//! but the "common" fields are still not folded in.
81//!
82//! See for example [`InstallerConfigInheritable`][]. We actually construct *THESE*
83//! instead of the `...Config` types, and then "finish" them with the apply_inheritance
84//! methods.
85//!
86//!
87//! ## Future Work
88//!
89//! The above situation is suboptimal and I believe the `...ConfigInheritable` types
90//! are a mistake. We should instead just use the `...Layer` types as that type, and
91//! apply defaults *at the end* instead of *at the start*.
92//!
93//! If you see a bunch of default code that doesn't make much sense, that's because
94//! this refactor should be done because it really doesn't make sense.
95//!
96//! This is in theory not *complex* work, but it is *a bunch* of work.
97//!
98//! It's possible it would be more worthwhile to put the effort into a derive macro
99//! that automates a bunch of this stuff if you're having to rejig a ton of the code/types
100//! anyway.
101
102// We very intentionally manually implement Default a lot in this submodule
103// to keep things very explicit and clear
104#![allow(clippy::derivable_impls)]
105
106pub mod layer;
107
108pub mod artifacts;
109pub mod builds;
110pub mod ci;
111pub mod hosts;
112pub mod installers;
113pub mod publishers;
114
115use axoproject::{PackageIdx, WorkspaceGraph};
116use semver::Version;
117use v0::CargoDistUrlOverride;
118
119use super::*;
120use layer::*;
121
122use artifacts::*;
123use builds::*;
124use ci::*;
125use hosts::*;
126use installers::*;
127use publishers::*;
128
129/// Compute the workspace-level config
130pub fn workspace_config(
131 workspaces: &WorkspaceGraph,
132 mut global_config: TomlLayer,
133) -> WorkspaceConfig {
134 // Rewrite config-file-relative paths
135 global_config.make_relative_to(&workspaces.root_workspace().workspace_dir);
136
137 let mut config = WorkspaceConfigInheritable::defaults_for_workspace(workspaces);
138 config.apply_layer(global_config);
139 config.apply_inheritance_for_workspace(workspaces)
140}
141
142/// Compute the package-level config
143pub fn app_config(
144 workspaces: &WorkspaceGraph,
145 pkg_idx: PackageIdx,
146 mut global_config: TomlLayer,
147 mut local_config: TomlLayer,
148) -> AppConfig {
149 // Rewrite config-file-relative paths
150 let package = workspaces.package(pkg_idx);
151 global_config.make_relative_to(&workspaces.root_workspace().workspace_dir);
152 local_config.make_relative_to(&package.package_root);
153
154 let mut config = AppConfigInheritable::defaults_for_package(workspaces, pkg_idx);
155 config.apply_layer(global_config);
156 config.apply_layer(local_config);
157 config.apply_inheritance_for_package(workspaces, pkg_idx)
158}
159
160/// config that is global to the entire workspace
161#[derive(Debug, Clone)]
162pub struct WorkspaceConfig {
163 /// The intended version of dist to build with. (normal Cargo SemVer syntax)
164 pub dist_version: Option<Version>,
165 /// See [`CargoDistUrlOverride`]
166 pub dist_url_override: Option<CargoDistUrlOverride>,
167 /// Generate targets whose dist should avoid checking for up-to-dateness.
168 pub allow_dirty: Vec<GenerateMode>,
169 /// ci config
170 pub ci: CiConfig,
171 /// artifact config
172 pub artifacts: WorkspaceArtifactConfig,
173 /// host config
174 pub hosts: WorkspaceHostConfig,
175 /// build config
176 pub builds: WorkspaceBuildConfig,
177 /// installer config
178 pub installers: WorkspaceInstallerConfig,
179}
180/// config that is global to the entire workspace
181///
182/// but inheritance relationships haven't been folded in yet.
183#[derive(Debug, Clone)]
184pub struct WorkspaceConfigInheritable {
185 /// The intended version of dist to build with. (normal Cargo SemVer syntax)
186 pub dist_version: Option<Version>,
187 /// See [`CargoDistUrlOverride`]
188 pub dist_url_override: Option<CargoDistUrlOverride>,
189 /// Generate targets whose dist should avoid checking for up-to-dateness.
190 pub allow_dirty: Vec<GenerateMode>,
191 /// artifact config
192 pub artifacts: WorkspaceArtifactConfig,
193 /// ci config
194 pub ci: CiConfigInheritable,
195 /// host config
196 pub hosts: HostConfigInheritable,
197 /// build config
198 pub builds: BuildConfigInheritable,
199 /// installer config
200 pub installers: InstallerConfigInheritable,
201}
202impl WorkspaceConfigInheritable {
203 /// Get the defaults for workspace-level config
204 pub fn defaults_for_workspace(workspaces: &WorkspaceGraph) -> Self {
205 Self {
206 artifacts: WorkspaceArtifactConfig::defaults_for_workspace(workspaces),
207 ci: CiConfigInheritable::defaults_for_workspace(workspaces),
208 hosts: HostConfigInheritable::defaults_for_workspace(workspaces),
209 builds: BuildConfigInheritable::defaults_for_workspace(workspaces),
210 installers: InstallerConfigInheritable::defaults_for_workspace(workspaces),
211 dist_version: None,
212 dist_url_override: None,
213 allow_dirty: vec![],
214 }
215 }
216 /// Apply the inheritance to get the final WorkspaceConfig
217 pub fn apply_inheritance_for_workspace(self, workspaces: &WorkspaceGraph) -> WorkspaceConfig {
218 let Self {
219 artifacts,
220 ci,
221 hosts,
222 builds,
223 installers,
224 dist_version,
225 dist_url_override,
226 allow_dirty,
227 } = self;
228 WorkspaceConfig {
229 artifacts,
230 ci: ci.apply_inheritance_for_workspace(workspaces),
231 hosts: hosts.apply_inheritance_for_workspace(workspaces),
232 builds: builds.apply_inheritance_for_workspace(workspaces),
233 installers: installers.apply_inheritance_for_workspace(workspaces),
234 dist_version,
235 dist_url_override,
236 allow_dirty,
237 }
238 }
239}
240impl ApplyLayer for WorkspaceConfigInheritable {
241 type Layer = TomlLayer;
242 fn apply_layer(
243 &mut self,
244 Self::Layer {
245 artifacts,
246 builds,
247 hosts,
248 installers,
249 ci,
250 allow_dirty,
251 dist_version,
252 dist_url_override,
253 // app-scope only
254 dist: _,
255 targets: _,
256 publishers: _,
257 }: Self::Layer,
258 ) {
259 self.artifacts.apply_val_layer(artifacts);
260 self.builds.apply_val_layer(builds);
261 self.hosts.apply_val_layer(hosts);
262 self.installers.apply_val_layer(installers);
263 self.ci.apply_val_layer(ci);
264 self.dist_version.apply_opt(dist_version);
265 self.dist_url_override.apply_opt(dist_url_override);
266 self.allow_dirty.apply_val(allow_dirty);
267 }
268}
269
270/// Config scoped to a particular App
271#[derive(Debug, Clone)]
272pub struct AppConfig {
273 /// artifact config
274 pub artifacts: AppArtifactConfig,
275 /// build config
276 pub builds: AppBuildConfig,
277 /// host config
278 pub hosts: AppHostConfig,
279 /// installer config
280 pub installers: AppInstallerConfig,
281 /// publisher config
282 pub publishers: PublisherConfig,
283 /// Whether the package should be distributed/built by dist
284 pub dist: Option<bool>,
285 /// The full set of target triples to build for.
286 pub targets: Vec<TripleName>,
287}
288/// Config scoped to a particular App
289///
290/// but inheritance relationships haven't been folded in yet.
291#[derive(Debug, Clone)]
292pub struct AppConfigInheritable {
293 /// artifact config
294 pub artifacts: AppArtifactConfig,
295 /// build config
296 pub builds: BuildConfigInheritable,
297 /// host config
298 pub hosts: HostConfigInheritable,
299 /// installer config
300 pub installers: InstallerConfigInheritable,
301 /// publisher config
302 pub publishers: PublisherConfigInheritable,
303 /// Whether the package should be distributed/built by dist
304 pub dist: Option<bool>,
305 /// The full set of target triples to build for.
306 pub targets: Vec<TripleName>,
307}
308impl AppConfigInheritable {
309 /// Get the defaults for the given package
310 pub fn defaults_for_package(workspaces: &WorkspaceGraph, pkg_idx: PackageIdx) -> Self {
311 Self {
312 artifacts: AppArtifactConfig::defaults_for_package(workspaces, pkg_idx),
313 builds: BuildConfigInheritable::defaults_for_package(workspaces, pkg_idx),
314 hosts: HostConfigInheritable::defaults_for_package(workspaces, pkg_idx),
315 installers: InstallerConfigInheritable::defaults_for_package(workspaces, pkg_idx),
316 publishers: PublisherConfigInheritable::defaults_for_package(workspaces, pkg_idx),
317 dist: None,
318 targets: vec![],
319 }
320 }
321 /// Fold in inheritance relationships to get the final package config
322 pub fn apply_inheritance_for_package(
323 self,
324 workspaces: &WorkspaceGraph,
325 pkg_idx: PackageIdx,
326 ) -> AppConfig {
327 let Self {
328 artifacts,
329 builds,
330 hosts,
331 installers,
332 publishers,
333 dist: do_dist,
334 targets,
335 } = self;
336 AppConfig {
337 artifacts,
338 builds: builds.apply_inheritance_for_package(workspaces, pkg_idx),
339 hosts: hosts.apply_inheritance_for_package(workspaces, pkg_idx),
340 installers: installers.apply_inheritance_for_package(workspaces, pkg_idx),
341 publishers: publishers.apply_inheritance_for_package(workspaces, pkg_idx),
342 dist: do_dist,
343 targets,
344 }
345 }
346}
347impl ApplyLayer for AppConfigInheritable {
348 type Layer = TomlLayer;
349 fn apply_layer(
350 &mut self,
351 Self::Layer {
352 artifacts,
353 builds,
354 hosts,
355 installers,
356 publishers,
357 dist,
358 targets,
359 // workspace-scope only
360 ci: _,
361 allow_dirty: _,
362 dist_version: _,
363 dist_url_override: _,
364 }: Self::Layer,
365 ) {
366 self.artifacts.apply_val_layer(artifacts);
367 self.builds.apply_val_layer(builds);
368 self.hosts.apply_val_layer(hosts);
369 self.installers.apply_val_layer(installers);
370 self.publishers.apply_val_layer(publishers);
371 self.dist.apply_opt(dist);
372 self.targets.apply_val(targets);
373 }
374}
375
376/// The "raw" input from a toml file containing config
377#[derive(Debug, Clone, Serialize, Deserialize)]
378#[serde(rename_all = "kebab-case")]
379pub struct TomlLayer {
380 /// The intended version of dist to build with. (normal Cargo SemVer syntax)
381 ///
382 /// When generating full tasks graphs (such as CI scripts) we will pick this version.
383 ///
384 /// FIXME: Should we produce a warning if running locally with a different version? In theory
385 /// it shouldn't be a problem and newer versions should just be Better... probably you
386 /// Really want to have the exact version when running generate to avoid generating
387 /// things other dist versions can't handle!
388 #[serde(skip_serializing_if = "Option::is_none")]
389 pub dist_version: Option<Version>,
390
391 /// see [`CargoDistUrlOverride`]
392 #[serde(skip_serializing_if = "Option::is_none")]
393 pub dist_url_override: Option<CargoDistUrlOverride>,
394
395 /// Whether the package should be distributed/built by dist
396 ///
397 /// This mainly exists to be set to `false` to make dist ignore the existence of this
398 /// package. Note that we may still build the package as a side-effect of building the
399 /// workspace -- we just won't bundle it up and report it.
400 ///
401 /// FIXME: maybe you should also be allowed to make this a list of binary names..?
402 #[serde(skip_serializing_if = "Option::is_none")]
403 pub dist: Option<bool>,
404
405 /// Generate targets whose dist should avoid checking for up-to-dateness.
406 #[serde(skip_serializing_if = "Option::is_none")]
407 pub allow_dirty: Option<Vec<GenerateMode>>,
408
409 /// The full set of target triples to build for.
410 ///
411 /// When generating full task graphs (such as CI scripts) we will to try to generate these.
412 ///
413 /// The inputs should be valid rustc target triples (see `rustc --print target-list`) such
414 /// as `x86_64-pc-windows-msvc`, `aarch64-apple-darwin`, or `x86_64-unknown-linux-gnu`.
415 ///
416 /// FIXME: We should also accept one magic target: `universal2-apple-darwin`. This will induce
417 /// us to build `x86_64-apple-darwin` and `aarch64-apple-darwin` (arm64) and then combine
418 /// them into a "universal" binary that can run on either arch (using apple's `lipo` tool).
419 ///
420 /// FIXME: Allow higher level requests like "[macos, windows, linux] x [x86_64, aarch64]"?
421 #[serde(skip_serializing_if = "Option::is_none")]
422 pub targets: Option<Vec<TripleName>>,
423
424 /// artifact config
425 #[serde(skip_serializing_if = "Option::is_none")]
426 pub artifacts: Option<ArtifactLayer>,
427 /// build config
428 #[serde(skip_serializing_if = "Option::is_none")]
429 pub builds: Option<BuildLayer>,
430 /// ci config
431 #[serde(skip_serializing_if = "Option::is_none")]
432 pub ci: Option<CiLayer>,
433 /// host config
434 #[serde(skip_serializing_if = "Option::is_none")]
435 pub hosts: Option<HostLayer>,
436 /// installer config
437 #[serde(skip_serializing_if = "Option::is_none")]
438 pub installers: Option<InstallerLayer>,
439 /// publisher config
440 #[serde(skip_serializing_if = "Option::is_none")]
441 pub publishers: Option<PublisherLayer>,
442}
443
444impl TomlLayer {
445 /// Take any configs that contain paths that are *relative to the file they came from*
446 /// and make them relative to the given basepath.
447 ///
448 /// This is important to do eagerly, because once we start merging configs
449 /// we'll forget what file they came from!
450 fn make_relative_to(&mut self, base_path: &Utf8Path) {
451 // It's kind of unfortunate that we don't exhaustively match this to
452 // force you to update it BUT almost no config is ever applicable for
453 // this so even when we used to, everyone just skimmed over this so
454 // whatever just Get Good and remember this transform is necessary
455 // if you every add another config-file-relative path to the config
456 if let Some(artifacts) = &mut self.artifacts {
457 if let Some(archives) = &mut artifacts.archives {
458 if let Some(include) = &mut archives.include {
459 for path in include {
460 make_path_relative_to(path, base_path);
461 }
462 }
463 }
464 if let Some(extras) = &mut artifacts.extra {
465 for extra in extras {
466 make_path_relative_to(&mut extra.working_dir, base_path);
467 }
468 }
469 }
470 if let Some(hosts) = &mut self.hosts {
471 if let Some(BoolOr::Val(github)) = &mut hosts.github {
472 if let Some(path) = &mut github.submodule_path {
473 make_path_relative_to(path, base_path);
474 }
475 }
476 }
477 }
478}
479
480fn make_path_relative_to(path: &mut Utf8PathBuf, base_path: &Utf8Path) {
481 // FIXME: should absolute paths be a hard error? Or should we force them relative?
482 if !path.is_absolute() {
483 *path = base_path.join(&path);
484 }
485}