mod icon;
mod manifest;
mod program_name;
mod template;
use std::fs;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use self::program_name::ProgramName;
pub use self::template::Template;
const TOOLCHAIN: &str = r#"# Nightly because Waft needs `arbitrary_self_types`, pinned to the date WaOS is built with.
# `wasm32-wasip2` is what a program is built for.
[toolchain]
channel = "nightly-2026-09-02"
components = ["rustfmt", "clippy"]
targets = ["wasm32-wasip2"]
"#;
const GITIGNORE: &str = "/target\n";
pub fn create(name: &str, template: Template, parent: &Path) -> Result<PathBuf> {
let name: ProgramName = name.parse()?;
let folder = parent.join(name.as_str());
make_folder(&folder)?;
write_program(&folder, &name, template)?;
Ok(folder)
}
fn make_folder(folder: &Path) -> Result<()> {
match fs::create_dir(folder) {
Ok(()) => Ok(()),
Err(error) if error.kind() == ErrorKind::AlreadyExists => {
bail!("{} already exists", folder.display())
}
Err(error) => Err(error).with_context(|| format!("making {}", folder.display())),
}
}
fn write_program(folder: &Path, name: &ProgramName, template: Template) -> Result<()> {
write(folder, "Cargo.toml", manifest::cargo_toml(name, template))?;
write(folder, "rust-toolchain.toml", TOOLCHAIN)?;
write(folder, icon::FILE, icon::placeholder())?;
write(folder, ".gitignore", GITIGNORE)?;
let source = folder.join("src");
fs::create_dir(&source).with_context(|| format!("making {}", source.display()))?;
write(folder, "src/main.rs", template.main_rs())
}
fn write(folder: &Path, file: &str, contents: impl AsRef<[u8]>) -> Result<()> {
let path = folder.join(file);
fs::write(&path, contents).with_context(|| format!("writing {}", path.display()))
}
#[cfg(test)]
mod tests {
use cargo_metadata::MetadataCommand;
use waos_package::{Manifest, SigningKey};
use super::*;
use crate::metadata::WaosMetadata;
#[test]
fn a_new_program_has_its_manifest_toolchain_icon_ignores_and_main() {
let scratch = tempfile::tempdir().unwrap();
let folder = create("hello", Template::Winui, scratch.path()).unwrap();
assert_eq!(folder, scratch.path().join("hello"));
let mut written: Vec<String> = walk(&folder)
.iter()
.map(|path| path.strip_prefix(&folder).unwrap().display().to_string())
.collect();
written.sort();
assert_eq!(
written,
[
".gitignore",
"Cargo.toml",
"icon.png",
"rust-toolchain.toml",
"src/main.rs"
]
);
let main_rs = fs::read_to_string(folder.join("src/main.rs")).unwrap();
assert_eq!(main_rs, Template::Winui.main_rs());
}
#[test]
fn each_template_runs_a_waft_app_in_its_kit() {
for (template, app) in [
(Template::Cupertino, "CupertinoApp"),
(Template::Winui, "WidgetsApp"),
] {
let main_rs = template.main_rs();
assert!(main_rs.contains("#[wasdk::main]\nasync fn main()"));
assert!(main_rs.contains("waft_embedder_wasdk::run("));
assert!(main_rs.contains(app), "{template:?}");
assert!(main_rs.contains(&template.kit().replace('-', "_")));
}
}
#[test]
fn a_folder_there_already_is_left_as_it_is() {
let scratch = tempfile::tempdir().unwrap();
let folder = scratch.path().join("hello");
fs::create_dir(&folder).unwrap();
fs::write(folder.join("notes.txt"), "mine").unwrap();
let error = create("hello", Template::Cupertino, scratch.path()).unwrap_err();
assert!(error.to_string().contains("already exists"), "{error}");
assert_eq!(walk(&folder), [folder.join("notes.txt")]);
}
#[test]
fn a_name_that_is_not_a_package_name_writes_nothing() {
let scratch = tempfile::tempdir().unwrap();
let error = create("My App", Template::Cupertino, scratch.path()).unwrap_err();
assert!(error.to_string().contains("not a package name"), "{error}");
assert!(walk(scratch.path()).is_empty());
}
#[test]
fn a_new_program_has_what_a_package_needs() {
let scratch = tempfile::tempdir().unwrap();
let folder = create("my-app", Template::Cupertino, scratch.path()).unwrap();
let metadata = MetadataCommand::new()
.manifest_path(folder.join("Cargo.toml"))
.no_deps()
.exec()
.unwrap();
let package = metadata.root_package().unwrap();
let waos = WaosMetadata::of(package).unwrap();
assert_eq!(waos.id.as_str(), "dev.example.myapp");
let manifest = Manifest::new(waos.id, waos.name, package.version.clone()).unwrap();
let component = scratch.path().join("program.wasm");
fs::write(&component, b"\0asm").unwrap();
let written = scratch.path().join("my-app.wapk");
waos_package::write(
&written,
&manifest,
&component,
&waos.icon,
&SigningKey::generate(),
)
.unwrap();
}
#[test]
fn the_toolchain_is_waos_own() {
let waos = concat!(env!("CARGO_MANIFEST_DIR"), "/../../rust-toolchain.toml");
let waos: toml::Table = fs::read_to_string(waos).unwrap().parse().unwrap();
let program: toml::Table = TOOLCHAIN.parse().unwrap();
assert_eq!(program, waos);
}
fn walk(folder: &Path) -> Vec<PathBuf> {
let mut files = Vec::new();
for entry in fs::read_dir(folder).unwrap() {
let path = entry.unwrap().path();
if path.is_dir() {
files.extend(walk(&path));
} else {
files.push(path);
}
}
files
}
}