Initial commit

Signed-off-by: Radu Matei <radu.matei@fermyon.com>
This commit is contained in:
Radu Matei 2021-11-01 19:13:46 -07:00
commit 308c266925
No known key found for this signature in database
GPG Key ID: 53A4E7168B7782C2
12 changed files with 3240 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

2797
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

29
Cargo.toml Normal file
View File

@ -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"

25
crates/engine/Cargo.toml Normal file
View File

@ -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" }

62
crates/engine/src/lib.rs Normal file
View File

@ -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,
}

View File

@ -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"

160
crates/templates/src/lib.rs Normal file
View File

@ -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()
}
}

3
readme.md Normal file
View File

@ -0,0 +1,3 @@
# Project Spin
Project Spin is the next version of the Fermyon runtime.

32
src/bin/spin.rs Normal file
View File

@ -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,
}
}
}

4
src/commands.rs Normal file
View File

@ -0,0 +1,4 @@
//! Commands for the Spin CLI.
/// Commands for working with templates.
pub mod templates;

104
src/commands/templates.rs Normal file
View File

@ -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(())
}
}

1
src/lib.rs Normal file
View File

@ -0,0 +1 @@
pub mod commands;