#![forbid(unsafe_code)]
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::{fs, io};
use clap::Parser;
use fs_extra::dir::CopyOptions;
use wasmtime::{component::Component, Result, *};
use wasmtime_wasi::bindings::Command;
use wasmtime_wasi::{DirPerms, FilePerms, ResourceTable, WasiView};
mod cargo;
use cargo::{CargoToml, InheritEnv, TaskDetail};
#[derive(clap::Parser, Debug)]
#[command(version, about)]
struct Args {
#[arg(hide = true)]
argv0: String,
task_name: String,
args: Vec<String>,
}
type Error = Box<dyn std::error::Error + Send + Sync + 'static>;
struct Ctx {
wasi: wasmtime_wasi::WasiCtx,
table: wasmtime::component::ResourceTable,
}
impl WasiView for Ctx {
fn table(&mut self) -> &mut ResourceTable {
&mut self.table
}
fn ctx(&mut self) -> &mut wasmtime_wasi::WasiCtx {
&mut self.wasi
}
}
#[derive(Debug, PartialEq, Clone)]
struct TaskDefinition {
name: String,
path: PathBuf,
env: EnvVars,
}
#[tokio::main]
async fn main() -> Result<(), Error> {
let args = Args::parse();
assert_eq!(
args.argv0, "task",
"cargo-task-wasm should be invoked as a `cargo` subcommand"
);
let workspace_root_dir = findup_workspace(&std::env::current_dir()?)?;
let cargo_toml_path = workspace_root_dir.join("Cargo.toml");
let cargo_toml: CargoToml = toml::from_str(&fs::read_to_string(&cargo_toml_path)?)?;
let target_workspace_dir = build_task_workspace(
&cargo_toml,
&workspace_root_dir.join("target/tasks"),
&workspace_root_dir.join("tasks"),
)?;
let tasks_target_dir = target_workspace_dir.join("../target");
let _cargo_status = std::process::Command::new("cargo")
.arg("+beta")
.arg("build")
.arg("--target")
.arg("wasm32-wasip2")
.current_dir(&target_workspace_dir)
.arg("--target-dir")
.arg(&tasks_target_dir)
.status()?;
let tasks = resolve_tasks(&cargo_toml, &workspace_root_dir.join("tasks"))?;
let task_definition = tasks.get(&args.task_name).expect(&format!(
"could not find a task with the name {:?}",
&args.task_name
));
let task_wasm_path =
tasks_target_dir.join(format!("wasm32-wasip2/debug/{}.wasm", task_definition.name));
let mut config = Config::default();
config.wasm_component_model(true);
config.async_support(true);
let engine = Engine::new(&config)?;
let component = Component::from_file(&engine, task_wasm_path)?;
let mut linker: component::Linker<Ctx> = component::Linker::new(&engine);
wasmtime_wasi::add_to_linker_async(&mut linker)?;
let mut wasi = wasmtime_wasi::WasiCtxBuilder::new();
wasi.inherit_stderr();
wasi.inherit_stdout();
match &task_definition.env {
EnvVars::None => {}
EnvVars::All => {
wasi.inherit_env();
}
EnvVars::AllowList(vars) => {
for (key, value) in vars {
eprintln!("setting environment variable: {key} = {value}");
wasi.env(key, value);
}
}
}
wasi.args(&args.args);
wasi.preopened_dir(
&target_workspace_dir,
"/",
DirPerms::all(),
FilePerms::all(),
)?;
let host = Ctx {
wasi: wasi.build(),
table: wasmtime::component::ResourceTable::new(),
};
let mut store: Store<Ctx> = Store::new(&engine, host);
let command = Command::instantiate_async(&mut store, &component, &linker).await?;
let program_result = command.wasi_cli_run().call_run(&mut store).await?;
match program_result {
Ok(()) => Ok(()),
Err(()) => std::process::exit(1),
}
}
fn findup_workspace(entry: &Path) -> io::Result<PathBuf> {
if entry.join("Cargo.toml").exists() {
return Ok(entry.to_path_buf());
}
let parent = entry
.parent()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "No Cargo.toml found"))?;
findup_workspace(parent)
}
#[derive(Debug, PartialEq, Clone)]
enum EnvVars {
All,
None,
AllowList(Vec<(String, String)>),
}
fn build_sandbox_env(task_details: &TaskDetail) -> EnvVars {
let Some(inherit_env) = &task_details.inherit_env else {
return EnvVars::None;
};
match inherit_env {
InheritEnv::Bool(true) => EnvVars::All,
InheritEnv::Bool(false) => EnvVars::None,
InheritEnv::AllowList(vars) => {
let mut map = Vec::new();
for var in vars {
if let Ok(value) = std::env::var(&var) {
map.push((var.clone(), value));
}
}
EnvVars::AllowList(map)
}
}
}
fn build_task_workspace(
cargo_toml: &CargoToml,
root_dir: &Path,
input_tasks_dir: &Path,
) -> Result<PathBuf, Error> {
use std::fmt::Write;
let workspace_dir = root_dir.join("ws");
if workspace_dir.exists() {
fs::remove_dir_all(&workspace_dir)?;
}
fs::create_dir_all(&workspace_dir)?;
let mut virtual_cargo_toml_contents = String::new();
writeln!(
virtual_cargo_toml_contents,
r#"
[package]
name = "_tasks"
version = "0.1.0"
edition = "2021"
"#
)?;
let src_dir = workspace_dir.join("src");
fs::create_dir_all(&src_dir)?;
fs_extra::dir::copy(
input_tasks_dir,
src_dir,
&CopyOptions::new().content_only(true),
)?;
let tasks = resolve_tasks(cargo_toml, input_tasks_dir)?;
for task_definition in tasks.values() {
let task_name = &task_definition.name;
writeln!(
virtual_cargo_toml_contents,
r#"
[[bin]]
name = "{task_name}"
path = "src/{task_name}.rs"
"#
)?;
}
if let Some(metadata) = &cargo_toml.package.metadata {
if let Some(task_deps) = &metadata.task_dependencies {
writeln!(virtual_cargo_toml_contents, "[dependencies]")?;
for (dep_name, dep_version) in task_deps.iter() {
writeln!(
virtual_cargo_toml_contents,
r#"{dep_name} = "{dep_version}""#
)?;
}
}
}
std::fs::write(
workspace_dir.join("Cargo.toml"),
virtual_cargo_toml_contents,
)?;
Ok(workspace_dir)
}
fn resolve_tasks(
cargo_toml: &CargoToml,
tasks_dir: &Path,
) -> io::Result<BTreeMap<String, TaskDefinition>> {
let mut tasks = BTreeMap::new();
let default_path = |task_name: &str| PathBuf::from(format!("tasks/{task_name}.rs"));
if let Some(metadata) = &cargo_toml.package.metadata {
if let Some(toml_tasks) = &metadata.tasks {
for (task_name, task_details) in toml_tasks {
let task_path = default_path(&task_name[..]);
let task_env = build_sandbox_env(task_details);
tasks.insert(
task_name.to_string(),
TaskDefinition {
name: task_name.to_string(),
path: task_path,
env: task_env,
},
);
}
}
}
for entry in fs::read_dir(tasks_dir)? {
if let Ok(entry) = entry {
let filename = entry.file_name();
let filename = filename.to_string_lossy();
if let Some(task_name) = filename.strip_suffix(".rs") {
if tasks.contains_key(task_name) {
continue;
}
tasks.insert(
task_name.to_string(),
TaskDefinition {
name: task_name.to_string(),
path: default_path(&task_name[..]),
env: EnvVars::None,
},
);
}
}
}
Ok(tasks)
}