1mod config;
2mod context;
3mod dev;
4mod filesystem;
5mod framework;
6mod package;
7mod workflow;
8
9use std::path::PathBuf;
10
11use context::Context;
12
13use anyhow::Context as _;
14use clap::{Parser, Subcommand};
15
16#[derive(Parser)]
17#[command(
18 name = "tuff",
19 version,
20 about = "Build and package Tuffite desktop applications"
21)]
22pub struct Cli {
23 #[arg(short = 'v', action = clap::ArgAction::Count, global = true)]
25 verbose: u8,
26 #[command(subcommand)]
27 command: Command,
28}
29
30#[derive(Debug, Subcommand)]
31enum Command {
32 Init {
34 directory: PathBuf,
36 #[arg(long)]
38 name: Option<String>,
39 },
40 Check {
42 #[command(flatten)]
43 manifest: config::ManifestOptions,
44 },
45 Build {
47 #[command(flatten)]
48 manifest: config::ManifestOptions,
49 #[arg(long)]
51 release: bool,
52 },
53 Doctor {
55 #[command(flatten)]
56 manifest: config::ManifestOptions,
57 },
58 Dev(dev::Options),
60 Package(package::Options),
62 Framework(framework::Options),
64}
65
66pub fn main() {
67 if let Err(error) = Cli::parse().run() {
68 tracing::error!(error = %format_args!("{error:#}"), "command failed");
69 std::process::exit(2);
70 }
71}
72
73impl Cli {
74 pub fn run(self) -> anyhow::Result<()> {
75 Self::init_tracing(self.verbose);
76 tracing::debug!(command = ?self.command, "running Tuffite CLI command");
77 let mut context = Context::new(matches!(
78 &self.command,
79 Command::Dev(options) if options.force_download_framework
80 ));
81 match self.command {
82 Command::Init { directory, name } => workflow::init(&directory, name.as_deref())
83 .map_err(anyhow::Error::msg)
84 .with_context(|| format!("initialize {}", directory.display())),
85 Command::Check { manifest } => {
86 let manifest = manifest.resolve().map_err(anyhow::Error::msg)?;
87 config::check(&manifest)
88 .map_err(anyhow::Error::msg)
89 .with_context(|| format!("check {}", manifest.display()))?;
90 tracing::info!(manifest = %manifest.display(), "configuration is valid");
91 Ok(())
92 }
93 Command::Build { manifest, release } => {
94 let manifest = manifest.resolve().map_err(anyhow::Error::msg)?;
95 workflow::build(&mut context, &manifest, release)
96 .map_err(anyhow::Error::msg)
97 .with_context(|| format!("build {}", manifest.display()))
98 }
99 Command::Doctor { manifest } => {
100 let manifest = manifest.resolve().map_err(anyhow::Error::msg)?;
101 workflow::doctor(&mut context, &manifest)
102 .map_err(anyhow::Error::msg)
103 .with_context(|| format!("diagnose {}", manifest.display()))
104 }
105 Command::Dev(options) => options
106 .run(&mut context)
107 .map_err(anyhow::Error::msg)
108 .context("launch development application"),
109 Command::Package(options) => options
110 .run(&mut context)
111 .map_err(anyhow::Error::msg)
112 .context("package application"),
113 Command::Framework(options) => framework::run(&options)
114 .map_err(anyhow::Error::msg)
115 .context("run framework command"),
116 }
117 }
118
119 fn init_tracing(verbosity: u8) {
120 let filter = if verbosity == 0 {
121 tracing_subscriber::EnvFilter::try_from_default_env()
122 .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"))
123 } else {
124 tracing_subscriber::EnvFilter::new(if verbosity == 1 { "debug" } else { "trace" })
125 };
126 let builder = tracing_subscriber::fmt()
127 .with_env_filter(filter)
128 .with_ansi(false)
129 .with_writer(std::io::stderr);
130 let _ = if verbosity >= 3 {
131 builder
132 .with_target(true)
133 .with_file(true)
134 .with_line_number(true)
135 .with_thread_ids(true)
136 .try_init()
137 } else {
138 builder.with_target(false).try_init()
139 };
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use clap::Parser as _;
146
147 use super::{Cli, Command};
148
149 #[test]
150 fn verbosity_is_global_and_counted() {
151 assert_eq!(
152 Cli::try_parse_from(["tuff", "-v", "check"])
153 .unwrap()
154 .verbose,
155 1
156 );
157 assert_eq!(
158 Cli::try_parse_from(["tuff", "check", "-vv"])
159 .unwrap()
160 .verbose,
161 2
162 );
163 assert_eq!(
164 Cli::try_parse_from(["tuff", "-vvv", "doctor"])
165 .unwrap()
166 .verbose,
167 3
168 );
169 }
170
171 #[test]
172 fn bindings_and_downloads_have_no_standalone_commands() {
173 for arguments in [
174 vec!["tuff", "generate"],
175 vec!["tuff", "download"],
176 vec!["tuff", "framework", "download"],
177 ] {
178 assert!(Cli::try_parse_from(arguments).is_err());
179 }
180 assert!(Cli::try_parse_from(["tuff", "dev", "--prepare-only"]).is_ok());
181 }
182
183 #[test]
184 fn dev_accepts_forced_framework_download() {
185 let cli = Cli::try_parse_from(["tuff", "dev", "--force-download-framework"]).unwrap();
186 let Command::Dev(options) = cli.command else {
187 panic!("expected dev command");
188 };
189 assert!(options.force_download_framework);
190 }
191
192 #[test]
193 fn application_commands_accept_discovery_and_manifest_overrides() {
194 for command in ["check", "build", "doctor", "dev", "package"] {
195 assert!(Cli::try_parse_from(["tuff", command]).is_ok());
196 assert!(
197 Cli::try_parse_from(["tuff", command, "--manifest", "project/tuffite.json"])
198 .is_ok()
199 );
200 }
201 assert!(Cli::try_parse_from(["tuff", "package", "--format", "app"]).is_ok());
202 for command in ["check", "build", "doctor"] {
203 assert!(Cli::try_parse_from(["tuff", command, "project/tuffite.json"]).is_ok());
204 assert!(
205 Cli::try_parse_from([
206 "tuff",
207 command,
208 "project/tuffite.json",
209 "--manifest",
210 "other/tuffite.json"
211 ])
212 .is_err()
213 );
214 }
215 }
216}