Skip to main content

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}