#![feature(never_type)]
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use anyhow::{Result, bail, ensure};
use clap::{CommandFactory, Parser, ValueHint};
use clap_complete::Shell;
use tokio::process::Command;
use wasefire_cli_tools::{action, cmd, fs};
use wasefire_one_of::at_most_one_of;
at_most_one_of!["_dev", "_prod"];
#[derive(Parser)]
#[command(name = "wasefire", version, about)]
struct Flags {
#[command(flatten)]
options: Options,
#[command(subcommand)]
action: Action,
}
#[test]
fn flags() {
<Flags as clap::CommandFactory>::command().debug_assert();
}
#[derive(clap::Args)]
struct Options {}
#[derive(clap::Subcommand)]
enum Action {
AppletList,
#[group(id = "Action::AppletInstall")]
AppletInstall {
#[command(flatten)]
options: action::ConnectionOptions,
#[command(flatten)]
action: action::AppletInstall,
},
AppletUpdate,
#[group(id = "Action::AppletUninstall")]
AppletUninstall {
#[command(flatten)]
options: action::ConnectionOptions,
#[command(flatten)]
action: action::AppletUninstall,
},
#[group(id = "Action::AppletMetadata")]
AppletMetadata {
#[command(flatten)]
options: action::ConnectionOptions,
#[command(flatten)]
action: action::AppletMetadata,
},
#[group(id = "Action::AppletExitStatus")]
AppletExitStatus {
#[command(flatten)]
options: action::ConnectionOptions,
#[command(flatten)]
action: action::AppletExitStatus,
},
#[group(id = "Action::AppletReboot")]
AppletReboot {
#[command(flatten)]
options: action::ConnectionOptions,
#[command(flatten)]
action: action::AppletReboot,
},
#[group(id = "Action::AppletRpc")]
AppletRpc {
#[command(flatten)]
options: action::ConnectionOptions,
#[command(flatten)]
action: action::AppletRpc,
},
#[group(id = "Action::BundleInfo")]
BundleInfo {
#[command(flatten)]
action: action::BundleInfo,
},
Host(Host),
#[group(id = "Action::PlatformClearStore")]
PlatformClearStore {
#[command(flatten)]
options: action::ConnectionOptions,
#[command(flatten)]
action: action::PlatformClearStore,
},
#[group(id = "Action::PlatformInfo")]
PlatformInfo {
#[command(flatten)]
options: action::ConnectionOptions,
#[command(flatten)]
action: action::PlatformInfo,
},
PlatformList(action::PlatformList),
#[group(id = "Action::PlatformUpdate")]
PlatformUpdate {
#[command(flatten)]
options: action::ConnectionOptions,
#[command(flatten)]
action: action::PlatformUpdate,
},
#[group(id = "Action::PlatformReboot")]
PlatformReboot {
#[command(flatten)]
options: action::ConnectionOptions,
#[command(flatten)]
action: action::PlatformReboot,
},
#[group(id = "Action::PlatformLock")]
PlatformLock {
#[command(flatten)]
options: action::ConnectionOptions,
#[command(flatten)]
action: action::PlatformLock,
},
#[group(id = "Action::PlatformRpc")]
PlatformRpc {
#[command(flatten)]
options: action::ConnectionOptions,
#[command(flatten)]
action: action::PlatformRpc,
},
RustAppletNew(action::RustAppletNew),
RustAppletBuild(action::RustAppletBuild),
RustAppletTest(action::RustAppletTest),
#[group(id = "Action::RustAppletInstall")]
RustAppletInstall {
#[command(flatten)]
options: action::ConnectionOptions,
#[command(flatten)]
action: action::RustAppletInstall,
},
SelfUpdate,
Completion(Completion),
}
#[derive(clap::Args)]
struct Host {
#[arg(long, default_value = "wasefire/host", value_hint = ValueHint::DirPath)]
#[arg(verbatim_doc_comment)]
dir: PathBuf,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
}
#[derive(clap::Args)]
struct Completion {
shell: Option<Shell>,
#[arg(long, default_value = "-", value_hint = ValueHint::FilePath)]
output: PathBuf,
}
impl Host {
async fn run(&self) -> Result<!> {
let bin = self.dir.join("platform.bin");
#[cfg(feature = "_dev")]
if !fs::exists(&bin).await {
let bundle = "target/wasefire/platform.bin";
anyhow::ensure!(
fs::exists(&bundle).await,
"Run `cargo xtask runner host bundle` first"
);
fs::copy(bundle, &bin).await?;
}
#[cfg(feature = "_prod")]
if !fs::exists(&bin).await {
fs::create_dir_all(&self.dir).await?;
static HOST_PLATFORM: &[u8] = include_bytes!(env!("WASEFIRE_HOST_PLATFORM"));
let mut params = fs::WriteParams::new(&bin);
params.options().write(true).create_new(true).mode(0o777);
fs::write(params, HOST_PLATFORM).await?;
}
loop {
let mut host = Command::new(&bin);
host.arg(&self.dir);
host.args(&self.args);
cmd::exit_status(&mut host).await?;
}
}
}
impl Completion {
async fn run(&self) -> Result<()> {
let shell = match self.shell.or_else(Shell::from_env) {
Some(x) => x,
None => bail!("failed to guess a shell"),
};
let mut cmd = Flags::command();
let name = "wasefire".to_string();
let mut output: Box<dyn Write> = if self.output == Path::new("-") {
Box::new(std::io::stdout())
} else {
fs::create_parent(&self.output).await?;
Box::new(File::create(&self.output)?)
};
clap_complete::generate(shell, &mut cmd, name, &mut output);
Ok(())
}
}
async fn self_update() -> Result<PathBuf> {
const URL: &str = concat!(
"https://github.com/google/wasefire/releases/latest/download/wasefire-",
target_triple::target!(),
".tar.gz"
);
let path = std::env::current_exe()?;
let dir = path.parent().expect("current exe should be a file");
let content = fs::download(URL).await?;
let names = fs::targz_list(&content)?;
ensure!(names.len() == 1, "CLI tarball is not a singleton");
fs::targz_extract(content, dir.to_owned()).await?;
fs::rename(dir.join(&names[0]), &path).await?;
Ok(path)
}
#[tokio::main]
async fn main() -> Result<()> {
env_logger::init();
if !cfg!(any(feature = "_dev", feature = "_prod")) {
let path = self_update().await?;
let mut rerun = Command::new(path);
let mut args = std::env::args_os();
if let Some(arg0) = args.next() {
rerun.arg0(arg0);
}
rerun.args(args);
cmd::replace(rerun)
}
let flags = Flags::parse();
match flags.action {
Action::AppletList => bail!("not implemented yet"),
Action::AppletInstall { options, action } => action.run(&options.connect().await?).await,
Action::AppletUpdate => bail!("not implemented yet"),
Action::AppletUninstall { options, action } => action.run(&options.connect().await?).await,
Action::AppletMetadata { options, action } => action.run(&options.connect().await?).await,
Action::AppletExitStatus { options, action } => action.run(&options.connect().await?).await,
Action::AppletReboot { options, action } => action.run(&options.connect().await?).await,
Action::AppletRpc { options, action } => action.run(&options.connect().await?).await,
Action::BundleInfo { action } => action.run().await,
Action::Host(x) => x.run().await?,
Action::PlatformClearStore { options, action } => {
action.run(&options.connect().await?).await
}
Action::PlatformInfo { options, action } => action.run(&options.connect().await?).await,
Action::PlatformList(x) => x.run().await,
Action::PlatformUpdate { options, action } => action.run(&options.connect().await?).await,
Action::PlatformReboot { options, action } => action.run(&options.connect().await?).await,
Action::PlatformLock { options, action } => action.run(&options.connect().await?).await,
Action::PlatformRpc { options, action } => action.run(&options.connect().await?).await,
Action::RustAppletNew(x) => x.run().await,
Action::RustAppletBuild(x) => x.run().await.map(|_| ()),
Action::RustAppletTest(x) => x.run().await,
Action::RustAppletInstall { options, action } => {
action.run(&options.connect().await?).await
}
Action::SelfUpdate => self_update().await.map(|_| ()),
Action::Completion(x) => x.run().await,
}
}