diff --git a/Cargo.lock b/Cargo.lock index 6ea72e55a..6222a34dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -354,6 +354,7 @@ dependencies = [ "cargo_metadata", "heck", "quill-plugin-format", + "toml", ] [[package]] diff --git a/docs/cargo_quill.md b/docs/cargo_quill.md new file mode 100644 index 000000000..9f56fa888 --- /dev/null +++ b/docs/cargo_quill.md @@ -0,0 +1,31 @@ +# cargo-quill +We have created an extension to cargo, that provides utilities for creating, +building, and testing feather plugins written in rust. This utility is called +cargo-quill. + +The sourcecode for cargo-quill can be found in /feather/quill/cargo-quill. +If there is a bug, or some missing feature, this is were to look. + +# How to install: +Install cargo-quill by running the following command from the main folder. +> cargo install --path quill/cargo-quill + +## How to create a new plugin +This command works similar to +> cargo-quill new 'name' + +## build +> cargo-quill build + +Builds the source and puts a '.plugin' file in the target folder. Just like the +regular cargo build you can choose the compilation to be in '--release' mode or not. +> cargo-quill build --release + +A command option you might not have considerd is that you can use '--native' if the +plugin should be compiled to native code instead of web assembly. + +> cargo-quill build --native + +You can also add some compression to the plugin. + +> cargo-quill build --release --compression (0 to 9) diff --git a/quill/cargo-quill/Cargo.toml b/quill/cargo-quill/Cargo.toml index c2c0ff565..dd17285fd 100644 --- a/quill/cargo-quill/Cargo.toml +++ b/quill/cargo-quill/Cargo.toml @@ -10,3 +10,4 @@ cargo_metadata = "0.12" anyhow = "1" argh = "0.1" heck = "0.3" +toml = "0.5.8" \ No newline at end of file diff --git a/quill/cargo-quill/src/build.rs b/quill/cargo-quill/src/build.rs new file mode 100644 index 000000000..dd439ed03 --- /dev/null +++ b/quill/cargo-quill/src/build.rs @@ -0,0 +1,168 @@ +/** + This file contains code for the build command. + 'cargo build --release? --native?' +*/ +use anyhow::{bail, Context}; +use argh::FromArgs; +use cargo_metadata::Metadata; +use heck::CamelCase; +use quill_plugin_format::{PluginFile, PluginMetadata, PluginTarget, Triple}; +use std::{ + fs, + path::PathBuf, + process::{Command, Stdio}, +}; + +use crate::{WASM_TARGET, WASM_TARGET_FEATURES}; + +#[derive(Debug, FromArgs)] +#[argh(subcommand, name = "build")] +/// Build a Quill plugin. +pub(crate) struct Build { + #[argh(switch)] + /// whether to build in release mode + release: bool, + #[argh(switch)] + /// whether to compile to a native shared library + /// instead of a WebAssembly module + native: bool, + #[argh(option, default = "6")] + /// the compression level to compress the plugin + /// binary. 0 is worst and 9 is best. + compression_level: u32, +} + +impl Build { + pub fn module_extension(&self) -> &'static str { + if !self.native { + "wasm" + } else if cfg!(windows) { + "dll" + } else if cfg!(target_vendor = "apple") { + "dylib" + } else { + // assume Linux / other Unix + "so" + } + } + + pub fn target_dir(&self, cargo_meta: &Metadata) -> PathBuf { + let mut target_dir = cargo_meta.target_directory.clone(); + if !self.native { + target_dir.push(WASM_TARGET); + } + + if self.release { + target_dir.push("release"); + } else { + target_dir.push("debug"); + } + + target_dir + } + + pub fn module_path(&self, cargo_meta: &Metadata, plugin_meta: &PluginMetadata) -> PathBuf { + let target_dir = self.target_dir(cargo_meta); + let module_filename = plugin_meta.identifier.replace("-", "_"); + + let module_extension = self.module_extension(); + let lib_prefix = if self.native && cfg!(unix) { "lib" } else { "" }; + + target_dir.join(format!( + "{}{}.{}", + lib_prefix, module_filename, module_extension + )) + } +} + +pub(crate) fn build(args: Build) -> anyhow::Result<()> { + let cargo_meta = get_cargo_metadata()?; + validate_cargo_metadata(&cargo_meta)?; + + let mut command = cargo_build_command(&args); + let status = command.spawn()?.wait()?; + if !status.success() { + bail!("build failed"); + } + + let meta = find_metadata(&cargo_meta, &args)?; + let module_path = args.module_path(&cargo_meta, &meta); + let module = fs::read(&module_path) + .with_context(|| format!("failed to read {}", module_path.display()))?; + + let file = PluginFile::new(module, meta.clone()); + let target_path = module_path + .parent() + .unwrap() + .join(format!("{}.plugin", meta.identifier)); + fs::write(&target_path, file.encode(args.compression_level))?; + + println!("Wrote plugin file to {}", target_path.display()); + Ok(()) +} + +fn cargo_build_command(args: &Build) -> Command { + let mut cmd = Command::new("cargo"); + cmd.arg("rustc"); + if args.release { + cmd.arg("--release"); + } + + if !args.native { + cmd.args(&["--target", WASM_TARGET]); + cmd.args(&["--", "-C", WASM_TARGET_FEATURES]); + } + + cmd.stdout(Stdio::piped()); + + cmd +} + +fn get_cargo_metadata() -> anyhow::Result { + let cmd = cargo_metadata::MetadataCommand::new(); + let cargo_meta = cmd.exec()?; + Ok(cargo_meta) +} + +fn validate_cargo_metadata(cargo_meta: &Metadata) -> anyhow::Result<()> { + let package = cargo_meta.root_package().context("missing root package")?; + if !package + .targets + .iter() + .any(|t| t.crate_types.contains(&"cdylib".to_owned())) + { + bail!("crate-type = [\"cdylib\"] must be set in the plugin Cargo.toml"); + } + + Ok(()) +} + +fn find_metadata(cargo_meta: &Metadata, args: &Build) -> anyhow::Result { + let package = cargo_meta.root_package().context("missing root package")?; + + let quill_dependency = package + .dependencies + .iter() + .find(|d| d.name == "quill") + .context("plugin does not depend on the `quill` crate")?; + + let target = if args.native { + PluginTarget::Native { + target_triple: Triple::host(), + } + } else { + PluginTarget::Wasm + }; + + let plugin_meta = PluginMetadata { + name: package.name.to_camel_case(), + identifier: package.name.clone(), + version: package.version.to_string(), + api_version: quill_dependency.req.to_string(), + description: package.description.clone(), + authors: package.authors.clone(), + target, + }; + + Ok(plugin_meta) +} diff --git a/quill/cargo-quill/src/main.rs b/quill/cargo-quill/src/main.rs index d2efec172..ea7013cd1 100644 --- a/quill/cargo-quill/src/main.rs +++ b/quill/cargo-quill/src/main.rs @@ -1,13 +1,8 @@ -use anyhow::{bail, Context}; use argh::FromArgs; -use cargo_metadata::Metadata; -use heck::CamelCase; -use quill_plugin_format::{PluginFile, PluginMetadata, PluginTarget, Triple}; -use std::{ - fs, - path::PathBuf, - process::{Command, Stdio}, -}; + +mod build; +mod new; +mod testing; const WASM_TARGET_FEATURES: &str = "target-feature=+bulk-memory,+mutable-globals,+simd128"; const WASM_TARGET: &str = "wasm32-wasi"; @@ -22,164 +17,16 @@ struct CargoQuill { #[derive(Debug, FromArgs)] #[argh(subcommand)] enum Subcommand { - Build(Build), -} - -#[derive(Debug, FromArgs)] -#[argh(subcommand, name = "build")] -/// Build a Quill plugin. -struct Build { - #[argh(switch)] - /// whether to build in release mode - release: bool, - #[argh(switch)] - /// whether to compile to a native shared library - /// instead of a WebAssembly module - native: bool, - #[argh(option, default = "6")] - /// the compression level to compress the plugin - /// binary. 0 is worst and 9 is best. - compression_level: u32, -} - -impl Build { - pub fn module_extension(&self) -> &'static str { - if !self.native { - "wasm" - } else if cfg!(windows) { - "dll" - } else if cfg!(target_vendor = "apple") { - "dylib" - } else { - // assume Linux / other Unix - "so" - } - } - - pub fn target_dir(&self, cargo_meta: &Metadata) -> PathBuf { - let mut target_dir = cargo_meta.target_directory.clone(); - if !self.native { - target_dir.push(WASM_TARGET); - } - - if self.release { - target_dir.push("release"); - } else { - target_dir.push("debug"); - } - - target_dir - } - - pub fn module_path(&self, cargo_meta: &Metadata, plugin_meta: &PluginMetadata) -> PathBuf { - let target_dir = self.target_dir(cargo_meta); - let module_filename = plugin_meta.identifier.replace("-", "_"); - - let module_extension = self.module_extension(); - let lib_prefix = if self.native && cfg!(unix) { "lib" } else { "" }; - - target_dir.join(format!( - "{}{}.{}", - lib_prefix, module_filename, module_extension - )) - } + Build(build::Build), + New(new::New), + Test(testing::Testing), } fn main() -> anyhow::Result<()> { let args: CargoQuill = argh::from_env(); match args.subcommand { - Subcommand::Build(args) => build(args), - } -} - -fn build(args: Build) -> anyhow::Result<()> { - let cargo_meta = get_cargo_metadata()?; - validate_cargo_metadata(&cargo_meta)?; - - let mut command = cargo_build_command(&args); - let status = command.spawn()?.wait()?; - if !status.success() { - bail!("build failed"); + Subcommand::Build(args) => build::build(args), + Subcommand::New(args) => new::new_command(args), + Subcommand::Test(args) => testing::test_command(args), } - - let meta = find_metadata(&cargo_meta, &args)?; - let module_path = args.module_path(&cargo_meta, &meta); - let module = fs::read(&module_path) - .with_context(|| format!("failed to read {}", module_path.display()))?; - - let file = PluginFile::new(module, meta.clone()); - let target_path = module_path - .parent() - .unwrap() - .join(format!("{}.plugin", meta.identifier)); - fs::write(&target_path, file.encode(args.compression_level))?; - - println!("Wrote plugin file to {}", target_path.display()); - Ok(()) -} - -fn cargo_build_command(args: &Build) -> Command { - let mut cmd = Command::new("cargo"); - cmd.arg("rustc"); - if args.release { - cmd.arg("--release"); - } - - if !args.native { - cmd.args(&["--target", WASM_TARGET]); - cmd.args(&["--", "-C", WASM_TARGET_FEATURES]); - } - - cmd.stdout(Stdio::piped()); - - cmd -} - -fn get_cargo_metadata() -> anyhow::Result { - let cmd = cargo_metadata::MetadataCommand::new(); - let cargo_meta = cmd.exec()?; - Ok(cargo_meta) -} - -fn validate_cargo_metadata(cargo_meta: &Metadata) -> anyhow::Result<()> { - let package = cargo_meta.root_package().context("missing root package")?; - if !package - .targets - .iter() - .any(|t| t.crate_types.contains(&"cdylib".to_owned())) - { - bail!("crate-type = [\"cdylib\"] must be set in the plugin Cargo.toml"); - } - - Ok(()) -} - -fn find_metadata(cargo_meta: &Metadata, args: &Build) -> anyhow::Result { - let package = cargo_meta.root_package().context("missing root package")?; - - let quill_dependency = package - .dependencies - .iter() - .find(|d| d.name == "quill") - .context("plugin does not depend on the `quill` crate")?; - - let target = if args.native { - PluginTarget::Native { - target_triple: Triple::host(), - } - } else { - PluginTarget::Wasm - }; - - let plugin_meta = PluginMetadata { - name: package.name.to_camel_case(), - identifier: package.name.clone(), - version: package.version.to_string(), - api_version: quill_dependency.req.to_string(), - description: package.description.clone(), - authors: package.authors.clone(), - target, - }; - - Ok(plugin_meta) } diff --git a/quill/cargo-quill/src/new.rs b/quill/cargo-quill/src/new.rs new file mode 100644 index 000000000..ec4e1ab34 --- /dev/null +++ b/quill/cargo-quill/src/new.rs @@ -0,0 +1,68 @@ +use anyhow::bail; +use argh::FromArgs; +use std::{ + fs, + process::{Command, Stdio}, +}; + +use std::io::Write; + +const QUILL_DEPENDENCY: &str = + "quill = {git = \"https://github.com/feather-rs/feather.git\", branch = \"main\"}\n"; +const QUILL_CDYNLIB: &str = "[lib]\ncrate-type = [\"cdylib\"]\n"; + +#[derive(Debug, FromArgs)] +#[argh(subcommand, name = "new")] +/// Build a Quill plugin. +pub struct New { + #[argh(positional)] + name: String, +} + +pub fn new_command(args: New) -> anyhow::Result<()> { + create_new_crate(args) +} + +fn create_new_crate(args: New) -> anyhow::Result<()> { + let cwd = match std::env::current_dir() { + Ok(x) => x, + Err(_) => bail!("Unable to get the current directory using std::env."), + }; + + if !run_cargo_new_command(args.name.as_str())? { + // Then cargo new failed + bail!("Cargo new failed for the given name {}", args.name); + } + + // The new library was created time to modify the cargo.toml + // file to contain quill as a dependency, and make it a cdylib + + let mut cargo_toml = cwd.clone(); + cargo_toml.push(args.name.as_str()); + cargo_toml.push("Cargo.toml"); + + assert!(cwd.exists()); + + let mut file = fs::OpenOptions::new() + .write(true) + .append(true) + .open(cargo_toml) + .unwrap(); + + write!(file, "{}", QUILL_DEPENDENCY)?; + write!(file, "{}", QUILL_CDYNLIB)?; + + Ok(()) +} + +fn run_cargo_new_command(name: &str) -> anyhow::Result { + let mut cmd = Command::new("cargo"); + cmd.arg("new"); + cmd.arg("--lib"); + cmd.arg(name); + + cmd.stdout(Stdio::piped()); + + let res = cmd.spawn()?.wait()?; + Ok(res.success()) +} diff --git a/quill/cargo-quill/src/testing.rs b/quill/cargo-quill/src/testing.rs new file mode 100644 index 000000000..eade48098 --- /dev/null +++ b/quill/cargo-quill/src/testing.rs @@ -0,0 +1,173 @@ +/* + This file contains code for handeling the command 'cargo-quill test'. + This command does different things depending on the current directory + and, depending on the arguments you pass to it. +*/ + +use std::{convert::{TryFrom, TryInto}, path::{Path, PathBuf}}; + +use anyhow::bail; +use argh::{FromArgs}; + + +#[derive(Debug, FromArgs)] +#[argh(subcommand, name = "test")] +/// Build a Quill plugin. +pub struct Testing { + #[argh(positional)] + args: Vec +} + + +#[derive(Debug)] +pub enum Directory { + ServerBinary(PathBuf), + ServerFolder{ + cargo_toml: PathBuf, + target_dir: PathBuf, + }, + PluginBinary(PathBuf), + PluginFolder { + cargo_toml: PathBuf, + target_dir: PathBuf, + exec_name: String, + } +} + +impl TryFrom<&Path> for Directory { + type Error = anyhow::Error; + + fn try_from(it: &Path) -> Result { + + if !it.exists() { + bail!("The path {:?} does not exist", it); + } + + match it.is_file() { + true if it.ends_with("Cargo.toml") => { + Self::try_load_from_cargo_toml(it) + }, + true if it.extension().map(|s| s.to_str()) == Some(Some(".plugin")) => Ok(Self::PluginBinary(it.to_owned())), + true => + // We assume it is a server binary. + Ok(Self::ServerBinary(it.to_owned())), + false if it.is_dir() => { + // Either a plugin dir or feather server directory + let cargo_toml = { + let mut path = it.to_owned(); + path.push("Cargo.toml"); + + if ! path.is_file() { + bail!("The folder {:?} does not contain a Cargo.toml file.",it); + } + path + }; + Self::try_load_from_cargo_toml(cargo_toml.as_path()) + } + _ => { + bail!("The path {:?} does not exist",it); + } + } + } + +} + + +impl Directory { + fn try_load_from_cargo_toml(cargo_toml: &Path) -> anyhow::Result { + + let cargo_metadata = { + let mut cmd = cargo_metadata::MetadataCommand::new(); + let cmd = cmd.manifest_path(&cargo_toml); + cmd.exec()? + }; + + let target_dir = { + cargo_metadata.target_directory.as_path() + }; + + + /* + If one of the package dependecies is quill + + */ + + + /* + If the name is Some("feather-server") or its a workspace with feather server in it, + then it is a server folder. Else we assume plugin. + */ + + let name = { + let root_package = cargo_metadata.root_package(); + root_package.map(|x| x.name.clone()) + }; + + let is_server_dir = { + + if name == Some("feather-server".to_owned()) { + true + } else { + cargo_metadata.workspace_members.iter().any(|x| { + let x = &cargo_metadata[x]; + x.name == "feather-server" + }) + } + }; + + if is_server_dir { + Ok(Self::ServerFolder{ + cargo_toml: cargo_toml.to_path_buf(), + target_dir: target_dir.to_path_buf(), + }) + } else { + Ok(Self::PluginFolder{ + cargo_toml: cargo_toml.to_path_buf(), + target_dir: target_dir.to_path_buf(), + exec_name: name.expect("The path: {} is assumed to be a plugin directory, however we are not able to determine the plugins name") + }) + } + } +} + + +// This function is invoked when using the cargo-quill test, command. +pub fn test_command(args: Testing) -> anyhow::Result<()> { + + let args = args.args.iter() + .map(|x| Path::new(x)) + .map(|x| x.try_into().unwrap()) + .collect::>(); + + if args.iter().all(|x| match x { + Directory::ServerBinary(_) => false, + Directory::ServerFolder { cargo_toml: _, target_dir: _ } => false, + Directory::PluginBinary(_) => true, + Directory::PluginFolder { cargo_toml: _, target_dir: _ , exec_name: _} => true, + }) { + bail!("At least one of the arguments after test, must be a path to a server binary or directory"); + } + + if args.iter().filter(|x| match x { + Directory::ServerBinary(_) => true, + Directory::ServerFolder { cargo_toml: _, target_dir: _ } => true, + Directory::PluginBinary(_) => false, + Directory::PluginFolder { cargo_toml: _, target_dir: _ , exec_name: _} => false, + }).count() > 1 { + bail!("Currently you can't run this command on multiple servers. This feature might be added in the future."); + } + + + /* + We no compile every each on + + */ + + for arg in &args { + println!("{:?}",arg); + } + + + + Ok(()) +} \ No newline at end of file