1pub mod build;
5pub mod check;
6pub mod component;
7pub mod guide;
8pub mod install;
9pub mod pack;
10pub mod project;
11pub mod scaffold;
12pub mod world;
13
14use std::ffi::OsString;
15use std::path::{Path, PathBuf};
16use std::process::ExitCode;
17
18use clap::{Parser, Subcommand};
19
20use crate::project::Plugin;
21use crate::scaffold::{NewKind, SdkSource};
22
23#[derive(Debug, Parser)]
24#[command(name = "standard-plugin", version, about)]
25struct Cli {
26 #[command(subcommand)]
27 command: Command,
28}
29
30#[derive(Debug, Subcommand)]
31enum Command {
32 New {
34 name: String,
36 #[arg(long, value_enum, default_value = "ui")]
37 kind: NewKind,
38 #[arg(long, default_value = ".")]
40 dir: PathBuf,
41 #[arg(long)]
43 sdk_path: Option<PathBuf>,
44 },
45 Build {
47 #[arg(long, default_value = ".")]
48 dir: PathBuf,
49 #[arg(long)]
51 debug: bool,
52 },
53 Check {
55 #[arg(long, default_value = ".")]
56 dir: PathBuf,
57 #[arg(long)]
59 deny_warnings: bool,
60 },
61 Pack {
64 #[arg(long, default_value = ".")]
65 dir: PathBuf,
66 #[arg(long)]
69 out: Option<PathBuf>,
70 },
71 Install {
74 #[arg(long, default_value = ".")]
75 dir: PathBuf,
76 },
77 Guide {
80 name: Option<String>,
83 #[arg(long, default_value = ".")]
84 dir: PathBuf,
85 },
86}
87
88pub fn run(args: impl IntoIterator<Item = OsString>) -> ExitCode {
90 let cli = match Cli::try_parse_from(args) {
91 Ok(cli) => cli,
92 Err(error) => {
93 let _ = error.print();
94 return if error.use_stderr() {
95 ExitCode::from(2)
96 } else {
97 ExitCode::SUCCESS
98 };
99 }
100 };
101 match execute(cli.command) {
102 Ok(true) => ExitCode::SUCCESS,
103 Ok(false) => ExitCode::FAILURE,
104 Err(error) => {
105 eprintln!("error: {error}");
106 ExitCode::FAILURE
107 }
108 }
109}
110
111fn heading(plugin: &Plugin) -> String {
112 match plugin.manifest() {
113 Ok(manifest) => format!(
114 "{} {} ({}) in {}",
115 manifest.id,
116 manifest.version,
117 manifest.kind,
118 plugin.dir.display()
119 ),
120 Err(_) => plugin.dir.display().to_string(),
121 }
122}
123
124fn check_plugin(plugin: &Plugin, deny_warnings: bool) -> Result<bool, String> {
126 let bytes = plugin.manifest_bytes()?;
127 let module = check::check(&bytes, None)
128 .manifest
129 .map(|manifest| plugin.bundle.join(manifest.module));
130 let component = match &module {
131 Some(path) if path.is_file() => {
132 Some(std::fs::read(path).map_err(|error| format!("{}: {error}", path.display()))?)
133 }
134 _ => None,
135 };
136 let report = check::check(&bytes, component.as_deref());
137 println!("{}", heading(plugin));
138 print!("{report}");
139 if component.is_none() && report.is_ok() {
140 println!(" note: not built yet; the component was not checked (standard-plugin build)");
141 }
142 let ok = report.is_ok() && !(deny_warnings && !report.warnings.is_empty());
143 println!(
144 " {}: {} error(s), {} warning(s)",
145 if ok { "ok" } else { "failed" },
146 report.errors.len(),
147 report.warnings.len()
148 );
149 Ok(ok)
150}
151
152fn pack_dir(dir: &Path, out: Option<&Path>) -> Result<Option<(PathBuf, pack::Packed)>, String> {
154 for plugin in project::plugins(dir)? {
155 if !check_plugin(&plugin, false)? {
156 return Ok(None);
157 }
158 }
159 let packed = pack::package(dir)?;
160 let out = out.map(Path::to_path_buf).unwrap_or_else(|| {
161 dir.join("target/standard-plugin").join(format!(
162 "{}-{}.tar",
163 packed.manifest.id, packed.manifest.version
164 ))
165 });
166 if let Some(parent) = out.parent() {
167 std::fs::create_dir_all(parent)
168 .map_err(|error| format!("{}: {error}", parent.display()))?;
169 }
170 std::fs::write(&out, &packed.bytes).map_err(|error| format!("{}: {error}", out.display()))?;
171 let names = pack::entry_names(&packed.bytes)?;
172 println!(
173 " packed {} ({} files, {} bytes)\n sha256 {}",
174 out.display(),
175 names.len(),
176 packed.bytes.len(),
177 pack::content_hash(&packed.bytes)
178 );
179 Ok(Some((out, packed)))
180}
181
182fn execute(command: Command) -> Result<bool, String> {
183 match command {
184 Command::New {
185 name,
186 kind,
187 dir,
188 sdk_path,
189 } => {
190 let sdk = match sdk_path {
191 Some(path) => SdkSource::Path(
192 path.canonicalize()
193 .map_err(|error| format!("{}: {error}", path.display()))?,
194 ),
195 None => SdkSource::default_for_this_build(),
196 };
197 let root = scaffold::scaffold(&dir, &name, kind, &sdk).map_err(|e| e.to_string())?;
198 println!("created {}", root.display());
199 println!("next: cd {} && standard-plugin build", root.display());
200 Ok(true)
201 }
202 Command::Build { dir, debug } => {
203 let mut ok = true;
204 for plugin in project::plugins(&dir)? {
205 let built = build::build(&plugin, !debug)?;
206 println!("built {}", built.component.display());
207 match &built.browser {
208 Ok(path) => println!("browser artifact: {}", path.display()),
209 Err(reason) => println!("browser artifact: {reason}"),
210 }
211 ok &= check_plugin(&plugin, false)?;
212 }
213 Ok(ok)
214 }
215 Command::Check { dir, deny_warnings } => {
216 let mut ok = true;
217 for plugin in project::plugins(&dir)? {
218 ok &= check_plugin(&plugin, deny_warnings)?;
219 }
220 Ok(ok)
221 }
222 Command::Pack { dir, out } => Ok(pack_dir(&dir, out.as_deref())?.is_some()),
223 Command::Install { dir } => {
224 if pack_dir(&dir, None)?.is_none() {
225 return Ok(false);
226 }
227 Err(install::exec(&dir))
230 }
231 Command::Guide { name, dir } => {
232 let found = guide::guides(&dir);
233 match name {
234 None => {
235 print!("{}", found.listing());
236 Ok(true)
237 }
238 Some(name) => match found.find(&name) {
239 Some(guide) => {
240 print!("{}", guide.text);
241 Ok(true)
242 }
243 None => Err(format!(
244 "no guide named {name:?}; `standard-plugin guide` lists them"
245 )),
246 },
247 }
248 }
249 }
250}