Initial commit
Signed-off-by: Radu Matei <radu.matei@fermyon.com>
This commit is contained in:
commit
308c266925
|
@ -0,0 +1 @@
|
|||
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,29 @@
|
|||
[package]
|
||||
name = "spin-cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Radu Matei <radu.matei@fermyon.com>"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
async-trait = "0.1"
|
||||
bytes = "1.1"
|
||||
comfy-table = "4.1"
|
||||
env_logger = "0.9"
|
||||
fermyon-templates = { path = "crates/templates" }
|
||||
futures = "0.3"
|
||||
log = { version = "0.4", default-features = false }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
structopt = "0.3"
|
||||
tokio = { version = "1.11", features = ["full"] }
|
||||
toml = "0.5"
|
||||
|
||||
|
||||
[workspace]
|
||||
members = ["crates/templates", "crates/engine"]
|
||||
|
||||
|
||||
[[bin]]
|
||||
name = "spin"
|
||||
path = "src/bin/spin.rs"
|
|
@ -0,0 +1,25 @@
|
|||
[package]
|
||||
name = "fermyon-engine"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Radu Matei <radu.matei@fermyon.com>"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.44"
|
||||
async-trait = "0.1.51"
|
||||
bytes = "1.1.0"
|
||||
dirs = "3.0.2"
|
||||
env_logger = "0.9.0"
|
||||
fs_extra = "1.2.0"
|
||||
futures = "0.3.17"
|
||||
git2 = "0.13"
|
||||
log = { version = "0.4.14", default-features = false }
|
||||
serde = { version = "1.0.130", features = ["derive"] }
|
||||
structopt = "0.3.23"
|
||||
tokio = { version = "1.10.0", features = ["fs"] }
|
||||
toml = "0.5.8"
|
||||
wasi-common = "0.30"
|
||||
wasi-experimental-http-wasmtime = "0.6.0"
|
||||
wasmtime = "0.30"
|
||||
witx-bindgen-rust = { git = "https://github.com/bytecodealliance/witx-bindgen", rev = "0fea5183d121fdf736585ab9d291b3cc08487cd0" }
|
|
@ -0,0 +1,62 @@
|
|||
//! A Fermyon engine.
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use std::sync::Arc;
|
||||
use wasi_common::WasiCtx;
|
||||
use wasmtime::{Engine, InstancePre};
|
||||
|
||||
/// Engine configuration.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Config {
|
||||
/// Environment variables to set inside the WebAssembly module.
|
||||
pub env_vars: Vec<(String, String)>,
|
||||
/// Preopened directories to map inside the WebAssembly module.
|
||||
pub preopen_dirs: Vec<(String, String)>,
|
||||
/// Optional list of HTTP hosts WebAssembly modules are allowed to connect to.
|
||||
pub allowed_http_hosts: Option<Vec<String>>,
|
||||
/// Wasmtime engine configuration.
|
||||
pub wasmtime_config: wasmtime::Config,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Create a new configuration instance.
|
||||
pub fn new(
|
||||
env_vars: Vec<(String, String)>,
|
||||
preopen_dirs: Vec<(String, String)>,
|
||||
allowed_http_hosts: Option<Vec<String>>,
|
||||
) -> Self {
|
||||
let mut wasmtime_config = wasmtime::Config::default();
|
||||
wasmtime_config.wasm_multi_memory(true);
|
||||
wasmtime_config.wasm_module_linking(true);
|
||||
|
||||
Self {
|
||||
env_vars,
|
||||
preopen_dirs,
|
||||
allowed_http_hosts,
|
||||
wasmtime_config,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Top-level runtime context data to be passed to a WebAssembly module.
|
||||
#[derive(Default)]
|
||||
pub struct RuntimeContext<T> {
|
||||
/// WASI context data.
|
||||
pub wasi: Option<WasiCtx>,
|
||||
/// Generic runtime data that can be configured by specific engines.
|
||||
pub data: Option<T>,
|
||||
}
|
||||
|
||||
/// The generic execution context.
|
||||
#[derive(Clone)]
|
||||
pub struct ExecutionContext<T: Default> {
|
||||
/// Entrypoint of the WebAssembly compnent.
|
||||
pub entrypoint_path: String,
|
||||
/// Top-level runtime configuration.
|
||||
pub config: Config,
|
||||
/// Pre-initialized WebAssembly instance.
|
||||
pub pre: Arc<InstancePre<RuntimeContext<T>>>,
|
||||
/// Wasmtime engine.
|
||||
pub engine: Engine,
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
[package]
|
||||
name = "fermyon-templates"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Radu Matei <radu.matei@fermyon.com>"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
async-trait = "0.1"
|
||||
bytes = "1.1"
|
||||
dirs = "3.0"
|
||||
env_logger = "0.9"
|
||||
fs_extra = "1.2"
|
||||
futures = "0.3"
|
||||
git2 = "0.13"
|
||||
log = { version = "0.4", default-features = false }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
structopt = "0.3.23"
|
||||
tokio = { version = "1.10", features = ["fs"] }
|
||||
toml = "0.5"
|
||||
walkdir = "2"
|
|
@ -0,0 +1,160 @@
|
|||
//! Package for working with Wasm component templates.
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use fs_extra::dir::CopyOptions;
|
||||
use git2::{build::RepoBuilder, Repository};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::fs;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
const SPIN_DIR: &str = "spin";
|
||||
const TEMPLATES_DIR: &str = "templates";
|
||||
|
||||
/// A WebAssembly component template repository
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct TemplateRepository {
|
||||
/// The name of the template repository
|
||||
pub name: String,
|
||||
/// The git repository
|
||||
pub git: Option<String>,
|
||||
/// The branch of the git repository.
|
||||
pub branch: Option<String>,
|
||||
/// List of templates in the repository.
|
||||
pub templates: Vec<String>,
|
||||
}
|
||||
|
||||
/// A templates manager that handles the local cache.
|
||||
pub struct TemplatesManager {
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
impl TemplatesManager {
|
||||
/// Creates a cache using the default root directory.
|
||||
pub async fn default() -> Result<Self> {
|
||||
let mut root = dirs::cache_dir().context("cannot get system cache directory")?;
|
||||
root.push(SPIN_DIR);
|
||||
|
||||
Ok(Self::new(root)
|
||||
.await
|
||||
.context("failed to create cache root directory")?)
|
||||
}
|
||||
|
||||
/// Creates a cache using the given root directory.
|
||||
pub async fn new(dir: impl Into<PathBuf>) -> Result<Self> {
|
||||
let root = dir.into();
|
||||
|
||||
let cache = Self { root };
|
||||
cache.ensure_root().await?;
|
||||
Ok(cache)
|
||||
}
|
||||
|
||||
/// Adds the given templates repository locally and offline by cloning it.
|
||||
pub fn add_repo(&self, name: &str, url: &str, branch: Option<&str>) -> Result<()> {
|
||||
let dst = &self.root.join(TEMPLATES_DIR).join(name);
|
||||
log::debug!("adding repository {} to {:?}", url, dst);
|
||||
|
||||
match branch {
|
||||
Some(b) => RepoBuilder::new().branch(b).clone(url, dst)?,
|
||||
None => RepoBuilder::new().clone(url, dst)?,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate a new project given a template name from a template repository.
|
||||
pub async fn generate(&self, repo: &str, template: &str, dst: PathBuf) -> Result<()> {
|
||||
let src = self.get_path(repo, template)?;
|
||||
let mut options = CopyOptions::new();
|
||||
options.copy_inside = true;
|
||||
let _ = fs_extra::dir::copy(src, dst, &options)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lists all the templates repositories.
|
||||
pub async fn list(&self) -> Result<Vec<TemplateRepository>> {
|
||||
let mut res = vec![];
|
||||
let templates = &self.root.join(TEMPLATES_DIR);
|
||||
|
||||
// Search the top-level directories in $XDG_CACHE/spin/templates.
|
||||
for tr in WalkDir::new(templates).max_depth(1).follow_links(true) {
|
||||
let tr = tr?.clone();
|
||||
if tr.path().eq(templates) || !tr.path().is_dir() {
|
||||
continue;
|
||||
}
|
||||
let name = Self::path_to_name(tr.clone().path());
|
||||
let mut templates = vec![];
|
||||
let td = tr.clone().path().join(TEMPLATES_DIR);
|
||||
for t in WalkDir::new(td.clone()).max_depth(1).follow_links(true) {
|
||||
let t = t?.clone();
|
||||
if t.path().eq(&td) || !t.path().is_dir() {
|
||||
continue;
|
||||
}
|
||||
templates.push(Self::path_to_name(t.path()));
|
||||
}
|
||||
|
||||
let repo = match Repository::open(tr.clone().path()) {
|
||||
Ok(repo) => TemplateRepository {
|
||||
name,
|
||||
git: repo
|
||||
.find_remote(repo.remotes()?.get(0).unwrap_or("origin"))?
|
||||
.url()
|
||||
.map(|s| s.to_string()),
|
||||
branch: repo.head().unwrap().name().map(|s| s.to_string()),
|
||||
templates,
|
||||
},
|
||||
Err(_) => TemplateRepository {
|
||||
name,
|
||||
git: None,
|
||||
branch: None,
|
||||
templates,
|
||||
},
|
||||
};
|
||||
res.push(repo);
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Get the path of a template from the given repository.
|
||||
fn get_path(&self, repo: &str, template: &str) -> Result<PathBuf> {
|
||||
let repo_path = &self.root.join(TEMPLATES_DIR).join(repo);
|
||||
if !repo_path.exists() {
|
||||
bail!("cannot find templates repository {} locally", repo)
|
||||
}
|
||||
|
||||
let template_path = repo_path.join(TEMPLATES_DIR).join(template);
|
||||
if !template_path.exists() {
|
||||
bail!("cannot find template {} in repository {}", template, repo);
|
||||
}
|
||||
|
||||
Ok(template_path)
|
||||
}
|
||||
|
||||
/// Ensure the root directory exists, or else create it.
|
||||
async fn ensure_root(&self) -> Result<()> {
|
||||
if !self.root.exists() {
|
||||
log::debug!("creating cache root directory `{}`", self.root.display());
|
||||
fs::create_dir_all(&self.root).await.with_context(|| {
|
||||
format!(
|
||||
"failed to create cache root directory `{}`",
|
||||
self.root.display()
|
||||
)
|
||||
})?;
|
||||
} else if !self.root.is_dir() {
|
||||
bail!("cache root `{}` already exists and is not a directory");
|
||||
} else {
|
||||
log::debug!(
|
||||
"using existing cache root directory `{}`",
|
||||
self.root.display()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn path_to_name(p: &Path) -> String {
|
||||
p.file_name().unwrap().to_str().unwrap().to_string()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
# Project Spin
|
||||
|
||||
Project Spin is the next version of the Fermyon runtime.
|
|
@ -0,0 +1,32 @@
|
|||
use anyhow::Error;
|
||||
use spin_cli::commands::templates::TemplatesCommand;
|
||||
use structopt::{clap::AppSettings, StructOpt};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
env_logger::init();
|
||||
|
||||
SpinApp::from_args().run().await
|
||||
}
|
||||
|
||||
/// The Spin CLI
|
||||
#[derive(StructOpt)]
|
||||
#[structopt(
|
||||
name = "spin",
|
||||
version = env!("CARGO_PKG_VERSION"),
|
||||
global_settings = &[
|
||||
AppSettings::VersionlessSubcommands,
|
||||
AppSettings::ColoredHelp
|
||||
])]
|
||||
enum SpinApp {
|
||||
Templates(TemplatesCommand),
|
||||
}
|
||||
|
||||
impl SpinApp {
|
||||
/// The main entry point to Spin.
|
||||
pub async fn run(self) -> Result<(), Error> {
|
||||
match self {
|
||||
SpinApp::Templates(t) => t.run().await,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
//! Commands for the Spin CLI.
|
||||
|
||||
/// Commands for working with templates.
|
||||
pub mod templates;
|
|
@ -0,0 +1,104 @@
|
|||
use anyhow::Result;
|
||||
use comfy_table::Table;
|
||||
use fermyon_templates::TemplatesManager;
|
||||
use std::path::PathBuf;
|
||||
use structopt::StructOpt;
|
||||
|
||||
/// Commands for working with WebAssembly component templates.
|
||||
#[derive(StructOpt, Debug)]
|
||||
pub enum TemplatesCommand {
|
||||
/// Add a template repository locally.
|
||||
Add(Add),
|
||||
|
||||
/// List the template repositories configured.
|
||||
List(List),
|
||||
|
||||
/// Generate a new project from a template.
|
||||
Generate(Generate),
|
||||
}
|
||||
|
||||
impl TemplatesCommand {
|
||||
pub async fn run(self) -> Result<()> {
|
||||
match self {
|
||||
TemplatesCommand::Add(cmd) => cmd.run().await,
|
||||
TemplatesCommand::Generate(cmd) => cmd.run().await,
|
||||
TemplatesCommand::List(cmd) => cmd.run().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a templates repository from a remote git URL.
|
||||
#[derive(StructOpt, Debug)]
|
||||
pub struct Add {
|
||||
/// The name of the templates repository.
|
||||
#[structopt(long = "name")]
|
||||
pub name: String,
|
||||
|
||||
/// The URL of the templates git repository.
|
||||
/// The templates must be in a git repository in a "templates" directory.
|
||||
#[structopt(long = "git")]
|
||||
pub git: String,
|
||||
|
||||
/// The optional branch of the git repository.
|
||||
#[structopt(long = "branch")]
|
||||
pub branch: Option<String>,
|
||||
}
|
||||
|
||||
impl Add {
|
||||
pub async fn run(self) -> Result<()> {
|
||||
let tm = TemplatesManager::default().await?;
|
||||
Ok(tm.add_repo(&self.name, &self.git, self.branch.as_deref())?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a new project based on a template.
|
||||
#[derive(StructOpt, Debug)]
|
||||
pub struct Generate {
|
||||
/// The local templates repository.
|
||||
#[structopt(long = "repo")]
|
||||
pub repo: String,
|
||||
|
||||
/// The name of the template.
|
||||
#[structopt(long = "template")]
|
||||
pub template: String,
|
||||
|
||||
/// The destination where the template will be used.
|
||||
#[structopt(long = "path")]
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl Generate {
|
||||
pub async fn run(self) -> Result<()> {
|
||||
let tm = TemplatesManager::default().await?;
|
||||
tm.generate(&self.repo, &self.template, self.path).await
|
||||
}
|
||||
}
|
||||
|
||||
/// List existing templates.
|
||||
#[derive(StructOpt, Debug)]
|
||||
pub struct List {}
|
||||
|
||||
impl List {
|
||||
pub async fn run(self) -> Result<()> {
|
||||
let tm = TemplatesManager::default().await?;
|
||||
let res = tm.list().await?;
|
||||
let mut table = Table::new();
|
||||
table.set_header(vec!["Name", "Repository", "URL", "Branch"]);
|
||||
table.load_preset(comfy_table::presets::ASCII_BORDERS_ONLY_CONDENSED);
|
||||
|
||||
for repo in res {
|
||||
for t in repo.clone().templates {
|
||||
table.add_row(vec![
|
||||
t,
|
||||
repo.clone().name,
|
||||
repo.clone().git.unwrap_or("".to_string()),
|
||||
repo.clone().branch.unwrap_or("".to_string()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
println!("{}", table);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
pub mod commands;
|
Loading…
Reference in New Issue