pub mod build;
pub mod check;
pub mod component;
pub mod guide;
pub mod install;
pub mod pack;
pub mod project;
pub mod scaffold;
pub mod world;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use clap::{Parser, Subcommand};
use crate::project::Plugin;
use crate::scaffold::{NewKind, SdkSource};
#[derive(Debug, Parser)]
#[command(name = "standard-plugin", version, about)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
New {
name: String,
#[arg(long, value_enum, default_value = "ui")]
kind: NewKind,
#[arg(long, default_value = ".")]
dir: PathBuf,
#[arg(long)]
sdk_path: Option<PathBuf>,
},
Build {
#[arg(long, default_value = ".")]
dir: PathBuf,
#[arg(long)]
debug: bool,
},
Check {
#[arg(long, default_value = ".")]
dir: PathBuf,
#[arg(long)]
deny_warnings: bool,
},
Pack {
#[arg(long, default_value = ".")]
dir: PathBuf,
#[arg(long)]
out: Option<PathBuf>,
},
Install {
#[arg(long, default_value = ".")]
dir: PathBuf,
},
Guide {
name: Option<String>,
#[arg(long, default_value = ".")]
dir: PathBuf,
},
}
pub fn run(args: impl IntoIterator<Item = OsString>) -> ExitCode {
let cli = match Cli::try_parse_from(args) {
Ok(cli) => cli,
Err(error) => {
let _ = error.print();
return if error.use_stderr() {
ExitCode::from(2)
} else {
ExitCode::SUCCESS
};
}
};
match execute(cli.command) {
Ok(true) => ExitCode::SUCCESS,
Ok(false) => ExitCode::FAILURE,
Err(error) => {
eprintln!("error: {error}");
ExitCode::FAILURE
}
}
}
fn heading(plugin: &Plugin) -> String {
match plugin.manifest() {
Ok(manifest) => format!(
"{} {} ({}) in {}",
manifest.id,
manifest.version,
manifest.kind,
plugin.dir.display()
),
Err(_) => plugin.dir.display().to_string(),
}
}
fn check_plugin(plugin: &Plugin, deny_warnings: bool) -> Result<bool, String> {
let bytes = plugin.manifest_bytes()?;
let module = check::check(&bytes, None)
.manifest
.map(|manifest| plugin.bundle.join(manifest.module));
let component = match &module {
Some(path) if path.is_file() => {
Some(std::fs::read(path).map_err(|error| format!("{}: {error}", path.display()))?)
}
_ => None,
};
let report = check::check(&bytes, component.as_deref());
println!("{}", heading(plugin));
print!("{report}");
if component.is_none() && report.is_ok() {
println!(" note: not built yet; the component was not checked (standard-plugin build)");
}
let ok = report.is_ok() && !(deny_warnings && !report.warnings.is_empty());
println!(
" {}: {} error(s), {} warning(s)",
if ok { "ok" } else { "failed" },
report.errors.len(),
report.warnings.len()
);
Ok(ok)
}
fn pack_dir(dir: &Path, out: Option<&Path>) -> Result<Option<(PathBuf, pack::Packed)>, String> {
for plugin in project::plugins(dir)? {
if !check_plugin(&plugin, false)? {
return Ok(None);
}
}
let packed = pack::package(dir)?;
let out = out.map(Path::to_path_buf).unwrap_or_else(|| {
dir.join("target/standard-plugin").join(format!(
"{}-{}.tar",
packed.manifest.id, packed.manifest.version
))
});
if let Some(parent) = out.parent() {
std::fs::create_dir_all(parent)
.map_err(|error| format!("{}: {error}", parent.display()))?;
}
std::fs::write(&out, &packed.bytes).map_err(|error| format!("{}: {error}", out.display()))?;
let names = pack::entry_names(&packed.bytes)?;
println!(
" packed {} ({} files, {} bytes)\n sha256 {}",
out.display(),
names.len(),
packed.bytes.len(),
pack::content_hash(&packed.bytes)
);
Ok(Some((out, packed)))
}
fn execute(command: Command) -> Result<bool, String> {
match command {
Command::New {
name,
kind,
dir,
sdk_path,
} => {
let sdk = match sdk_path {
Some(path) => SdkSource::Path(
path.canonicalize()
.map_err(|error| format!("{}: {error}", path.display()))?,
),
None => SdkSource::default_for_this_build(),
};
let root = scaffold::scaffold(&dir, &name, kind, &sdk).map_err(|e| e.to_string())?;
println!("created {}", root.display());
println!("next: cd {} && standard-plugin build", root.display());
Ok(true)
}
Command::Build { dir, debug } => {
let mut ok = true;
for plugin in project::plugins(&dir)? {
let built = build::build(&plugin, !debug)?;
println!("built {}", built.component.display());
match &built.browser {
Ok(path) => println!("browser artifact: {}", path.display()),
Err(reason) => println!("browser artifact: {reason}"),
}
ok &= check_plugin(&plugin, false)?;
}
Ok(ok)
}
Command::Check { dir, deny_warnings } => {
let mut ok = true;
for plugin in project::plugins(&dir)? {
ok &= check_plugin(&plugin, deny_warnings)?;
}
Ok(ok)
}
Command::Pack { dir, out } => Ok(pack_dir(&dir, out.as_deref())?.is_some()),
Command::Install { dir } => {
if pack_dir(&dir, None)?.is_none() {
return Ok(false);
}
Err(install::exec(&dir))
}
Command::Guide { name, dir } => {
let found = guide::guides(&dir);
match name {
None => {
print!("{}", found.listing());
Ok(true)
}
Some(name) => match found.find(&name) {
Some(guide) => {
print!("{}", guide.text);
Ok(true)
}
None => Err(format!(
"no guide named {name:?}; `standard-plugin guide` lists them"
)),
},
}
}
}
}