1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#![deny(clippy::all)]
#![warn(clippy::nursery, clippy::pedantic)]

use std::env;

use anyhow::Result;

mod commands;
mod coverage;
mod dev;
mod dist;
mod fixup;
mod utils;

const HELP: &str = "\
cargo xtask - helper scripts for running common project tasks

USAGE:
    cargo xtask [OPTIONS] [TASK]...

OPTIONS:
    --cross                Uses cross for cross-compilation instead of cargo
    -i, --ignore-missing   Ignores any missing tools; only warns during fixup
    --target <TRIPLE>      Sets the target triple that dist will build
    -h, --help             Prints help information

TASKS:
    check                  Watch for file changes and auto-trigger clippy linting
    coverage               Generate and print a code coverage report summary
    coverage.html          Generate and open an HTML code coverage report
    dist                   Package project assets into distributable artifacts
    doc                    Watch for file changes and auto-trigger doc generation
    fixup                  Run all fixup xtasks, editing files in-place
    fixup.github-actions   Lint CI YAML files using actionlint static checker
    fixup.markdown         Format Markdown files in-place
    fixup.rust             Fix lints and format Rust files in-place
    fixup.spelling         Fix common misspellings across all files in-place
    install                Install required Rust components and Cargo dependencies
    test                   Run all tests/doctests and generate Insta snapshots
";

enum Task {
    Check,
    Coverage,
    CoverageHtml,
    Dist,
    Doc,
    Fixup,
    FixupGithubActions,
    FixupMarkdown,
    FixupRust,
    FixupSpelling,
    Install,
    Test,
}

pub struct Config {
    cross: bool,
    run_tasks: Vec<Task>,
    ignore_missing_commands: bool,
    target: Option<String>,
}

fn main() -> Result<()> {
    // print help when no arguments are given
    if env::args().len() == 1 {
        print!("{HELP}");
        std::process::exit(1);
    }

    let config = parse_args()?;
    for task in &config.run_tasks {
        match task {
            Task::Check => dev::watch_clippy(&config)?,
            Task::Coverage => coverage::report_summary(&config)?,
            Task::CoverageHtml => coverage::html_report(&config)?,
            Task::Dist => dist::dist(&config)?,
            Task::Doc => dev::watch_doc(&config)?,
            Task::Fixup => fixup::everything(&config)?,
            Task::FixupGithubActions => fixup::github_actions(&config)?,
            Task::FixupMarkdown => fixup::markdown(&config)?,
            Task::FixupRust => fixup::rust(&config)?,
            Task::FixupSpelling => fixup::spelling(&config)?,
            Task::Install => dev::install_rust_deps(&config)?,
            Task::Test => dev::test_with_snapshots(&config)?,
        }
    }

    Ok(())
}

fn parse_args() -> Result<Config> {
    use lexopt::prelude::*;

    // default config values
    let mut cross = false;
    let mut ignore_missing_commands = false;
    let mut target: Option<String> = None;
    let mut run_tasks = Vec::new();

    let mut parser = lexopt::Parser::from_env();
    while let Some(arg) = parser.next()? {
        match arg {
            Short('h') | Long("help") => {
                print!("{HELP}");
                std::process::exit(0);
            }
            Long("cross") => {
                cross = true;
            }
            Short('i') | Long("ignore-missing") => {
                ignore_missing_commands = true;
            }
            Long("target") => {
                target = Some(parser.value()?.string()?);
            }
            Value(value) => {
                let value = value.string()?;
                let task = match value.as_str() {
                    "check" => Task::Check,
                    "coverage" => Task::Coverage,
                    "coverage.html" => Task::CoverageHtml,
                    "dist" => Task::Dist,
                    "doc" => Task::Doc,
                    "fixup" => Task::Fixup,
                    "fixup.github-actions" => Task::FixupGithubActions,
                    "fixup.markdown" => Task::FixupMarkdown,
                    "fixup.rust" => Task::FixupRust,
                    "fixup.spelling" => Task::FixupSpelling,
                    "install" => Task::Install,
                    "test" => Task::Test,
                    value => {
                        anyhow::bail!("unknown task '{}'", value);
                    }
                };
                run_tasks.push(task);
            }
            _ => anyhow::bail!(arg.unexpected()),
        }
    }

    if run_tasks.is_empty() {
        anyhow::bail!("no task given");
    }

    Ok(Config {
        cross,
        run_tasks,
        ignore_missing_commands,
        target,
    })
}