Skip to main content

origin_xtask/
lib.rs

1//! Origin maintenance tasks, as a library.
2//!
3//! A derivative's `xtask` is three lines:
4//!
5//! ```ignore
6//! fn main() -> std::process::ExitCode {
7//!     origin_xtask::main()
8//! }
9//! ```
10//!
11//! That is the point. Architecture rules, the generator and the CI recipe arrive with
12//! a version bump instead of being copied into each project and drifting apart —
13//! a new rule lands in every derivative and fails its CI the same day (see the update
14//! system plan, category C).
15
16mod contracts;
17mod generate;
18mod migrations;
19mod scaffold;
20mod update;
21mod validate;
22mod version;
23
24pub use generate::{check as check_generated, run as generate};
25pub use scaffold::{Options as NewOptions, run as new};
26pub use update::run as update;
27pub use version::Version;
28
29pub(crate) use generate::find_manifests;
30pub use validate::run as validate;
31
32use std::path::{Path, PathBuf};
33use std::process::{Command, ExitCode};
34
35/// Dispatch a task from the process arguments.
36pub fn main() -> ExitCode {
37    let task = std::env::args().nth(1);
38    let flags: Vec<String> = std::env::args().skip(2).collect();
39
40    let result = match task.as_deref() {
41        Some("validate") => validate(&workspace_root()),
42        Some("generate") if flags.iter().any(|flag| flag == "--check") => {
43            check_generated(&workspace_root())
44        }
45        Some("generate") => generate(&workspace_root()),
46        Some("update") => update(
47            &workspace_root(),
48            flags.iter().any(|flag| flag == "--dry-run"),
49        ),
50        Some("new") => new_from_args(&flags),
51        Some("ci") => ci(),
52        Some("demo") => demo(),
53        Some(other) => {
54            eprintln!("unknown task `{other}`");
55            usage();
56            return ExitCode::FAILURE;
57        }
58        None => {
59            usage();
60            return ExitCode::FAILURE;
61        }
62    };
63
64    match result {
65        Ok(()) => ExitCode::SUCCESS,
66        Err(message) => {
67            eprintln!("\nxtask failed: {message}");
68            ExitCode::FAILURE
69        }
70    }
71}
72
73fn usage() {
74    eprintln!(
75        "\nusage: cargo xtask <task>\n\n\
76         tasks:\n  \
77           validate           check the rules in ARCHITECTURE.md\n  \
78           generate           write the files derived from app.toml\n  \
79           generate --check   fail if generated files are stale or hand-edited\n  \
80           update             run pending migrations and regenerate\n  \
81           update --dry-run   report what an update would change\n  \
82           new <slug>         create a new application from the template\n  \
83           ci                 fmt --check, clippy -D warnings, test, generate --check, validate\n  \
84           demo               run the reference application\n"
85    );
86}
87
88/// Parse `new <slug> [--name X] [--id Y] [--into DIR] [--local]`.
89fn new_from_args(flags: &[String]) -> Result<(), String> {
90    let root = workspace_root();
91
92    let slug = flags
93        .first()
94        .filter(|argument| !argument.starts_with("--"))
95        .ok_or_else(|| {
96            "usage: cargo xtask new <slug> [--name X] [--id Y] [--into DIR] [--local]".to_owned()
97        })?
98        .clone();
99
100    let value = |name: &str| -> Option<String> {
101        flags
102            .iter()
103            .position(|flag| flag == name)
104            .and_then(|index| flags.get(index + 1))
105            .cloned()
106    };
107
108    let options = scaffold::Options {
109        name: value("--name").unwrap_or_else(|| title_case(&slug)),
110        id: value("--id").unwrap_or_else(|| format!("dev.local.{}", slug.replace('-', ""))),
111        into: value("--into")
112            .map(PathBuf::from)
113            .unwrap_or_else(|| root.clone()),
114        // Released Origin packages are the normal dependency source. `--local` is
115        // reserved for Origin's downstream CI and side-by-side platform development.
116        local: flags
117            .iter()
118            .any(|flag| flag == "--local")
119            .then(|| root.clone()),
120        slug,
121    };
122
123    scaffold::run(&root, &options)
124}
125
126/// `my-app` → `My App`.
127fn title_case(slug: &str) -> String {
128    slug.split(['-', '_'])
129        .map(|word| {
130            let mut characters = word.chars();
131            match characters.next() {
132                Some(first) => first.to_uppercase().collect::<String>() + characters.as_str(),
133                None => String::new(),
134            }
135        })
136        .collect::<Vec<_>>()
137        .join(" ")
138}
139
140fn ci() -> Result<(), String> {
141    let root = workspace_root();
142
143    cargo(&["fmt", "--all", "--check"])?;
144    cargo(&[
145        "clippy",
146        "--workspace",
147        "--all-targets",
148        "--",
149        "-D",
150        "warnings",
151    ])?;
152    cargo(&["test", "--workspace"])?;
153    check_generated(&root)?;
154    validate(&root)
155}
156
157fn demo() -> Result<(), String> {
158    run("pnpm", &["--filter", "@origin/demo", "tauri", "dev"])
159}
160
161fn cargo(args: &[&str]) -> Result<(), String> {
162    run("cargo", args)
163}
164
165fn run(program: &str, args: &[&str]) -> Result<(), String> {
166    println!("\n$ {program} {}", args.join(" "));
167
168    let status = Command::new(program)
169        .args(args)
170        .current_dir(workspace_root())
171        .status()
172        .map_err(|error| format!("cannot run {program}: {error}"))?;
173
174    if status.success() {
175        Ok(())
176    } else {
177        Err(format!("{program} {} failed", args.join(" ")))
178    }
179}
180
181/// The repository root.
182///
183/// Discovered by walking up from the current directory rather than taken from
184/// `CARGO_MANIFEST_DIR`: as a library, this crate's manifest directory is somewhere in
185/// the Cargo registry, not in the project being worked on.
186pub fn workspace_root() -> PathBuf {
187    let start = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
188
189    for directory in start.ancestors() {
190        if is_workspace_root(directory) {
191            return directory.to_path_buf();
192        }
193    }
194
195    start
196}
197
198fn is_workspace_root(directory: &Path) -> bool {
199    let manifest = directory.join("Cargo.toml");
200    let Ok(contents) = std::fs::read_to_string(&manifest) else {
201        return false;
202    };
203
204    contents
205        .lines()
206        .any(|line| line.trim_start().starts_with("[workspace]"))
207}