waos-cli 0.1.0

cargo waos: writes a new program, builds a program as a component, runs it in the WaOS on this machine, and packages it as a signed .wapk.
//! `cargo waos`: writes a new program, builds a program as a component, runs it in the WaOS on
//! this machine, and packages it as a signed `.wapk` for a package source.
//!
//! The invariant: every command is about a program and the WaOS it runs in. A run goes to the
//! WaOS already running, starting the installed app when none answers, never to an engine of
//! this process's own (the design's decision 29); and nothing is ever installed on a device: a
//! phone runs programs in the WaOS app already on it, and the WaOS app itself is built with
//! `cargo waft`.

mod build;
mod metadata;
mod new;
mod package;
mod run;
mod signing_key;
mod waos_app;
mod workspace;

use std::path::Path;
use std::process::ExitCode;

use anyhow::{Context as _, Result};
use clap::{Args, Parser, Subcommand};

use crate::build::{BuiltProgram, Profile};
use crate::metadata::WaosMetadata;
use crate::new::Template;

/// What cargo hands this binary: its own name, then `waos`, then the arguments.
#[derive(Parser)]
#[command(name = "cargo", bin_name = "cargo")]
enum Cargo {
    #[command(
        name = "waos",
        about = "cargo waos: writes a new program, and builds a program as a component, runs it in WaOS and packages it",
        version
    )]
    Waos(WaosArgs),
}

#[derive(Args)]
struct WaosArgs {
    #[command(subcommand)]
    command: Command,
}

#[derive(Subcommand)]
enum Command {
    /// Writes a new program in a folder of its name: a Waft app that `cargo waos run` runs and
    /// `cargo waos package` packages as it is.
    New(NewArgs),
    /// Builds the program as a component, for wasm32-wasip2, and puts it at
    /// target/waos/<profile>/<package>.wasm.
    Build(BuildArgs),
    /// Builds the program and runs it in the WaOS on this machine, starting WaOS.app when none
    /// answers, as the program the id in its [package.metadata.waos] names, with that program's
    /// settings and folders. What it writes comes here as it writes it; Ctrl-C stops it; the exit
    /// status is 0 for a run that returned success and 1 otherwise.
    Run(BuildArgs),
    /// Builds the program's release component and writes it as a package signed with your
    /// signing key, at target/waos/<id>-<version>.wapk, listed in target/waos/index.json.
    /// The id, name and icon are the package's [package.metadata.waos]; the version is its own.
    Package(Program),
}

/// What a new program is called, and the widget kit it starts with.
#[derive(Args)]
struct NewArgs {
    /// The program's package name, and its folder's: lowercase letters, digits, `-` and `_`,
    /// starting with a letter.
    name: String,
    /// The widget kit the program starts with.
    #[arg(long, value_enum, default_value_t)]
    template: Template,
}

/// Which program.
#[derive(Args)]
struct Program {
    /// The program's package; without it, the package of this directory.
    #[arg(short, long, value_name = "PACKAGE")]
    package: Option<String>,
}

/// Which program, and which build of it.
#[derive(Args)]
struct BuildArgs {
    #[command(flatten)]
    program: Program,
    /// An optimized build.
    #[arg(long)]
    release: bool,
}

impl BuildArgs {
    fn profile(&self) -> Profile {
        if self.release {
            Profile::Release
        } else {
            Profile::Debug
        }
    }
}

fn main() -> ExitCode {
    let Cargo::Waos(args) = Cargo::parse();
    let done = match args.command {
        Command::New(new) => new_program(&new),
        Command::Build(build) => build_only(&build),
        Command::Run(build) => build_and_run(&build),
        Command::Package(program) => package(&program),
    };
    done.unwrap_or_else(|error| {
        eprintln!("cargo waos: {error:#}");
        ExitCode::FAILURE
    })
}

/// Writes a new program in a folder of this directory's, and says how to run it.
fn new_program(args: &NewArgs) -> Result<ExitCode> {
    let folder = new::create(&args.name, args.template, Path::new("."))?;
    println!("created {}", folder.display());
    println!("  cd {} && cargo waos run", args.name);
    Ok(ExitCode::SUCCESS)
}

/// Builds the program and says where its component is.
fn build_only(args: &BuildArgs) -> Result<ExitCode> {
    let built = build_chosen(args)?;
    println!("{}", built.component.display());
    Ok(ExitCode::SUCCESS)
}

/// Builds the program and runs it in WaOS as the program its id names until it ends, on a
/// runtime of this thread's. A package with no id is refused before anything is built.
fn build_and_run(args: &BuildArgs) -> Result<ExitCode> {
    let metadata = workspace::metadata()?;
    let package = workspace::chosen_package(&metadata, args.program.package.as_deref())?;
    let id = WaosMetadata::id_of(package)?;
    let built = build::build(&metadata, package, args.profile())?;
    let runtime = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .context("a runtime for the connection")?;
    runtime.block_on(run::run(id, &built))
}

/// Packages the program and says where its package is.
fn package(program: &Program) -> Result<ExitCode> {
    let packaged = package::package(program.package.as_deref())?;
    eprintln!("cargo waos: listed in {}", packaged.index.display());
    println!("{}", packaged.package.display());
    Ok(ExitCode::SUCCESS)
}

/// Builds the program the arguments choose.
fn build_chosen(args: &BuildArgs) -> Result<BuiltProgram> {
    let metadata = workspace::metadata()?;
    let package = workspace::chosen_package(&metadata, args.program.package.as_deref())?;
    build::build(&metadata, package, args.profile())
}