Skip to main content

origin_xtask/
update.rs

1//! `cargo xtask update` — bring a project up to this Origin version (ADR-0025).
2//!
3//! The point of the whole exercise: a platform improvement reaches four derivatives as
4//! four command runs, not four afternoons.
5
6use crate::migrations::{self, Context, Steps};
7use crate::version::Version;
8use crate::{find_manifests, generate};
9use origin_manifest::Manifest;
10use std::path::Path;
11
12/// Run every pending migration, regenerate, and record the new version.
13pub fn run(root: &Path, dry_run: bool) -> Result<(), String> {
14    let manifests = find_manifests(root)?;
15    if manifests.is_empty() {
16        return Err(format!("no app.toml found under {}", root.display()));
17    }
18
19    for manifest_path in manifests {
20        let manifest = Manifest::load(&manifest_path).map_err(|error| error.to_string())?;
21        let project = manifest_path.parent().unwrap_or(root);
22        let from = Version::parse(&manifest.origin.version)?;
23
24        println!("\n{}", manifest_path.display());
25        println!("  origin {from} → {}", migrations::CURRENT);
26
27        if from > migrations::CURRENT {
28            return Err(format!(
29                "{} tracks origin {from}, but this build only knows up to {}. \
30                 Upgrade the origin dependency instead of downgrading the project.",
31                manifest_path.display(),
32                migrations::CURRENT
33            ));
34        }
35
36        if from == migrations::CURRENT {
37            println!("  already up to date");
38            continue;
39        }
40
41        let context = Context {
42            project,
43            manifest: &manifest,
44            dry_run,
45        };
46        let steps = migrations::apply(&context, from)?;
47
48        if !dry_run {
49            // Regeneration comes last: a migration may have changed what the manifest
50            // describes, and generated files must reflect the final state.
51            generate::run(root)?;
52            set_version(&manifest_path, migrations::CURRENT)?;
53        }
54
55        report(&steps, dry_run);
56    }
57
58    Ok(())
59}
60
61fn report(steps: &Steps, dry_run: bool) {
62    if dry_run {
63        println!("\n  dry run — nothing was written");
64    }
65
66    for changed in &steps.changed {
67        println!("  ✓ {changed}");
68    }
69
70    for skipped in &steps.skipped {
71        println!("  – skipped: {skipped}");
72    }
73
74    if !steps.manual.is_empty() {
75        println!("\n  Manual steps required:");
76        for manual in &steps.manual {
77            println!("  → {manual}");
78        }
79    }
80}
81
82/// Write the new version into `app.toml`.
83///
84/// Format-preserving: the manifest is a file a human wrote and will read again, so
85/// comments and layout survive.
86fn set_version(manifest_path: &Path, version: Version) -> Result<(), String> {
87    let contents = std::fs::read_to_string(manifest_path)
88        .map_err(|error| format!("cannot read {}: {error}", manifest_path.display()))?;
89
90    let mut document: toml_edit::DocumentMut = contents
91        .parse()
92        .map_err(|error| format!("{} is not valid TOML: {error}", manifest_path.display()))?;
93
94    document["origin"]["version"] = toml_edit::value(version.to_string());
95
96    std::fs::write(manifest_path, document.to_string())
97        .map_err(|error| format!("cannot write {}: {error}", manifest_path.display()))
98}