From a98c83a4e4d767c709ac1a8f304d0403e590c3be Mon Sep 17 00:00:00 2001 From: vobradovich Date: Wed, 9 Oct 2024 20:28:59 +0200 Subject: [PATCH 01/13] add idl-meta crate --- Cargo.lock | 12 +++++++- Cargo.toml | 2 ++ rs/Cargo.toml | 3 ++ rs/idl-gen/Cargo.toml | 8 ++--- rs/idl-gen/src/lib.rs | 4 +-- rs/idl-gen/src/meta.rs | 3 +- rs/idl-gen/src/type_names.rs | 8 ++--- rs/idl-gen/tests/generator.rs | 17 ++--------- rs/idl-meta/Cargo.toml | 12 ++++++++ rs/idl-meta/src/lib.rs | 49 +++++++++++++++++++++++++++++++ rs/src/lib.rs | 55 +++-------------------------------- 11 files changed, 95 insertions(+), 78 deletions(-) create mode 100644 rs/idl-meta/Cargo.toml create mode 100644 rs/idl-meta/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 73cf3230..4fda1ad5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6188,16 +6188,24 @@ name = "sails-idl-gen" version = "0.6.1" dependencies = [ "convert_case 0.6.0", + "gprimitives", "handlebars", "insta", + "sails-idl-meta", "sails-idl-parser", - "sails-rs", "scale-info", "serde", "serde_json", "thiserror", ] +[[package]] +name = "sails-idl-meta" +version = "0.6.1" +dependencies = [ + "scale-info", +] + [[package]] name = "sails-idl-parser" version = "0.6.1" @@ -6252,6 +6260,8 @@ dependencies = [ "hex", "mockall", "parity-scale-codec", + "sails-idl-gen", + "sails-idl-meta", "sails-macros", "scale-info", "spin", diff --git a/Cargo.toml b/Cargo.toml index 5e77740a..c556f9fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ members = [ "rs/cli", "rs/client-gen", "rs/idl-gen", + "rs/idl-meta", "rs/idl-parser", "rs/macros", "rs/macros/core", @@ -35,6 +36,7 @@ sails-idl-parser = { path = "rs/idl-parser" } sails-client-gen = { path = "rs/client-gen" } sails-rs = { path = "rs" } sails-idl-gen = { path = "rs/idl-gen" } +sails-idl-meta = { path = "rs/idl-meta" } # Renamed sails-rs for `proxy` demo sails-rename = { package = "sails-rs", path = "rs" } diff --git a/rs/Cargo.toml b/rs/Cargo.toml index 5e00a5b8..f598156c 100644 --- a/rs/Cargo.toml +++ b/rs/Cargo.toml @@ -19,6 +19,8 @@ hashbrown.workspace = true hex.workspace = true mockall = { workspace = true, optional = true } parity-scale-codec = { workspace = true, features = ["derive"] } +sails-idl-gen = { workspace = true, optional = true } +sails-idl-meta = { workspace = true } sails-macros = { workspace = true, optional = true } scale-info = { workspace = true, features = ["derive", "docs"] } spin.workspace = true @@ -38,5 +40,6 @@ ethexe = ["gstd?/ethexe"] gclient = ["dep:gclient"] gstd = ["dep:gstd", "dep:gear-core", "dep:sails-macros"] gtest = ["dep:gtest"] +idl-gen = ["dep:sails-idl-gen"] mockall = ["dep:mockall"] wasm-builder = ["dep:gwasm-builder"] diff --git a/rs/idl-gen/Cargo.toml b/rs/idl-gen/Cargo.toml index a6acc74d..bfde7306 100644 --- a/rs/idl-gen/Cargo.toml +++ b/rs/idl-gen/Cargo.toml @@ -10,14 +10,14 @@ repository.workspace = true [dependencies] convert-case.workspace = true +gprimitives.workspace = true handlebars.workspace = true -sails-rs.workspace = true -scale-info = { workspace = true, features = ["serde"] } -serde.workspace = true +sails-idl-meta.workspace = true +scale-info = { workspace = true, features = ["derive", "docs", "serde"] } +serde = { workspace = true, features = ["derive"] } serde-json.workspace = true thiserror.workspace = true [dev-dependencies] insta.workspace = true sails-idl-parser.workspace = true -scale-info = { workspace = true, features = ["derive"] } diff --git a/rs/idl-gen/src/lib.rs b/rs/idl-gen/src/lib.rs index df93cd35..ce1b79f9 100644 --- a/rs/idl-gen/src/lib.rs +++ b/rs/idl-gen/src/lib.rs @@ -16,7 +16,7 @@ const VARIANT_TEMPLATE: &str = include_str!("../hbs/variant.hbs"); pub mod program { use super::*; - use sails_rs::meta::ProgramMeta; + use sails_idl_meta::ProgramMeta; pub fn generate_idl(idl_writer: impl Write) -> Result<()> { render_idl( @@ -42,7 +42,7 @@ pub mod program { pub mod service { use super::*; - use sails_rs::meta::{AnyServiceMeta, ServiceMeta}; + use sails_idl_meta::{AnyServiceMeta, ServiceMeta}; pub fn generate_idl(idl_writer: impl Write) -> Result<()> { render_idl( diff --git a/rs/idl-gen/src/meta.rs b/rs/idl-gen/src/meta.rs index f7597a66..95c66ad2 100644 --- a/rs/idl-gen/src/meta.rs +++ b/rs/idl-gen/src/meta.rs @@ -22,7 +22,8 @@ use crate::{ errors::{Error, Result}, type_names, }; -use sails_rs::{meta::AnyServiceMeta, ActorId, CodeId, MessageId, NonZeroU256, H160, H256, U256}; +use gprimitives::*; +use sails_idl_meta::*; use scale_info::{ form::PortableForm, Field, MetaType, PortableRegistry, PortableType, Registry, TypeDef, Variant, }; diff --git a/rs/idl-gen/src/type_names.rs b/rs/idl-gen/src/type_names.rs index 8dfb05db..47fe4956 100644 --- a/rs/idl-gen/src/type_names.rs +++ b/rs/idl-gen/src/type_names.rs @@ -20,14 +20,12 @@ use crate::errors::{Error, Result}; use convert_case::{Case, Casing}; -use sails_rs::scale_info::{ +use core::num::{NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8}; +use gprimitives::*; +use scale_info::{ form::PortableForm, PortableType, Type, TypeDef, TypeDefArray, TypeDefPrimitive, TypeDefSequence, TypeDefTuple, TypeInfo, }; -use sails_rs::{ - ActorId, CodeId, MessageId, NonZeroU128, NonZeroU16, NonZeroU256, NonZeroU32, NonZeroU64, - NonZeroU8, H160, H256, U256, -}; use std::{ collections::{BTreeMap, HashMap}, rc::Rc, diff --git a/rs/idl-gen/tests/generator.rs b/rs/idl-gen/tests/generator.rs index 5ae14e65..24e26b84 100644 --- a/rs/idl-gen/tests/generator.rs +++ b/rs/idl-gen/tests/generator.rs @@ -1,10 +1,8 @@ +use gprimitives::*; use meta_params::*; use sails_idl_gen::{program, service}; -use sails_rs::{ - meta::{AnyServiceMeta, ProgramMeta, ServiceMeta as RtlServiceMeta}, - scale_info::{MetaType, StaticTypeInfo, TypeInfo}, - H256, U256, -}; +use sails_idl_meta::{AnyServiceMeta, ProgramMeta, ServiceMeta as RtlServiceMeta}; +use scale_info::{MetaType, StaticTypeInfo, TypeInfo}; use std::{collections::BTreeMap, result::Result as StdResult}; #[allow(dead_code)] @@ -13,7 +11,6 @@ mod types { /// GenericStruct docs #[derive(TypeInfo)] - #[scale_info(crate = sails_rs::scale_info)] pub struct GenericStruct { /// GenericStruct field `p1` pub p1: T, @@ -21,7 +18,6 @@ mod types { /// GenericConstStruct docs #[derive(TypeInfo)] - #[scale_info(crate = sails_rs::scale_info)] pub struct GenericConstStruct { /// GenericStruct field `field` field: [u8; N], @@ -30,7 +26,6 @@ mod types { /// GenericEnum docs /// with two lines #[derive(TypeInfo)] - #[scale_info(crate = sails_rs::scale_info)] pub enum GenericEnum { /// GenericEnum `Variant1` of type 'T1' Variant1(T1), @@ -40,11 +35,9 @@ mod types { /// TupleStruct docs #[derive(TypeInfo)] - #[scale_info(crate = sails_rs::scale_info)] pub struct TupleStruct(bool); #[derive(TypeInfo)] - #[scale_info(crate = sails_rs::scale_info)] pub enum ManyVariants { One, Two(u32), @@ -115,7 +108,6 @@ mod meta_params { #[allow(dead_code)] #[derive(TypeInfo)] -#[scale_info(crate = sails_rs::scale_info)] enum CommandsMeta { /// Some description DoThis(DoThisParams, String), @@ -134,7 +126,6 @@ enum BaseCommandsMeta { #[allow(dead_code)] #[derive(TypeInfo)] -#[scale_info(crate = sails_rs::scale_info)] enum QueriesMeta { /// This is a query This(ThisParams, StdResult<(String, u32), String>), @@ -152,7 +143,6 @@ enum BaseQueriesMeta { #[allow(dead_code)] #[derive(TypeInfo)] -#[scale_info(crate = sails_rs::scale_info)] enum EventsMeta { /// `This` Done ThisDone(u32), @@ -247,7 +237,6 @@ impl ProgramMeta for TestProgramWithEmptyCtorsMeta { #[allow(dead_code)] #[derive(TypeInfo)] -#[scale_info(crate = sails_rs::scale_info)] enum NonEmptyCtorsMeta { /// This is New constructor New(NoParams), diff --git a/rs/idl-meta/Cargo.toml b/rs/idl-meta/Cargo.toml new file mode 100644 index 00000000..e943ff30 --- /dev/null +++ b/rs/idl-meta/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "sails-idl-meta" +description = "IDL meta information for the Sails framework" +documentation = "https://docs.rs/sails-idl-gen" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +scale-info = { workspace = true } diff --git a/rs/idl-meta/src/lib.rs b/rs/idl-meta/src/lib.rs new file mode 100644 index 00000000..7f5edb2b --- /dev/null +++ b/rs/idl-meta/src/lib.rs @@ -0,0 +1,49 @@ +#![no_std] + +use scale_info::{prelude::vec::Vec, MetaType}; + +pub trait ServiceMeta { + fn commands() -> MetaType; + fn queries() -> MetaType; + fn events() -> MetaType; + fn base_services() -> impl Iterator; +} + +pub struct AnyServiceMeta { + commands: MetaType, + queries: MetaType, + events: MetaType, + base_services: Vec, +} + +impl AnyServiceMeta { + pub fn new() -> Self { + Self { + commands: S::commands(), + queries: S::queries(), + events: S::events(), + base_services: S::base_services().collect(), + } + } + + pub fn commands(&self) -> &MetaType { + &self.commands + } + + pub fn queries(&self) -> &MetaType { + &self.queries + } + + pub fn events(&self) -> &MetaType { + &self.events + } + + pub fn base_services(&self) -> impl Iterator { + self.base_services.iter() + } +} + +pub trait ProgramMeta { + fn constructors() -> MetaType; + fn services() -> impl Iterator; +} diff --git a/rs/src/lib.rs b/rs/src/lib.rs index 486cc9a5..c122ce7b 100644 --- a/rs/src/lib.rs +++ b/rs/src/lib.rs @@ -9,6 +9,10 @@ extern crate std; pub use gwasm_builder::build as build_wasm; pub use hex::{self}; pub use prelude::*; +#[cfg(feature = "idl-gen")] +#[cfg(not(target_arch = "wasm32"))] +pub use sails_idl_gen::{generate_idl, generate_idl_to_file}; +pub use sails_idl_meta::{self as meta}; pub use spin::{self}; pub mod calls; @@ -28,54 +32,3 @@ pub mod gtest; pub mod mockall; pub mod prelude; mod types; - -pub mod meta { - use crate::Vec; - use scale_info::MetaType; - - pub trait ServiceMeta { - fn commands() -> MetaType; - fn queries() -> MetaType; - fn events() -> MetaType; - fn base_services() -> impl Iterator; - } - - pub struct AnyServiceMeta { - commands: MetaType, - queries: MetaType, - events: MetaType, - base_services: Vec, - } - - impl AnyServiceMeta { - pub fn new() -> Self { - Self { - commands: S::commands(), - queries: S::queries(), - events: S::events(), - base_services: S::base_services().collect(), - } - } - - pub fn commands(&self) -> &MetaType { - &self.commands - } - - pub fn queries(&self) -> &MetaType { - &self.queries - } - - pub fn events(&self) -> &MetaType { - &self.events - } - - pub fn base_services(&self) -> impl Iterator { - self.base_services.iter() - } - } - - pub trait ProgramMeta { - fn constructors() -> MetaType; - fn services() -> impl Iterator; - } -} From fe0223f8e73e00edf47934d14a40a44d1b461d09 Mon Sep 17 00:00:00 2001 From: vobradovich Date: Thu, 10 Oct 2024 16:32:48 +0200 Subject: [PATCH 02/13] wip: CLI generate IDL from Cargo manifest --- Cargo.lock | 2 + Cargo.toml | 166 ++++++++++++++++---------------- rs/cli/Cargo.toml | 2 + rs/cli/src/idlgen.rs | 219 +++++++++++++++++++++++++++++++++++++++++++ rs/cli/src/lib.rs | 1 + rs/cli/src/main.rs | 17 +++- 6 files changed, 324 insertions(+), 83 deletions(-) create mode 100644 rs/cli/src/idlgen.rs diff --git a/Cargo.lock b/Cargo.lock index 4fda1ad5..67948aca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6167,8 +6167,10 @@ version = "0.6.1" dependencies = [ "anyhow", "cargo-generate", + "cargo_metadata", "clap", "sails-client-gen", + "toml_edit 0.22.22", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c556f9fe..1f563d82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,82 +1,84 @@ -[workspace.package] -version = "0.6.1" -authors = ["Gear Technologies"] -edition = "2021" -license = "GPL-3.0" -repository = "https://github.com/gear-tech/sails" - -[workspace] -resolver = "2" -members = [ - "examples/demo/app", - "examples/demo/client", - "examples/demo/walker", - "examples/no-svcs-prog/app", - "examples/no-svcs-prog/wasm", - "examples/proxy/", - "examples/rmrk/catalog/app", - "examples/rmrk/catalog/wasm", - "examples/rmrk/resource/app", - "examples/rmrk/resource/wasm", - "rs", - "rs/cli", - "rs/client-gen", - "rs/idl-gen", - "rs/idl-meta", - "rs/idl-parser", - "rs/macros", - "rs/macros/core", -] - -[workspace.dependencies] -# The order matches with publishing order. -sails-macros-core = { path = "rs/macros/core" } -sails-macros = { path = "rs/macros" } -sails-idl-parser = { path = "rs/idl-parser" } -sails-client-gen = { path = "rs/client-gen" } -sails-rs = { path = "rs" } -sails-idl-gen = { path = "rs/idl-gen" } -sails-idl-meta = { path = "rs/idl-meta" } -# Renamed sails-rs for `proxy` demo -sails-rename = { package = "sails-rs", path = "rs" } - -# Gear deps -gclient = "=1.6.2" -gear-core = { version = "=1.6.2", default-features = false } -gear-core-errors = "=1.6.2" -gprimitives = { version = "=1.6.2", features = ["codec"] } -gstd = "=1.6.2" -gtest = "=1.6.2" -gwasm-builder = { version = "=1.6.2", package = "gear-wasm-builder" } - -# Other deps in alphabetical order -anyhow = "1" -cargo-generate = "0.21" -clap = "4.5" -convert-case = { package = "convert_case", version = "0.6" } -futures = { version = "0.3", default-features = false } -genco = "0.17" -git-download = "0.1" -handlebars = "4.4" -hashbrown = "0.14" -hex = { version = "0.4", default-features = false } -insta = "1.40" -itertools = "0.13" -lalrpop = { version = "0.20", default-features = false } -lalrpop-util = "0.20" -logos = "0.13" -mockall = "0.12" -parity-scale-codec = { version = "3.6", default-features = false } -prettyplease = "0.2" -proc-macro-error = "1.0" -proc-macro2 = { version = "1", default-features = false } -quote = "1.0" -scale-info = { version = "2.11", default-features = false } -serde = "1.0" -serde-json = { package = "serde_json", version = "1.0" } -spin = { version = "0.9", default-features = false, features = ["spin_mutex"] } -syn = "2.0" -thiserror = "1.0" -thiserror-no-std = "2.0" -tokio = "1.40" -trybuild = "1" +[workspace.package] +version = "0.6.1" +authors = ["Gear Technologies"] +edition = "2021" +license = "GPL-3.0" +repository = "https://github.com/gear-tech/sails" + +[workspace] +resolver = "2" +members = [ + "examples/demo/app", + "examples/demo/client", + "examples/demo/walker", + "examples/no-svcs-prog/app", + "examples/no-svcs-prog/wasm", + "examples/proxy/", + "examples/rmrk/catalog/app", + "examples/rmrk/catalog/wasm", + "examples/rmrk/resource/app", + "examples/rmrk/resource/wasm", + "rs", + "rs/cli", + "rs/client-gen", + "rs/idl-gen", + "rs/idl-meta", + "rs/idl-parser", + "rs/macros", + "rs/macros/core", +] + +[workspace.dependencies] +# The order matches with publishing order. +sails-macros-core = { path = "rs/macros/core" } +sails-macros = { path = "rs/macros" } +sails-idl-parser = { path = "rs/idl-parser" } +sails-client-gen = { path = "rs/client-gen" } +sails-rs = { path = "rs" } +sails-idl-gen = { path = "rs/idl-gen" } +sails-idl-meta = { path = "rs/idl-meta" } +# Renamed sails-rs for `proxy` demo +sails-rename = { package = "sails-rs", path = "rs" } + +# Gear deps +gclient = "=1.6.2" +gear-core = { version = "=1.6.2", default-features = false } +gear-core-errors = "=1.6.2" +gprimitives = { version = "=1.6.2", features = ["codec"] } +gstd = "=1.6.2" +gtest = "=1.6.2" +gwasm-builder = { version = "=1.6.2", package = "gear-wasm-builder" } + +# Other deps in alphabetical order +anyhow = "1" +cargo-generate = "0.21" +cargo_metadata = "0.18" +clap = "4.5" +convert-case = { package = "convert_case", version = "0.6" } +futures = { version = "0.3", default-features = false } +genco = "0.17" +git-download = "0.1" +handlebars = "4.4" +hashbrown = "0.14" +hex = { version = "0.4", default-features = false } +insta = "1.40" +itertools = "0.13" +lalrpop = { version = "0.20", default-features = false } +lalrpop-util = "0.20" +logos = "0.13" +mockall = "0.12" +parity-scale-codec = { version = "3.6", default-features = false } +prettyplease = "0.2" +proc-macro-error = "1.0" +proc-macro2 = { version = "1", default-features = false } +quote = "1.0" +scale-info = { version = "2.11", default-features = false } +serde = "1.0" +serde-json = { package = "serde_json", version = "1.0" } +spin = { version = "0.9", default-features = false, features = ["spin_mutex"] } +syn = "2.0" +thiserror = "1.0" +thiserror-no-std = "2.0" +toml_edit = "0.22" +tokio = "1.40" +trybuild = "1" diff --git a/rs/cli/Cargo.toml b/rs/cli/Cargo.toml index 5bb7e5f8..015e330b 100644 --- a/rs/cli/Cargo.toml +++ b/rs/cli/Cargo.toml @@ -14,5 +14,7 @@ path = "src/main.rs" [dependencies] anyhow.workspace = true cargo-generate.workspace = true +cargo_metadata.workspace = true clap = { workspace = true, features = ["derive"] } sails-client-gen.workspace = true +toml_edit.workspace = true diff --git a/rs/cli/src/idlgen.rs b/rs/cli/src/idlgen.rs new file mode 100644 index 00000000..3b3b7595 --- /dev/null +++ b/rs/cli/src/idlgen.rs @@ -0,0 +1,219 @@ +use std::{ + fs, + path::{Path, PathBuf}, + process::{exit, Command, ExitStatus}, +}; + +use anyhow::Context; +use cargo_metadata::Package; + +pub struct CrateIdlGenerator { + manifest_path: PathBuf, + target_dir: Option, +} + +impl CrateIdlGenerator { + pub fn new(manifest_path: PathBuf, target_dir: Option) -> Self { + Self { + manifest_path, + target_dir, + } + } + + pub fn generate(self) -> anyhow::Result<()> { + let metadata = cargo_metadata::MetadataCommand::new() + .manifest_path(&self.manifest_path) + //.no_deps() + .exec()?; + + let sails_package = metadata + .packages + .iter() + .find(|p| p.name == "sails-rs") + .unwrap(); + + //self.cargo_doc() + // print!("{:?}", sails_package); + + let program_package = metadata + .packages + .iter() + .find(|p| &p.id == metadata.workspace_default_members.first().unwrap()) + .unwrap(); + // print!("{:?}", program_package); + let crate_name = get_crate_name(program_package); + let workspace_root = metadata.workspace_root; + + let crate_path = workspace_root.join(&crate_name); + let src_path = crate_path.join("src"); + fs::create_dir_all(&src_path)?; + + let manifest_path = crate_path.join("Cargo.toml"); + write_file(&manifest_path, gen_toml(program_package, sails_package))?; + let main_rs_path = src_path.join("main.rs"); + + let target_dir = self + .target_dir + .unwrap_or_else(|| metadata.target_directory.clone().into_std_path_buf()); + let out_file = target_dir.join(format!("{}.idl", program_package.name)); + write_file( + main_rs_path, + gen_main_rs("proxy::ProxyProgram", out_file.as_path()), + )?; + + workspace_members_add(&workspace_root.as_std_path(), &crate_name)?; + + _ = cargo_run_bin(&manifest_path, &crate_name)?; + + workspace_members_remove(&workspace_root.as_std_path(), &crate_name)?; + + fs::remove_dir_all(crate_path)?; + + Ok(()) + } + + fn cargo_doc(&self) -> anyhow::Result<()> { + let cargo_path = std::env::var("CARGO").unwrap_or("cargo".into()); + + let mut cmd = Command::new(cargo_path); + cmd.env("RUSTC_BOOTSTRAP", "1") + .env( + "RUSTDOCFLAGS", + "-Z unstable-options --document-private-items --document-hidden-items --output-format=json --cap-lints=allow", + ) + .stdout(std::process::Stdio::null()) // Don't pollute output + .arg("doc") + .arg("--manifest-path") + .arg(self.manifest_path.to_str().unwrap()) + .arg("--no-deps"); + + let status = match cmd + .status() + .context("Failed to execute `cargo doc` command") + { + Ok(status) => status, + Err(e) => { + // let _ = display_error(&e, &mut shell); + exit(1); + } + }; + + exit(status.code().unwrap_or(1)); + } +} + +fn get_crate_name(program_package: &Package) -> String { + format!("{}-idl-gen", program_package.name) +} + +fn gen_toml(program_package: &Package, sails_package: &Package) -> String { + let mut manifest = toml_edit::DocumentMut::new(); + manifest["package"] = toml_edit::Item::Table(toml_edit::Table::new()); + manifest["package"]["name"] = toml_edit::value(get_crate_name(program_package)); + manifest["package"]["version"] = toml_edit::value("0.1.0"); + manifest["package"]["edition"] = toml_edit::value(program_package.edition.as_str()); + + let mut dep_table = toml_edit::Table::default(); + let mut package_table = toml_edit::InlineTable::new(); + let manifets_dir = program_package.manifest_path.parent().unwrap(); + package_table.insert("path", manifets_dir.as_str().into()); + dep_table[&program_package.name] = toml_edit::value(package_table); + + let mut features = toml_edit::Array::default(); + features.push("idl-gen"); + let mut sails_table = toml_edit::InlineTable::new(); + let manifets_dir = sails_package.manifest_path.parent().unwrap(); + sails_table.insert("path", manifets_dir.as_str().into()); + sails_table.insert("features", features.into()); + dep_table[&sails_package.name] = toml_edit::value(sails_table); + + manifest["dependencies"] = toml_edit::Item::Table(dep_table); + + let mut bin = toml_edit::Table::new(); + bin["name"] = toml_edit::value(get_crate_name(program_package)); + bin["path"] = toml_edit::value("src/main.rs"); + manifest["bin"] + .or_insert(toml_edit::Item::ArrayOfTables( + toml_edit::ArrayOfTables::new(), + )) + .as_array_of_tables_mut() + .expect("bin is an array of tables") + .push(bin); + + manifest.to_string() +} + +fn gen_main_rs(program_struct_path: &str, out_file: &Path) -> String { + format!( + " +fn main() {{ + sails_rs::generate_idl_to_file::<{}>( + std::path::PathBuf::from(r\"{}\") + ) + .unwrap(); +}}", + program_struct_path, + out_file.to_str().unwrap(), + ) +} + +fn write_file, C: AsRef<[u8]>>(path: P, contents: C) -> anyhow::Result<()> { + let path = path.as_ref(); + fs::write(path, contents.as_ref()) + .with_context(|| format!("failed to write `{}`", path.display())) +} + +fn workspace_members_add(path: &Path, name: &str) -> anyhow::Result<()> { + println!("adding member to workspace: {:?}", name); + + let workspace_cargo_toml = path.join("Cargo.toml"); + let toml = fs::read_to_string(&workspace_cargo_toml).context("failed to read Cargo.toml")?; + let mut doc = toml + .parse::() + .context("failed to parse Cargo.toml")?; + let members = + doc["workspace"]["members"].or_insert(toml_edit::value(toml_edit::Array::default())); + members.as_array_mut().unwrap().push(name); + write_file(&workspace_cargo_toml, doc.to_string()) +} + +fn workspace_members_remove(path: &Path, name: &str) -> anyhow::Result<()> { + println!("removing member from workspace: {:?}", name); + + let workspace_cargo_toml = path.join("Cargo.toml"); + let toml = fs::read_to_string(&workspace_cargo_toml).context("failed to read Cargo.toml")?; + let mut doc = toml + .parse::() + .context("failed to parse Cargo.toml")?; + let members = doc["workspace"]["members"].as_array_mut(); + + if let Some(members) = members { + let position = members.iter().position(|m| m.as_str() == Some(name)); + if let Some(position) = position { + members.remove(position); + write_file(&workspace_cargo_toml, doc.to_string()) + } else { + Ok(()) + } + } else { + Ok(()) + } +} + +fn cargo_run_bin( + manifest_path: &cargo_metadata::camino::Utf8Path, + bin_name: &str, +) -> anyhow::Result { + let cargo_path = std::env::var("CARGO").unwrap_or("cargo".into()); + + let args = vec![ + "run", + "--manifest-path", + manifest_path.as_str(), + "--bin", + bin_name, + ]; + let mut cmd = Command::new(cargo_path); + cmd.args(args); + cmd.status().context("Failed to execute `cargo` command") +} diff --git a/rs/cli/src/lib.rs b/rs/cli/src/lib.rs index e2c04eaa..36af1f5d 100644 --- a/rs/cli/src/lib.rs +++ b/rs/cli/src/lib.rs @@ -1 +1,2 @@ +pub mod idlgen; pub mod program; diff --git a/rs/cli/src/main.rs b/rs/cli/src/main.rs index 3bf82a41..3c1a8dd8 100644 --- a/rs/cli/src/main.rs +++ b/rs/cli/src/main.rs @@ -1,5 +1,5 @@ use clap::{Parser, Subcommand}; -use sails_cli::program::ProgramGenerator; +use sails_cli::{idlgen::CrateIdlGenerator, program::ProgramGenerator}; use sails_client_gen::ClientGenerator; use std::{error::Error, path::PathBuf}; @@ -50,6 +50,17 @@ enum SailsCommands { #[arg(long)] no_derive_traits: bool, }, + + /// Generate IDL from Cargo manifest + #[command(name = "idl")] + IdlGen { + /// Path to the crate with program + #[arg(value_hint = clap::ValueHint::FilePath)] + manifest_path: PathBuf, + /// Directory for all generated artifacts + #[arg(long, value_hint = clap::ValueHint::DirPath)] + target_dir: Option, + }, } /// Parse a single key-value pair @@ -106,6 +117,10 @@ fn main() -> Result<(), i32> { let out_path = out_path.unwrap_or_else(|| idl_path.with_extension("rs")); client_gen.generate_to(out_path) } + SailsCommands::IdlGen { + manifest_path, + target_dir: target_dit, + } => CrateIdlGenerator::new(manifest_path, target_dit).generate(), }; if let Err(e) = result { From 44e710c65f4dde5cc37e36909789d4cd7f7c7b9b Mon Sep 17 00:00:00 2001 From: vobradovich Date: Fri, 11 Oct 2024 13:32:33 +0200 Subject: [PATCH 03/13] wip: idl gen --- Cargo.toml | 1 + rs/cli/Cargo.toml | 2 + rs/cli/src/idlgen.rs | 138 +++++++++++++++++++++++++++---------------- 3 files changed, 90 insertions(+), 51 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1f563d82..73f5587b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ parity-scale-codec = { version = "3.6", default-features = false } prettyplease = "0.2" proc-macro-error = "1.0" proc-macro2 = { version = "1", default-features = false } +rustdoc-types = { version = "0.29.1" } # replace with official `rustdoc_json_types` after stable release quote = "1.0" scale-info = { version = "2.11", default-features = false } serde = "1.0" diff --git a/rs/cli/Cargo.toml b/rs/cli/Cargo.toml index 015e330b..dc0de311 100644 --- a/rs/cli/Cargo.toml +++ b/rs/cli/Cargo.toml @@ -16,5 +16,7 @@ anyhow.workspace = true cargo-generate.workspace = true cargo_metadata.workspace = true clap = { workspace = true, features = ["derive"] } +rustdoc-types.workspace = true sails-client-gen.workspace = true +serde-json.workspace = true toml_edit.workspace = true diff --git a/rs/cli/src/idlgen.rs b/rs/cli/src/idlgen.rs index 3b3b7595..61bf0a37 100644 --- a/rs/cli/src/idlgen.rs +++ b/rs/cli/src/idlgen.rs @@ -1,29 +1,30 @@ use std::{ fs, path::{Path, PathBuf}, - process::{exit, Command, ExitStatus}, + process::{Command, ExitStatus}, }; use anyhow::Context; -use cargo_metadata::Package; +use cargo_metadata::{camino::*, Package}; pub struct CrateIdlGenerator { - manifest_path: PathBuf, + manifest_path: Utf8PathBuf, target_dir: Option, } impl CrateIdlGenerator { pub fn new(manifest_path: PathBuf, target_dir: Option) -> Self { Self { - manifest_path, + manifest_path: Utf8PathBuf::from_path_buf(manifest_path).unwrap(), target_dir, } } pub fn generate(self) -> anyhow::Result<()> { + println!("...reading metadata: {}", &self.manifest_path); + // Get metadata with deps let metadata = cargo_metadata::MetadataCommand::new() .manifest_path(&self.manifest_path) - //.no_deps() .exec()?; let sails_package = metadata @@ -35,26 +36,65 @@ impl CrateIdlGenerator { //self.cargo_doc() // print!("{:?}", sails_package); - let program_package = metadata - .packages - .iter() - .find(|p| &p.id == metadata.workspace_default_members.first().unwrap()) - .unwrap(); + let program_package = metadata.root_package().unwrap(); // print!("{:?}", program_package); + + let target_dir = self + .target_dir + .map(|p| p.canonicalize().ok()) + .flatten() + .map(Utf8PathBuf::from_path_buf) + .map(|t| t.ok()) + .flatten() + .unwrap_or_else(|| metadata.target_directory.clone()); + + _ = cargo_doc(&self.manifest_path, &target_dir)?; + let docs_path = target_dir + .join("doc") + .join(format!("{}.json", program_package.name)); + + println!("...reading docs: {:?}", docs_path); + let json_string = std::fs::read_to_string(docs_path)?; + let doc_crate: rustdoc_types::Crate = serde_json::from_str(&json_string)?; + let program_meta = doc_crate + .paths + .iter() + .find(|p| p.1.path == vec!["sails_idl_meta", "ProgramMeta"]) + .context("failed to find sails_rs::ProgramMeta implemetation")?; + // println!("{:?}", program_meta); + + let program_struct_path = doc_crate.index.iter().filter_map(|idx| { + if let rustdoc_types::ItemEnum::Impl(item) = &idx.1.inner { + if let Some(tp) = &item.trait_ { + if &tp.id == program_meta.0 { + if let rustdoc_types::Type::ResolvedPath(path) = &item.for_ { + Some(path) + } else { + None + } + } else { + None + } + } else { + None + } + } else { + None + } + }); + println!("{:?}", program_struct_path); + let crate_name = get_crate_name(program_package); - let workspace_root = metadata.workspace_root; + let workspace_root = &metadata.workspace_root; let crate_path = workspace_root.join(&crate_name); let src_path = crate_path.join("src"); fs::create_dir_all(&src_path)?; - let manifest_path = crate_path.join("Cargo.toml"); - write_file(&manifest_path, gen_toml(program_package, sails_package))?; + let gen_manifest_path = crate_path.join("Cargo.toml"); + write_file(&gen_manifest_path, gen_toml(program_package, sails_package))?; let main_rs_path = src_path.join("main.rs"); - let target_dir = self - .target_dir - .unwrap_or_else(|| metadata.target_directory.clone().into_std_path_buf()); let out_file = target_dir.join(format!("{}.idl", program_package.name)); write_file( main_rs_path, @@ -63,7 +103,7 @@ impl CrateIdlGenerator { workspace_members_add(&workspace_root.as_std_path(), &crate_name)?; - _ = cargo_run_bin(&manifest_path, &crate_name)?; + _ = cargo_run_bin(&gen_manifest_path, &crate_name)?; workspace_members_remove(&workspace_root.as_std_path(), &crate_name)?; @@ -71,35 +111,6 @@ impl CrateIdlGenerator { Ok(()) } - - fn cargo_doc(&self) -> anyhow::Result<()> { - let cargo_path = std::env::var("CARGO").unwrap_or("cargo".into()); - - let mut cmd = Command::new(cargo_path); - cmd.env("RUSTC_BOOTSTRAP", "1") - .env( - "RUSTDOCFLAGS", - "-Z unstable-options --document-private-items --document-hidden-items --output-format=json --cap-lints=allow", - ) - .stdout(std::process::Stdio::null()) // Don't pollute output - .arg("doc") - .arg("--manifest-path") - .arg(self.manifest_path.to_str().unwrap()) - .arg("--no-deps"); - - let status = match cmd - .status() - .context("Failed to execute `cargo doc` command") - { - Ok(status) => status, - Err(e) => { - // let _ = display_error(&e, &mut shell); - exit(1); - } - }; - - exit(status.code().unwrap_or(1)); - } } fn get_crate_name(program_package: &Package) -> String { @@ -143,7 +154,7 @@ fn gen_toml(program_package: &Package, sails_package: &Package) -> String { manifest.to_string() } -fn gen_main_rs(program_struct_path: &str, out_file: &Path) -> String { +fn gen_main_rs(program_struct_path: &str, out_file: &cargo_metadata::camino::Utf8Path) -> String { format!( " fn main() {{ @@ -153,7 +164,7 @@ fn main() {{ .unwrap(); }}", program_struct_path, - out_file.to_str().unwrap(), + out_file.as_str(), ) } @@ -164,7 +175,7 @@ fn write_file, C: AsRef<[u8]>>(path: P, contents: C) -> anyhow::R } fn workspace_members_add(path: &Path, name: &str) -> anyhow::Result<()> { - println!("adding member to workspace: {:?}", name); + println!("...adding member to workspace: {:?}", name); let workspace_cargo_toml = path.join("Cargo.toml"); let toml = fs::read_to_string(&workspace_cargo_toml).context("failed to read Cargo.toml")?; @@ -178,7 +189,7 @@ fn workspace_members_add(path: &Path, name: &str) -> anyhow::Result<()> { } fn workspace_members_remove(path: &Path, name: &str) -> anyhow::Result<()> { - println!("removing member from workspace: {:?}", name); + println!("...removing member from workspace: {:?}", name); let workspace_cargo_toml = path.join("Cargo.toml"); let toml = fs::read_to_string(&workspace_cargo_toml).context("failed to read Cargo.toml")?; @@ -200,6 +211,30 @@ fn workspace_members_remove(path: &Path, name: &str) -> anyhow::Result<()> { } } +fn cargo_doc( + manifest_path: &cargo_metadata::camino::Utf8Path, + target_dir: &cargo_metadata::camino::Utf8Path, +) -> anyhow::Result { + let cargo_path = std::env::var("CARGO").unwrap_or("cargo".into()); + + let mut cmd = Command::new(cargo_path); + cmd.env("RUSTC_BOOTSTRAP", "1") + .env( + "RUSTDOCFLAGS", + "-Z unstable-options --output-format=json --cap-lints=allow", + ) + .stdout(std::process::Stdio::null()) // Don't pollute output + .arg("doc") + .arg("--manifest-path") + .arg(manifest_path.as_str()) + .arg("--target-dir") + .arg(target_dir.as_str()) + .arg("--no-deps"); + + cmd.status() + .context("Failed to execute `cargo doc` command") +} + fn cargo_run_bin( manifest_path: &cargo_metadata::camino::Utf8Path, bin_name: &str, @@ -214,6 +249,7 @@ fn cargo_run_bin( bin_name, ]; let mut cmd = Command::new(cargo_path); - cmd.args(args); + cmd.stdout(std::process::Stdio::null()) // Don't pollute output + .args(args); cmd.status().context("Failed to execute `cargo` command") } From 38952010e7a8e6b3878a751b3b5f6f5773c7ff15 Mon Sep 17 00:00:00 2001 From: vobradovich Date: Wed, 16 Oct 2024 15:42:05 +0200 Subject: [PATCH 04/13] wip: idl gen with program path --- Cargo.lock | 11 ++++ rs/cli/src/idlgen.rs | 152 ++++++++++++++++++++----------------------- 2 files changed, 80 insertions(+), 83 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 67948aca..70d7e679 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5973,6 +5973,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rustdoc-types" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df53bab0198f33fc88c110aaabdb2df2cfee4b8a72aeaf942088e12bb305142" +dependencies = [ + "serde", +] + [[package]] name = "rustix" version = "0.36.17" @@ -6169,7 +6178,9 @@ dependencies = [ "cargo-generate", "cargo_metadata", "clap", + "rustdoc-types", "sails-client-gen", + "serde_json", "toml_edit 0.22.22", ] diff --git a/rs/cli/src/idlgen.rs b/rs/cli/src/idlgen.rs index 61bf0a37..4723e8bd 100644 --- a/rs/cli/src/idlgen.rs +++ b/rs/cli/src/idlgen.rs @@ -37,6 +37,7 @@ impl CrateIdlGenerator { // print!("{:?}", sails_package); let program_package = metadata.root_package().unwrap(); + let program_package_file_name = program_package.name.to_lowercase().replace('-', "_"); // print!("{:?}", program_package); let target_dir = self @@ -51,7 +52,7 @@ impl CrateIdlGenerator { _ = cargo_doc(&self.manifest_path, &target_dir)?; let docs_path = target_dir .join("doc") - .join(format!("{}.json", program_package.name)); + .join(format!("{}.json", &program_package_file_name)); println!("...reading docs: {:?}", docs_path); let json_string = std::fs::read_to_string(docs_path)?; @@ -59,60 +60,79 @@ impl CrateIdlGenerator { let program_meta = doc_crate .paths .iter() - .find(|p| p.1.path == vec!["sails_idl_meta", "ProgramMeta"]) - .context("failed to find sails_rs::ProgramMeta implemetation")?; + .find(|p| p.1.path == META_PATH_V2) + .context("failed to find `sails_rs::meta::ProgramMeta` definition in dependencies")?; // println!("{:?}", program_meta); - let program_struct_path = doc_crate.index.iter().filter_map(|idx| { - if let rustdoc_types::ItemEnum::Impl(item) = &idx.1.inner { - if let Some(tp) = &item.trait_ { - if &tp.id == program_meta.0 { - if let rustdoc_types::Type::ResolvedPath(path) = &item.for_ { - Some(path) - } else { - None - } - } else { - None - } - } else { - None - } - } else { - None - } - }); - println!("{:?}", program_struct_path); + let program_struct_path = find_program_path(&doc_crate, program_meta) + .context("failed to find `sails_rs::meta::ProgramMeta` implemetation")?; + let program_struct = doc_crate + .paths + .get(&program_struct_path.id) + .context("failed to find Program path")?; + let program_struct_path = program_struct.path.join("::"); + println!("...found Program implemetation: {:?}", program_struct_path); let crate_name = get_crate_name(program_package); - let workspace_root = &metadata.workspace_root; - let crate_path = workspace_root.join(&crate_name); - let src_path = crate_path.join("src"); - fs::create_dir_all(&src_path)?; + let crate_dir = target_dir.join(&crate_name); + let src_dir = crate_dir.join("src"); + fs::create_dir_all(&src_dir)?; - let gen_manifest_path = crate_path.join("Cargo.toml"); + let gen_manifest_path = crate_dir.join("Cargo.toml"); write_file(&gen_manifest_path, gen_toml(program_package, sails_package))?; - let main_rs_path = src_path.join("main.rs"); + let main_rs_path = src_dir.join("main.rs"); let out_file = target_dir.join(format!("{}.idl", program_package.name)); write_file( main_rs_path, - gen_main_rs("proxy::ProxyProgram", out_file.as_path()), + gen_main_rs(&program_struct_path, out_file.as_path()), )?; - workspace_members_add(&workspace_root.as_std_path(), &crate_name)?; + // Copy original `Cargo.lock` if any + let from_lock = &metadata.workspace_root.join("Cargo.lock"); + let to_lock = &crate_dir.join("Cargo.lock"); + drop(fs::copy(from_lock, to_lock)); - _ = cargo_run_bin(&gen_manifest_path, &crate_name)?; + // execute cargo run on generated manifest + _ = cargo_run_bin(&gen_manifest_path, &crate_name, &target_dir)?; - workspace_members_remove(&workspace_root.as_std_path(), &crate_name)?; - - fs::remove_dir_all(crate_path)?; + // remove generated files + fs::remove_dir_all(crate_dir)?; Ok(()) } } +const META_PATH_V1: &[&str] = &["sails_rs", "meta", "ProgramMeta"]; +const META_PATH_V2: &[&str] = &["sails_idl_meta", "ProgramMeta"]; + +fn find_program_path( + doc_crate: &rustdoc_types::Crate, + program_meta: (&rustdoc_types::Id, &rustdoc_types::ItemSummary), +) -> Option { + let program_struct_path = doc_crate.index.values().find_map(|idx| { + if let rustdoc_types::ItemEnum::Impl(item) = &idx.inner { + if let Some(tp) = &item.trait_ { + if &tp.id == program_meta.0 { + if let rustdoc_types::Type::ResolvedPath(path) = &item.for_ { + Some(path) + } else { + None + } + } else { + None + } + } else { + None + } + } else { + None + } + }); + program_struct_path.cloned() +} + fn get_crate_name(program_package: &Package) -> String { format!("{}-idl-gen", program_package.name) } @@ -151,6 +171,8 @@ fn gen_toml(program_package: &Package, sails_package: &Package) -> String { .expect("bin is an array of tables") .push(bin); + manifest["workspace"] = toml_edit::Item::Table(toml_edit::Table::new()); + manifest.to_string() } @@ -174,43 +196,6 @@ fn write_file, C: AsRef<[u8]>>(path: P, contents: C) -> anyhow::R .with_context(|| format!("failed to write `{}`", path.display())) } -fn workspace_members_add(path: &Path, name: &str) -> anyhow::Result<()> { - println!("...adding member to workspace: {:?}", name); - - let workspace_cargo_toml = path.join("Cargo.toml"); - let toml = fs::read_to_string(&workspace_cargo_toml).context("failed to read Cargo.toml")?; - let mut doc = toml - .parse::() - .context("failed to parse Cargo.toml")?; - let members = - doc["workspace"]["members"].or_insert(toml_edit::value(toml_edit::Array::default())); - members.as_array_mut().unwrap().push(name); - write_file(&workspace_cargo_toml, doc.to_string()) -} - -fn workspace_members_remove(path: &Path, name: &str) -> anyhow::Result<()> { - println!("...removing member from workspace: {:?}", name); - - let workspace_cargo_toml = path.join("Cargo.toml"); - let toml = fs::read_to_string(&workspace_cargo_toml).context("failed to read Cargo.toml")?; - let mut doc = toml - .parse::() - .context("failed to parse Cargo.toml")?; - let members = doc["workspace"]["members"].as_array_mut(); - - if let Some(members) = members { - let position = members.iter().position(|m| m.as_str() == Some(name)); - if let Some(position) = position { - members.remove(position); - write_file(&workspace_cargo_toml, doc.to_string()) - } else { - Ok(()) - } - } else { - Ok(()) - } -} - fn cargo_doc( manifest_path: &cargo_metadata::camino::Utf8Path, target_dir: &cargo_metadata::camino::Utf8Path, @@ -223,6 +208,7 @@ fn cargo_doc( "RUSTDOCFLAGS", "-Z unstable-options --output-format=json --cap-lints=allow", ) + .env("__GEAR_WASM_BUILDER_NO_BUILD", "1") .stdout(std::process::Stdio::null()) // Don't pollute output .arg("doc") .arg("--manifest-path") @@ -232,24 +218,24 @@ fn cargo_doc( .arg("--no-deps"); cmd.status() - .context("Failed to execute `cargo doc` command") + .context("failed to execute `cargo doc` command") } fn cargo_run_bin( manifest_path: &cargo_metadata::camino::Utf8Path, bin_name: &str, + target_dir: &cargo_metadata::camino::Utf8Path, ) -> anyhow::Result { let cargo_path = std::env::var("CARGO").unwrap_or("cargo".into()); - let args = vec![ - "run", - "--manifest-path", - manifest_path.as_str(), - "--bin", - bin_name, - ]; let mut cmd = Command::new(cargo_path); - cmd.stdout(std::process::Stdio::null()) // Don't pollute output - .args(args); - cmd.status().context("Failed to execute `cargo` command") + cmd.env("CARGO_TARGET_DIR", &target_dir) + .env("__GEAR_WASM_BUILDER_NO_BUILD", "1") + .stdout(std::process::Stdio::null()) // Don't pollute output + .arg("run") + .arg("--manifest-path") + .arg(manifest_path.as_str()) + .arg("--bin") + .arg(bin_name); + cmd.status().context("failed to execute `cargo` command") } From dcb78e70216fb5d1986d019e6f4d0607b5104542 Mon Sep 17 00:00:00 2001 From: vobradovich Date: Wed, 16 Oct 2024 22:56:12 +0200 Subject: [PATCH 05/13] cli idl-gen --- rs/cli/src/idlgen.rs | 351 +++++++++++++++++++++++++++---------------- rs/cli/src/main.rs | 14 +- 2 files changed, 233 insertions(+), 132 deletions(-) diff --git a/rs/cli/src/idlgen.rs b/rs/cli/src/idlgen.rs index 4723e8bd..c5509589 100644 --- a/rs/cli/src/idlgen.rs +++ b/rs/cli/src/idlgen.rs @@ -1,195 +1,210 @@ +use anyhow::Context; +use cargo_metadata::{camino::*, Package}; use std::{ - fs, + env, fs, path::{Path, PathBuf}, process::{Command, ExitStatus}, }; -use anyhow::Context; -use cargo_metadata::{camino::*, Package}; - pub struct CrateIdlGenerator { manifest_path: Utf8PathBuf, - target_dir: Option, + target_dir: Option, + workspace: bool, } impl CrateIdlGenerator { - pub fn new(manifest_path: PathBuf, target_dir: Option) -> Self { + pub fn new( + manifest_path: Option, + target_dir: Option, + workspace: bool, + ) -> Self { Self { - manifest_path: Utf8PathBuf::from_path_buf(manifest_path).unwrap(), - target_dir, + manifest_path: Utf8PathBuf::from_path_buf( + manifest_path.unwrap_or_else(|| env::current_dir().unwrap().join("Cargo.toml")), + ) + .unwrap(), + target_dir: target_dir + .and_then(|p| p.canonicalize().ok()) + .map(Utf8PathBuf::from_path_buf) + .and_then(|t| t.ok()), + workspace, } } pub fn generate(self) -> anyhow::Result<()> { println!("...reading metadata: {}", &self.manifest_path); - // Get metadata with deps + // get metadata with deps let metadata = cargo_metadata::MetadataCommand::new() .manifest_path(&self.manifest_path) .exec()?; + // find `sails-rs` package let sails_package = metadata .packages .iter() .find(|p| p.name == "sails-rs") - .unwrap(); + .context("failed to find `sails-rs` package in dependencies")?; - //self.cargo_doc() - // print!("{:?}", sails_package); + let target_dir = self + .target_dir + .as_ref() + .unwrap_or(&metadata.target_directory); + + // get all workspace packages or only default + let package_list = if self.workspace { + metadata.workspace_packages() + } else { + metadata.workspace_default_packages() + }; + + for program_package in package_list { + let idl_gen = PackageIdlGenerator::new( + program_package, + sails_package, + target_dir, + &metadata.workspace_root, + ); + match idl_gen.read_program_path_from_doc() { + Ok((program_struct_path, meta_path_version)) => { + println!("...found Program implemetation: {}", program_struct_path); + let file_path = idl_gen + .try_generate_for_package(&program_struct_path, meta_path_version)?; + println!("Generated IDL: {}", file_path); + } + Err(err) => { + println!("...no Program implementation found: {}", err); + if !self.workspace { + println!("Try run `cargo sails idl-gen --workspace` to generate IDL for all packages in the workspace"); + } + } + } + } - let program_package = metadata.root_package().unwrap(); - let program_package_file_name = program_package.name.to_lowercase().replace('-', "_"); - // print!("{:?}", program_package); + Ok(()) + } +} - let target_dir = self +struct PackageIdlGenerator<'a> { + program_package: &'a Package, + sails_package: &'a Package, + target_dir: &'a Utf8Path, + workspace_root: &'a Utf8Path, +} + +impl<'a> PackageIdlGenerator<'a> { + fn new( + program_package: &'a Package, + sails_package: &'a Package, + target_dir: &'a Utf8Path, + workspace_root: &'a Utf8Path, + ) -> Self { + Self { + program_package, + sails_package, + target_dir, + workspace_root, + } + } + + fn read_program_path_from_doc(&self) -> anyhow::Result<(String, MetaPathVersion)> { + let program_package_file_name = &self.program_package.name.to_lowercase().replace('-', "_"); + println!( + "...running `cargo doc --manifest-path {}`", + &self.program_package.manifest_path + ); + // run `cargo doc` + _ = cargo_doc(&self.program_package.manifest_path, self.target_dir)?; + // read doc + let docs_path = &self .target_dir - .map(|p| p.canonicalize().ok()) - .flatten() - .map(Utf8PathBuf::from_path_buf) - .map(|t| t.ok()) - .flatten() - .unwrap_or_else(|| metadata.target_directory.clone()); - - _ = cargo_doc(&self.manifest_path, &target_dir)?; - let docs_path = target_dir .join("doc") .join(format!("{}.json", &program_package_file_name)); - - println!("...reading docs: {:?}", docs_path); + println!("...reading docs: {}", docs_path); let json_string = std::fs::read_to_string(docs_path)?; let doc_crate: rustdoc_types::Crate = serde_json::from_str(&json_string)?; - let program_meta = doc_crate + + // find `sails_rs::meta::ProgramMeta` path id + let (program_meta_id, meta_path_version) = doc_crate .paths .iter() - .find(|p| p.1.path == META_PATH_V2) + .find_map(|p| { + if p.1.path == META_PATH_V1 { + Some((p.0, MetaPathVersion::V1)) + } else if p.1.path == META_PATH_V2 { + Some((p.0, MetaPathVersion::V2)) + } else { + None + } + }) .context("failed to find `sails_rs::meta::ProgramMeta` definition in dependencies")?; - // println!("{:?}", program_meta); - - let program_struct_path = find_program_path(&doc_crate, program_meta) + // find struct implementing `sails_rs::meta::ProgramMeta` + let program_struct_path = doc_crate + .index + .values() + .find_map(|idx| is_implement_trait_path(idx, program_meta_id)) .context("failed to find `sails_rs::meta::ProgramMeta` implemetation")?; let program_struct = doc_crate .paths .get(&program_struct_path.id) - .context("failed to find Program path")?; + .context("failed to get Program struct by id")?; let program_struct_path = program_struct.path.join("::"); - println!("...found Program implemetation: {:?}", program_struct_path); - - let crate_name = get_crate_name(program_package); + Ok((program_struct_path, meta_path_version)) + } - let crate_dir = target_dir.join(&crate_name); + fn try_generate_for_package( + &self, + program_struct_path: &str, + meta_path_version: MetaPathVersion, + ) -> Result { + let crate_name = get_crate_name(self.program_package); + let crate_dir = &self.target_dir.join(&crate_name); let src_dir = crate_dir.join("src"); fs::create_dir_all(&src_dir)?; let gen_manifest_path = crate_dir.join("Cargo.toml"); - write_file(&gen_manifest_path, gen_toml(program_package, sails_package))?; - let main_rs_path = src_dir.join("main.rs"); - - let out_file = target_dir.join(format!("{}.idl", program_package.name)); write_file( - main_rs_path, - gen_main_rs(&program_struct_path, out_file.as_path()), + &gen_manifest_path, + gen_cargo_toml(self.program_package, self.sails_package, meta_path_version), )?; - // Copy original `Cargo.lock` if any - let from_lock = &metadata.workspace_root.join("Cargo.lock"); + let out_file = self + .target_dir + .join(format!("{}.idl", &self.program_package.name)); + let main_rs_path = src_dir.join("main.rs"); + write_file(main_rs_path, gen_main_rs(program_struct_path, &out_file))?; + + let from_lock = &self.workspace_root.join("Cargo.lock"); let to_lock = &crate_dir.join("Cargo.lock"); drop(fs::copy(from_lock, to_lock)); - // execute cargo run on generated manifest - _ = cargo_run_bin(&gen_manifest_path, &crate_name, &target_dir)?; + _ = cargo_run_bin(&gen_manifest_path, &crate_name, self.target_dir)?; - // remove generated files fs::remove_dir_all(crate_dir)?; - Ok(()) + Ok(out_file) } } -const META_PATH_V1: &[&str] = &["sails_rs", "meta", "ProgramMeta"]; -const META_PATH_V2: &[&str] = &["sails_idl_meta", "ProgramMeta"]; - -fn find_program_path( - doc_crate: &rustdoc_types::Crate, - program_meta: (&rustdoc_types::Id, &rustdoc_types::ItemSummary), +fn is_implement_trait_path( + idx: &rustdoc_types::Item, + program_meta_id: &rustdoc_types::Id, ) -> Option { - let program_struct_path = doc_crate.index.values().find_map(|idx| { - if let rustdoc_types::ItemEnum::Impl(item) = &idx.inner { - if let Some(tp) = &item.trait_ { - if &tp.id == program_meta.0 { - if let rustdoc_types::Type::ResolvedPath(path) = &item.for_ { - Some(path) - } else { - None - } - } else { - None + if let rustdoc_types::ItemEnum::Impl(item) = &idx.inner { + if let Some(tp) = &item.trait_ { + if &tp.id == program_meta_id { + if let rustdoc_types::Type::ResolvedPath(path) = &item.for_ { + return Some(path.clone()); } - } else { - None } - } else { - None } - }); - program_struct_path.cloned() + } + None } fn get_crate_name(program_package: &Package) -> String { format!("{}-idl-gen", program_package.name) } -fn gen_toml(program_package: &Package, sails_package: &Package) -> String { - let mut manifest = toml_edit::DocumentMut::new(); - manifest["package"] = toml_edit::Item::Table(toml_edit::Table::new()); - manifest["package"]["name"] = toml_edit::value(get_crate_name(program_package)); - manifest["package"]["version"] = toml_edit::value("0.1.0"); - manifest["package"]["edition"] = toml_edit::value(program_package.edition.as_str()); - - let mut dep_table = toml_edit::Table::default(); - let mut package_table = toml_edit::InlineTable::new(); - let manifets_dir = program_package.manifest_path.parent().unwrap(); - package_table.insert("path", manifets_dir.as_str().into()); - dep_table[&program_package.name] = toml_edit::value(package_table); - - let mut features = toml_edit::Array::default(); - features.push("idl-gen"); - let mut sails_table = toml_edit::InlineTable::new(); - let manifets_dir = sails_package.manifest_path.parent().unwrap(); - sails_table.insert("path", manifets_dir.as_str().into()); - sails_table.insert("features", features.into()); - dep_table[&sails_package.name] = toml_edit::value(sails_table); - - manifest["dependencies"] = toml_edit::Item::Table(dep_table); - - let mut bin = toml_edit::Table::new(); - bin["name"] = toml_edit::value(get_crate_name(program_package)); - bin["path"] = toml_edit::value("src/main.rs"); - manifest["bin"] - .or_insert(toml_edit::Item::ArrayOfTables( - toml_edit::ArrayOfTables::new(), - )) - .as_array_of_tables_mut() - .expect("bin is an array of tables") - .push(bin); - - manifest["workspace"] = toml_edit::Item::Table(toml_edit::Table::new()); - - manifest.to_string() -} - -fn gen_main_rs(program_struct_path: &str, out_file: &cargo_metadata::camino::Utf8Path) -> String { - format!( - " -fn main() {{ - sails_rs::generate_idl_to_file::<{}>( - std::path::PathBuf::from(r\"{}\") - ) - .unwrap(); -}}", - program_struct_path, - out_file.as_str(), - ) -} - fn write_file, C: AsRef<[u8]>>(path: P, contents: C) -> anyhow::Result<()> { let path = path.as_ref(); fs::write(path, contents.as_ref()) @@ -215,7 +230,8 @@ fn cargo_doc( .arg(manifest_path.as_str()) .arg("--target-dir") .arg(target_dir.as_str()) - .arg("--no-deps"); + .arg("--no-deps") + .arg("--quiet"); cmd.status() .context("failed to execute `cargo doc` command") @@ -229,7 +245,7 @@ fn cargo_run_bin( let cargo_path = std::env::var("CARGO").unwrap_or("cargo".into()); let mut cmd = Command::new(cargo_path); - cmd.env("CARGO_TARGET_DIR", &target_dir) + cmd.env("CARGO_TARGET_DIR", target_dir) .env("__GEAR_WASM_BUILDER_NO_BUILD", "1") .stdout(std::process::Stdio::null()) // Don't pollute output .arg("run") @@ -239,3 +255,84 @@ fn cargo_run_bin( .arg(bin_name); cmd.status().context("failed to execute `cargo` command") } + +enum MetaPathVersion { + V1, + V2, +} + +pub const META_PATH_V1: &[&str] = &["sails_rs", "meta", "ProgramMeta"]; +pub const META_PATH_V2: &[&str] = &["sails_idl_meta", "ProgramMeta"]; + +fn gen_cargo_toml( + program_package: &Package, + sails_package: &Package, + meta_path_version: MetaPathVersion, +) -> String { + let mut manifest = toml_edit::DocumentMut::new(); + manifest["package"] = toml_edit::Item::Table(toml_edit::Table::new()); + manifest["package"]["name"] = toml_edit::value(get_crate_name(program_package)); + manifest["package"]["version"] = toml_edit::value("0.1.0"); + manifest["package"]["edition"] = toml_edit::value(program_package.edition.as_str()); + + let mut dep_table = toml_edit::Table::default(); + let mut package_table = toml_edit::InlineTable::new(); + let manifets_dir = program_package.manifest_path.parent().unwrap(); + package_table.insert("path", manifets_dir.as_str().into()); + dep_table[&program_package.name] = toml_edit::value(package_table); + + let sails_dep = match meta_path_version { + MetaPathVersion::V1 => sails_dep_v1(sails_package), + MetaPathVersion::V2 => sails_dep_v2(sails_package), + }; + dep_table[&sails_package.name] = toml_edit::value(sails_dep); + + manifest["dependencies"] = toml_edit::Item::Table(dep_table); + + let mut bin = toml_edit::Table::new(); + bin["name"] = toml_edit::value(get_crate_name(program_package)); + bin["path"] = toml_edit::value("src/main.rs"); + manifest["bin"] + .or_insert(toml_edit::Item::ArrayOfTables( + toml_edit::ArrayOfTables::new(), + )) + .as_array_of_tables_mut() + .expect("bin is an array of tables") + .push(bin); + + manifest["workspace"] = toml_edit::Item::Table(toml_edit::Table::new()); + + manifest.to_string() +} + +fn sails_dep_v1(sails_package: &Package) -> toml_edit::InlineTable { + let mut sails_table = toml_edit::InlineTable::new(); + sails_table.insert("package", "sails-idl-gen".into()); + sails_table.insert("version", sails_package.version.to_string().into()); + sails_table +} + +fn sails_dep_v2(sails_package: &Package) -> toml_edit::InlineTable { + let mut features = toml_edit::Array::default(); + features.push("idl-gen"); + let mut sails_table = toml_edit::InlineTable::new(); + let manifets_dir = sails_package.manifest_path.parent().unwrap(); + sails_table.insert("package", sails_package.name.as_str().into()); + sails_table.insert("path", manifets_dir.as_str().into()); + sails_table.insert("features", features.into()); + sails_table +} + +fn gen_main_rs(program_struct_path: &str, out_file: &cargo_metadata::camino::Utf8Path) -> String { + format!( + " +fn main() {{ + sails_rs::generate_idl_to_file::<{}>( + std::path::PathBuf::from(r\"{}\") + ) + .unwrap(); +}}", + program_struct_path, + out_file.as_str(), + ) +} diff --git a/rs/cli/src/main.rs b/rs/cli/src/main.rs index 3c1a8dd8..67cea07f 100644 --- a/rs/cli/src/main.rs +++ b/rs/cli/src/main.rs @@ -52,14 +52,17 @@ enum SailsCommands { }, /// Generate IDL from Cargo manifest - #[command(name = "idl")] + #[command(name = "idl-gen")] IdlGen { /// Path to the crate with program - #[arg(value_hint = clap::ValueHint::FilePath)] - manifest_path: PathBuf, + #[arg(long, value_hint = clap::ValueHint::FilePath)] + manifest_path: Option, /// Directory for all generated artifacts #[arg(long, value_hint = clap::ValueHint::DirPath)] target_dir: Option, + /// Generate IDL for all packages in the workspace + #[arg(long)] + workspace: bool, }, } @@ -119,8 +122,9 @@ fn main() -> Result<(), i32> { } SailsCommands::IdlGen { manifest_path, - target_dir: target_dit, - } => CrateIdlGenerator::new(manifest_path, target_dit).generate(), + target_dir, + workspace, + } => CrateIdlGenerator::new(manifest_path, target_dir, workspace).generate(), }; if let Err(e) = result { From 4e1aea8367638f279c4b8da4aa57f9286e1efcf4 Mon Sep 17 00:00:00 2001 From: vobradovich Date: Thu, 17 Oct 2024 11:18:14 +0200 Subject: [PATCH 06/13] handle error --- rs/cli/src/idlgen.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/rs/cli/src/idlgen.rs b/rs/cli/src/idlgen.rs index c5509589..0a771269 100644 --- a/rs/cli/src/idlgen.rs +++ b/rs/cli/src/idlgen.rs @@ -109,7 +109,7 @@ impl<'a> PackageIdlGenerator<'a> { fn read_program_path_from_doc(&self) -> anyhow::Result<(String, MetaPathVersion)> { let program_package_file_name = &self.program_package.name.to_lowercase().replace('-', "_"); println!( - "...running `cargo doc --manifest-path {}`", + "...running doc generation for `{}`", &self.program_package.manifest_path ); // run `cargo doc` @@ -119,7 +119,7 @@ impl<'a> PackageIdlGenerator<'a> { .target_dir .join("doc") .join(format!("{}.json", &program_package_file_name)); - println!("...reading docs: {}", docs_path); + println!("...reading doc: {}", docs_path); let json_string = std::fs::read_to_string(docs_path)?; let doc_crate: rustdoc_types::Crate = serde_json::from_str(&json_string)?; @@ -155,7 +155,7 @@ impl<'a> PackageIdlGenerator<'a> { &self, program_struct_path: &str, meta_path_version: MetaPathVersion, - ) -> Result { + ) -> anyhow::Result { let crate_name = get_crate_name(self.program_package); let crate_dir = &self.target_dir.join(&crate_name); let src_dir = crate_dir.join("src"); @@ -177,11 +177,15 @@ impl<'a> PackageIdlGenerator<'a> { let to_lock = &crate_dir.join("Cargo.lock"); drop(fs::copy(from_lock, to_lock)); - _ = cargo_run_bin(&gen_manifest_path, &crate_name, self.target_dir)?; + let res = cargo_run_bin(&gen_manifest_path, &crate_name, self.target_dir); fs::remove_dir_all(crate_dir)?; - Ok(out_file) + match res { + Ok(exit_status) if exit_status.success() => Ok(out_file), + Ok(exit_status) => Err(anyhow::anyhow!("Exit status: {}", exit_status)), + Err(err) => Err(err), + } } } From fc82073a7ed05aa9b68d6bbbefa4615563ecafb3 Mon Sep 17 00:00:00 2001 From: vobradovich Date: Thu, 17 Oct 2024 12:20:12 +0200 Subject: [PATCH 07/13] resolve sails-rs multi version deps --- rs/cli/src/idlgen.rs | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/rs/cli/src/idlgen.rs b/rs/cli/src/idlgen.rs index 0a771269..44af4a9b 100644 --- a/rs/cli/src/idlgen.rs +++ b/rs/cli/src/idlgen.rs @@ -38,12 +38,12 @@ impl CrateIdlGenerator { .manifest_path(&self.manifest_path) .exec()?; - // find `sails-rs` package - let sails_package = metadata + // find `sails-rs` packages (any version ) + let sails_packages = metadata .packages .iter() - .find(|p| p.name == "sails-rs") - .context("failed to find `sails-rs` package in dependencies")?; + .filter(|p| p.name == "sails-rs") + .collect::>(); let target_dir = self .target_dir @@ -60,7 +60,7 @@ impl CrateIdlGenerator { for program_package in package_list { let idl_gen = PackageIdlGenerator::new( program_package, - sails_package, + &sails_packages, target_dir, &metadata.workspace_root, ); @@ -86,7 +86,7 @@ impl CrateIdlGenerator { struct PackageIdlGenerator<'a> { program_package: &'a Package, - sails_package: &'a Package, + sails_packages: &'a Vec<&'a Package>, target_dir: &'a Utf8Path, workspace_root: &'a Utf8Path, } @@ -94,13 +94,13 @@ struct PackageIdlGenerator<'a> { impl<'a> PackageIdlGenerator<'a> { fn new( program_package: &'a Package, - sails_package: &'a Package, + sails_packages: &'a Vec<&'a Package>, target_dir: &'a Utf8Path, workspace_root: &'a Utf8Path, ) -> Self { Self { program_package, - sails_package, + sails_packages, target_dir, workspace_root, } @@ -156,6 +156,23 @@ impl<'a> PackageIdlGenerator<'a> { program_struct_path: &str, meta_path_version: MetaPathVersion, ) -> anyhow::Result { + // find `sails-rs` dependency + let sails_dep = self + .program_package + .dependencies + .iter() + .find(|p| p.name == "sails-rs") + .context("failed to find `sails-rs` dependency")?; + // find `sails-rs` package matches dep version + let sails_package = self + .sails_packages + .iter() + .find(|p| sails_dep.req.matches(&p.version)) + .context(format!( + "failed to find `sails-rs` package with matching version {}", + &sails_dep.req + ))?; + let crate_name = get_crate_name(self.program_package); let crate_dir = &self.target_dir.join(&crate_name); let src_dir = crate_dir.join("src"); @@ -164,7 +181,7 @@ impl<'a> PackageIdlGenerator<'a> { let gen_manifest_path = crate_dir.join("Cargo.toml"); write_file( &gen_manifest_path, - gen_cargo_toml(self.program_package, self.sails_package, meta_path_version), + gen_cargo_toml(self.program_package, sails_package, meta_path_version), )?; let out_file = self From 87c881664cc325087bd4bc7cd28f07ad85fc1ce1 Mon Sep 17 00:00:00 2001 From: vobradovich Date: Mon, 21 Oct 2024 15:25:11 +0200 Subject: [PATCH 08/13] rename & refactoring --- rs/cli/src/idlgen.rs | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/rs/cli/src/idlgen.rs b/rs/cli/src/idlgen.rs index 44af4a9b..850507b7 100644 --- a/rs/cli/src/idlgen.rs +++ b/rs/cli/src/idlgen.rs @@ -64,7 +64,7 @@ impl CrateIdlGenerator { target_dir, &metadata.workspace_root, ); - match idl_gen.read_program_path_from_doc() { + match idl_gen.get_program_struct_path_from_doc() { Ok((program_struct_path, meta_path_version)) => { println!("...found Program implemetation: {}", program_struct_path); let file_path = idl_gen @@ -106,7 +106,7 @@ impl<'a> PackageIdlGenerator<'a> { } } - fn read_program_path_from_doc(&self) -> anyhow::Result<(String, MetaPathVersion)> { + fn get_program_struct_path_from_doc(&self) -> anyhow::Result<(String, MetaPathVersion)> { let program_package_file_name = &self.program_package.name.to_lowercase().replace('-', "_"); println!( "...running doc generation for `{}`", @@ -127,21 +127,13 @@ impl<'a> PackageIdlGenerator<'a> { let (program_meta_id, meta_path_version) = doc_crate .paths .iter() - .find_map(|p| { - if p.1.path == META_PATH_V1 { - Some((p.0, MetaPathVersion::V1)) - } else if p.1.path == META_PATH_V2 { - Some((p.0, MetaPathVersion::V2)) - } else { - None - } - }) + .find_map(|(id, summary)| MetaPathVersion::matches(&summary.path).map(|v| (id, v))) .context("failed to find `sails_rs::meta::ProgramMeta` definition in dependencies")?; // find struct implementing `sails_rs::meta::ProgramMeta` let program_struct_path = doc_crate .index .values() - .find_map(|idx| is_implement_trait_path(idx, program_meta_id)) + .find_map(|idx| try_get_trait_implementation_path(idx, program_meta_id)) .context("failed to find `sails_rs::meta::ProgramMeta` implemetation")?; let program_struct = doc_crate .paths @@ -173,7 +165,7 @@ impl<'a> PackageIdlGenerator<'a> { &sails_dep.req ))?; - let crate_name = get_crate_name(self.program_package); + let crate_name = get_idl_gen_crate_name(self.program_package); let crate_dir = &self.target_dir.join(&crate_name); let src_dir = crate_dir.join("src"); fs::create_dir_all(&src_dir)?; @@ -206,7 +198,7 @@ impl<'a> PackageIdlGenerator<'a> { } } -fn is_implement_trait_path( +fn try_get_trait_implementation_path( idx: &rustdoc_types::Item, program_meta_id: &rustdoc_types::Id, ) -> Option { @@ -222,7 +214,7 @@ fn is_implement_trait_path( None } -fn get_crate_name(program_package: &Package) -> String { +fn get_idl_gen_crate_name(program_package: &Package) -> String { format!("{}-idl-gen", program_package.name) } @@ -282,8 +274,20 @@ enum MetaPathVersion { V2, } -pub const META_PATH_V1: &[&str] = &["sails_rs", "meta", "ProgramMeta"]; -pub const META_PATH_V2: &[&str] = &["sails_idl_meta", "ProgramMeta"]; +impl MetaPathVersion { + const META_PATH_V1: &[&str] = &["sails_rs", "meta", "ProgramMeta"]; + const META_PATH_V2: &[&str] = &["sails_idl_meta", "ProgramMeta"]; + + fn matches(path: &Vec) -> Option { + if path == Self::META_PATH_V1 { + Some(MetaPathVersion::V1) + } else if path == Self::META_PATH_V2 { + Some(MetaPathVersion::V2) + } else { + None + } + } +} fn gen_cargo_toml( program_package: &Package, @@ -292,7 +296,7 @@ fn gen_cargo_toml( ) -> String { let mut manifest = toml_edit::DocumentMut::new(); manifest["package"] = toml_edit::Item::Table(toml_edit::Table::new()); - manifest["package"]["name"] = toml_edit::value(get_crate_name(program_package)); + manifest["package"]["name"] = toml_edit::value(get_idl_gen_crate_name(program_package)); manifest["package"]["version"] = toml_edit::value("0.1.0"); manifest["package"]["edition"] = toml_edit::value(program_package.edition.as_str()); @@ -311,7 +315,7 @@ fn gen_cargo_toml( manifest["dependencies"] = toml_edit::Item::Table(dep_table); let mut bin = toml_edit::Table::new(); - bin["name"] = toml_edit::value(get_crate_name(program_package)); + bin["name"] = toml_edit::value(get_idl_gen_crate_name(program_package)); bin["path"] = toml_edit::value("src/main.rs"); manifest["bin"] .or_insert(toml_edit::Item::ArrayOfTables( From 2574150e82b06f809550815089ece530b29608ca Mon Sep 17 00:00:00 2001 From: vobradovich Date: Mon, 21 Oct 2024 15:32:59 +0200 Subject: [PATCH 09/13] rename commands, add CI test --- .github/workflows/ci.yml | 6 +++++- rs/cli/src/main.rs | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 395caf64..ad6e86ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,9 +123,13 @@ jobs: - name: Generate MyDemo via CLI run: | - SAILS_CLI_TEMPLATES_BRANCH=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} ./target/debug/cargo-sails sails new-program ~/tmp --name my-demo + SAILS_CLI_TEMPLATES_BRANCH=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} ./target/debug/cargo-sails sails program ~/tmp --name my-demo - name: Run Tests on MyDemo run: | cd ~/tmp/my-demo cargo test -p my-demo + + - name: Generate IDL from MyDemo via CLI + run: | + ./target/debug/cargo-sails sails idl --manifest-path ~/tmp/my-demo/Cargo.toml diff --git a/rs/cli/src/main.rs b/rs/cli/src/main.rs index 67cea07f..3d456579 100644 --- a/rs/cli/src/main.rs +++ b/rs/cli/src/main.rs @@ -13,7 +13,7 @@ enum CliCommand { #[derive(Subcommand)] enum SailsCommands { /// Create a new program from template - #[command(name = "new-program")] + #[command(name = "program")] NewProgram { #[arg(help = "Path to the new program")] path: String, @@ -52,7 +52,7 @@ enum SailsCommands { }, /// Generate IDL from Cargo manifest - #[command(name = "idl-gen")] + #[command(name = "idl")] IdlGen { /// Path to the crate with program #[arg(long, value_hint = clap::ValueHint::FilePath)] From d871ad5126fff471be89641e869f32ab9a12830d Mon Sep 17 00:00:00 2001 From: vobradovich Date: Mon, 21 Oct 2024 19:32:03 +0200 Subject: [PATCH 10/13] resolve root package & deps, remove workspace flag --- rs/cli/src/idlgen.rs | 49 ++++++++++++++++++++++++++------------------ rs/cli/src/main.rs | 6 +----- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/rs/cli/src/idlgen.rs b/rs/cli/src/idlgen.rs index 850507b7..0c367cba 100644 --- a/rs/cli/src/idlgen.rs +++ b/rs/cli/src/idlgen.rs @@ -9,15 +9,10 @@ use std::{ pub struct CrateIdlGenerator { manifest_path: Utf8PathBuf, target_dir: Option, - workspace: bool, } impl CrateIdlGenerator { - pub fn new( - manifest_path: Option, - target_dir: Option, - workspace: bool, - ) -> Self { + pub fn new(manifest_path: Option, target_dir: Option) -> Self { Self { manifest_path: Utf8PathBuf::from_path_buf( manifest_path.unwrap_or_else(|| env::current_dir().unwrap().join("Cargo.toml")), @@ -27,7 +22,6 @@ impl CrateIdlGenerator { .and_then(|p| p.canonicalize().ok()) .map(Utf8PathBuf::from_path_buf) .and_then(|t| t.ok()), - workspace, } } @@ -42,7 +36,31 @@ impl CrateIdlGenerator { let sails_packages = metadata .packages .iter() - .filter(|p| p.name == "sails-rs") + .filter(|&p| p.name == "sails-rs") + .collect::>(); + + let resolve = &metadata + .resolve + .context("failed to get resolve from metadata")?; + let root_pacakge_id = resolve + .root + .as_ref() + .context("failed to find root package")?; + let root_node = resolve + .nodes + .iter() + .find(|&node| &node.id == root_pacakge_id) + .context("failed to find root package")?; + + // find root package and it dependencies in workspace members + let package_list = &metadata + .packages + .iter() + .filter(|&p| { + &p.id == root_pacakge_id + || (metadata.workspace_members.contains(&p.id) + && root_node.dependencies.contains(&p.id)) + }) .collect::>(); let target_dir = self @@ -50,13 +68,6 @@ impl CrateIdlGenerator { .as_ref() .unwrap_or(&metadata.target_directory); - // get all workspace packages or only default - let package_list = if self.workspace { - metadata.workspace_packages() - } else { - metadata.workspace_default_packages() - }; - for program_package in package_list { let idl_gen = PackageIdlGenerator::new( program_package, @@ -70,17 +81,15 @@ impl CrateIdlGenerator { let file_path = idl_gen .try_generate_for_package(&program_struct_path, meta_path_version)?; println!("Generated IDL: {}", file_path); + + return Ok(()); } Err(err) => { println!("...no Program implementation found: {}", err); - if !self.workspace { - println!("Try run `cargo sails idl-gen --workspace` to generate IDL for all packages in the workspace"); - } } } } - - Ok(()) + Err(anyhow::anyhow!("no Program implementation found")) } } diff --git a/rs/cli/src/main.rs b/rs/cli/src/main.rs index 3d456579..ad2d00a7 100644 --- a/rs/cli/src/main.rs +++ b/rs/cli/src/main.rs @@ -60,9 +60,6 @@ enum SailsCommands { /// Directory for all generated artifacts #[arg(long, value_hint = clap::ValueHint::DirPath)] target_dir: Option, - /// Generate IDL for all packages in the workspace - #[arg(long)] - workspace: bool, }, } @@ -123,8 +120,7 @@ fn main() -> Result<(), i32> { SailsCommands::IdlGen { manifest_path, target_dir, - workspace, - } => CrateIdlGenerator::new(manifest_path, target_dir, workspace).generate(), + } => CrateIdlGenerator::new(manifest_path, target_dir).generate(), }; if let Err(e) = result { From 480da3e25e492d43171ee67e4c86b8f8799f0042 Mon Sep 17 00:00:00 2001 From: vobradovich Date: Mon, 21 Oct 2024 20:03:27 +0200 Subject: [PATCH 11/13] normalize eol --- examples/rmrk/resource/wasm/src/lib.rs | 10 +- rs/macros/core/src/service/mod.rs | 1276 ++++++++++++------------ templates/program/CargoTemplate.toml | 52 +- 3 files changed, 669 insertions(+), 669 deletions(-) diff --git a/examples/rmrk/resource/wasm/src/lib.rs b/examples/rmrk/resource/wasm/src/lib.rs index fe0a1cfd..89cc857c 100644 --- a/examples/rmrk/resource/wasm/src/lib.rs +++ b/examples/rmrk/resource/wasm/src/lib.rs @@ -1,5 +1,5 @@ -#![no_std] - -// Re-export functions required for buildng WASM app -#[cfg(target_arch = "wasm32")] -pub use rmrk_resource_app::wasm::*; +#![no_std] + +// Re-export functions required for buildng WASM app +#[cfg(target_arch = "wasm32")] +pub use rmrk_resource_app::wasm::*; diff --git a/rs/macros/core/src/service/mod.rs b/rs/macros/core/src/service/mod.rs index 2c152eb0..a09c89b2 100644 --- a/rs/macros/core/src/service/mod.rs +++ b/rs/macros/core/src/service/mod.rs @@ -1,638 +1,638 @@ -// This file is part of Gear. - -// Copyright (C) 2021-2023 Gear Technologies Inc. -// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -//! Supporting functions and structures for the `gservice` macro. - -use crate::{ - sails_paths, - shared::{self, Func}, -}; -use args::ServiceArgs; -use convert_case::{Case, Casing}; -use parity_scale_codec::Encode; -use proc_macro2::{Span, TokenStream}; -use proc_macro_error::abort; -use quote::quote; -use std::collections::BTreeMap; -use syn::{ - parse_quote, punctuated::Punctuated, spanned::Spanned, token::Comma, Ident, ImplItemFn, - ItemImpl, Lifetime, Path, Type, Visibility, -}; - -mod args; - -static mut SERVICE_SPANS: BTreeMap = BTreeMap::new(); - -pub fn gservice(args: TokenStream, service_impl: TokenStream) -> TokenStream { - let service_impl = parse_gservice_impl(service_impl); - ensure_single_gservice_on_impl(&service_impl); - ensure_single_gservice_by_name(&service_impl); - generate_gservice(args, service_impl) -} - -#[doc(hidden)] -pub fn __gservice_internal(args: TokenStream, service_impl: TokenStream) -> TokenStream { - let service_impl = parse_gservice_impl(service_impl); - generate_gservice(args, service_impl) -} - -fn parse_gservice_impl(service_impl_tokens: TokenStream) -> ItemImpl { - syn::parse2(service_impl_tokens).unwrap_or_else(|err| { - abort!( - err.span(), - "`service` attribute can be applied to impls only: {}", - err - ) - }) -} - -fn ensure_single_gservice_on_impl(service_impl: &ItemImpl) { - let attr_gservice = service_impl.attrs.iter().find(|attr| { - attr.meta - .path() - .segments - .last() - .map(|s| s.ident == "service") - .unwrap_or(false) - }); - if attr_gservice.is_some() { - abort!( - service_impl, - "multiple `service` attributes on the same impl are not allowed", - ) - } -} - -fn ensure_single_gservice_by_name(service_impl: &ItemImpl) { - let (path, ..) = shared::impl_type(service_impl); - let type_ident = path.path.segments.last().unwrap().ident.to_string(); - if unsafe { SERVICE_SPANS.get(&type_ident) }.is_some() { - abort!( - service_impl, - "multiple `service` attributes on a type with the same name are not allowed" - ) - } - unsafe { SERVICE_SPANS.insert(type_ident, service_impl.span()) }; -} - -fn generate_gservice(args: TokenStream, service_impl: ItemImpl) -> TokenStream { - let service_args = syn::parse2::(args).unwrap_or_else(|err| { - abort!( - err.span(), - "failed to parse `service` attribute arguments: {}", - err - ) - }); - let sails_path = service_args.sails_path(); - let scale_codec_path = sails_paths::scale_codec_path(&sails_path); - let scale_info_path = sails_paths::scale_info_path(&sails_path); - - let (service_type_path, service_type_args) = shared::impl_type(&service_impl); - let (generics, service_type_constraints) = shared::impl_constraints(&service_impl); - - let service_handlers = discover_service_handlers(&service_impl); - - if service_handlers.is_empty() && service_args.base_types().is_empty() { - abort!( - service_impl, - "`service` attribute requires impl to define at least one public method or extend another service" - ); - } - - let events_type = service_args.events_type().as_ref(); - - let mut service_impl = service_impl.clone(); - if let Some(events_type) = events_type { - service_impl.items.push(parse_quote!( - fn notify_on(&mut self, event: #events_type ) -> #sails_path::errors::Result<()> { - #[cfg(not(target_arch = "wasm32"))] - { - let self_ptr = self as *const _ as usize; - let event_listeners = event_listeners().lock(); - if let Some(event_listener_ptr) = event_listeners.get(&self_ptr) { - let event_listener = - unsafe { &mut *(*event_listener_ptr as *mut Box) }; - core::mem::drop(event_listeners); - event_listener(&event); - } - } - #sails_path::gstd::events::__notify_on(event) - } - )); - } - - let inner_ident = Ident::new("inner", Span::call_site()); - let inner_ptr_ident = Ident::new("inner_ptr", Span::call_site()); - let input_ident = Ident::new("input", Span::call_site()); - - let mut exposure_funcs = Vec::with_capacity(service_handlers.len()); - let mut invocation_params_structs = Vec::with_capacity(service_handlers.len()); - let mut invocation_funcs = Vec::with_capacity(service_handlers.len()); - let mut invocation_dispatches = Vec::with_capacity(service_handlers.len()); - let mut commands_meta_variants = Vec::with_capacity(service_handlers.len()); - let mut queries_meta_variants = Vec::with_capacity(service_handlers.len()); - - for (handler_route, (handler_fn, ..)) in &service_handlers { - // We propagate only known attributes as we don't know the consequences of unknown ones - let handler_allow_attrs = handler_fn - .attrs - .iter() - .filter(|attr| attr.path().is_ident("allow")); - let handler_docs_attrs = handler_fn - .attrs - .iter() - .filter(|attr| attr.path().is_ident("doc")); - - let handler_fn = &handler_fn.sig; - let handler_func = Func::from(handler_fn); - let handler_generator = HandlerGenerator::from(handler_func.clone()); - let invocation_func_ident = handler_generator.invocation_func_ident(); - - exposure_funcs.push({ - let handler_ident = handler_func.ident(); - let handler_params = handler_func.params().iter().map(|item| item.0); - let handler_await_token = handler_func.is_async().then(|| quote!(.await)); - quote!( - #( #handler_allow_attrs )* - pub #handler_fn { - let exposure_scope = #sails_path::gstd::services::ExposureCallScope::new(self); - self. #inner_ident . #handler_ident (#(#handler_params),*) #handler_await_token - } - ) - }); - invocation_params_structs.push(handler_generator.params_struct()); - invocation_funcs.push(handler_generator.invocation_func()); - invocation_dispatches.push({ - let handler_route_bytes = handler_route.encode(); - let handler_route_len = handler_route_bytes.len(); - quote!( - if #input_ident.starts_with(& [ #(#handler_route_bytes),* ]) { - let (output, value) = self.#invocation_func_ident(&#input_ident[#handler_route_len..]).await; - static INVOCATION_ROUTE: [u8; #handler_route_len] = [ #(#handler_route_bytes),* ]; - return Some(([INVOCATION_ROUTE.as_ref(), &output].concat(), value)); - } - ) - }); - - let handler_meta_variant = { - let params_struct_ident = handler_generator.params_struct_ident(); - let result_type = handler_generator.result_type(); - let handler_route_ident = Ident::new(handler_route, Span::call_site()); - - quote!( - #( #handler_docs_attrs )* - #handler_route_ident(#params_struct_ident, #result_type) - ) - }; - if handler_generator.is_query() { - queries_meta_variants.push(handler_meta_variant); - } else { - commands_meta_variants.push(handler_meta_variant); - } - } - - let message_id_ident = Ident::new("message_id", Span::call_site()); - let route_ident = Ident::new("route", Span::call_site()); - - let code_for_base_types = service_args.base_types().iter() - .enumerate() - .map(|(idx, base_type)| { - let base_ident = Ident::new(&format!("base_{}", idx), Span::call_site()); - let as_base_ident = Ident::new(&format!("as_base_{}", idx), Span::call_site()); - - let base_exposure_accessor = quote!( - pub fn #as_base_ident (&self) -> &< #base_type as #sails_path::gstd::services::Service>::Exposure { - &self. #base_ident - } - ); - - let base_exposure_member = quote!( - #base_ident : < #base_type as #sails_path::gstd::services::Service>::Exposure, - ); - - let base_exposure_instantiation = quote!( - #base_ident : < #base_type as Clone>::clone(AsRef::< #base_type >::as_ref( #inner_ident )) - .expose( #message_id_ident , #route_ident ), - ); - - let base_exposure_invocation = quote!( - if let Some((output, value)) = self. #base_ident .try_handle(#input_ident).await { - return Some((output, value)); - } - ); - - let base_service_meta = quote!(#sails_path::meta::AnyServiceMeta::new::< #base_type >()); - - (base_exposure_accessor, base_exposure_member, base_exposure_instantiation, base_exposure_invocation, base_service_meta) - }); - - let base_exposure_accessors = code_for_base_types - .clone() - .map(|(base_exposure_accessor, ..)| base_exposure_accessor); - - let base_exposures_members = code_for_base_types - .clone() - .map(|(_, base_exposure_member, ..)| base_exposure_member); - - let base_exposures_instantiations = code_for_base_types - .clone() - .map(|(_, _, base_exposure_instantiation, ..)| base_exposure_instantiation); - - let base_exposures_invocations = code_for_base_types - .clone() - .map(|(_, _, _, base_exposure_invocation, ..)| base_exposure_invocation); - - let base_services_meta = - code_for_base_types.map(|(_, _, _, _, base_service_meta)| base_service_meta); - - let events_listeners_code = events_type.map(|_| generate_event_listeners(&sails_path)); - - let lifetimes = shared::extract_lifetime_names(&service_type_args); - let exposure_set_event_listener_code = events_type.map(|t| { - // get non conflicting lifetime name - - let mut lt = "__elg".to_owned(); - while lifetimes.contains(<) { - lt = format!("_{}", lt); - } - let lifetime_name = format!("'{0}", lt); - generate_exposure_set_event_listener(t, Lifetime::new(&lifetime_name, Span::call_site())) - }); - - let exposure_drop_code = events_type.map(|_| generate_exposure_drop()); - - let no_events_type = Path::from(Ident::new("NoEvents", Span::call_site())); - let events_type = events_type.unwrap_or(&no_events_type); - - let unexpected_route_panic = - shared::generate_unexpected_input_panic(&input_ident, "Unknown request", &sails_path); - - let mut exposure_lifetimes: Punctuated = Punctuated::new(); - if !service_args.base_types().is_empty() { - for lt in lifetimes.iter().map(|lt| { - let lt = format!("'{lt}"); - Lifetime::new(<, Span::call_site()) - }) { - exposure_lifetimes.push(lt); - } - }; - - let exposure_generic_args = if exposure_lifetimes.is_empty() { - quote! { T } - } else { - quote! { #exposure_lifetimes, T } - }; - let exposure_args = if exposure_lifetimes.is_empty() { - quote! { #service_type_path } - } else { - quote! { #exposure_lifetimes, #service_type_path } - }; - - // We propagate only known attributes as we don't know the consequences of unknown ones - let exposure_allow_attrs = service_impl - .attrs - .iter() - .filter(|attr| matches!(attr.path().get_ident(), Some(ident) if ident == "allow")); - - quote!( - #service_impl - - pub struct Exposure<#exposure_generic_args> { - #message_id_ident : #sails_path::MessageId, - #route_ident : &'static [u8], - #[cfg(not(target_arch = "wasm32"))] - #inner_ident : Box, // Ensure service is not movable - #[cfg(not(target_arch = "wasm32"))] - #inner_ptr_ident : *const T, // Prevent exposure being Send + Sync - #[cfg(target_arch = "wasm32")] - #inner_ident : T, - #( #base_exposures_members )* - } - - #exposure_drop_code - - #( #exposure_allow_attrs )* - impl #generics Exposure< #exposure_args > #service_type_constraints { - #( #exposure_funcs )* - - #( #base_exposure_accessors )* - - pub async fn handle(&mut self, #input_ident: &[u8]) -> (Vec, u128) { - self.try_handle( #input_ident ).await.unwrap_or_else(|| { - #unexpected_route_panic - }) - } - - pub async fn try_handle(&mut self, #input_ident : &[u8]) -> Option<(Vec, u128)> { - #( #invocation_dispatches )* - #( #base_exposures_invocations )* - None - } - - #( #invocation_funcs )* - - #exposure_set_event_listener_code - } - - impl #generics #sails_path::gstd::services::Exposure for Exposure< #exposure_args > #service_type_constraints { - fn message_id(&self) -> #sails_path::MessageId { - self. #message_id_ident - } - - fn route(&self) -> &'static [u8] { - self. #route_ident - } - } - - impl #generics #sails_path::gstd::services::Service for #service_type_path #service_type_constraints { - type Exposure = Exposure< #exposure_args >; - - fn expose(self, #message_id_ident : #sails_path::MessageId, #route_ident : &'static [u8]) -> Self::Exposure { - #[cfg(not(target_arch = "wasm32"))] - let inner_box = Box::new(self); - #[cfg(not(target_arch = "wasm32"))] - let #inner_ident = inner_box.as_ref(); - #[cfg(target_arch = "wasm32")] - let #inner_ident = &self; - Self::Exposure { - #message_id_ident , - #route_ident , - #( #base_exposures_instantiations )* - #[cfg(not(target_arch = "wasm32"))] - #inner_ptr_ident : inner_box.as_ref() as *const Self, - #[cfg(not(target_arch = "wasm32"))] - #inner_ident : inner_box , - #[cfg(target_arch = "wasm32")] - #inner_ident : self, - } - } - } - - impl #generics #sails_path::meta::ServiceMeta for #service_type_path #service_type_constraints { - fn commands() -> #scale_info_path ::MetaType { - #scale_info_path ::MetaType::new::() - } - - fn queries() -> #scale_info_path ::MetaType { - #scale_info_path ::MetaType::new::() - } - - fn events() -> #scale_info_path ::MetaType { - #scale_info_path ::MetaType::new::() - } - - fn base_services() -> impl Iterator { - [ - #( #base_services_meta ),* - ].into_iter() - } - } - - #events_listeners_code - - use #sails_path ::Decode as __ServiceDecode; - use #sails_path ::Encode as __ServiceEncode; - use #sails_path ::TypeInfo as __ServiceTypeInfo; - - #( - #[derive(__ServiceDecode, __ServiceTypeInfo)] - #[codec(crate = #scale_codec_path )] - #[scale_info(crate = #scale_info_path )] - #invocation_params_structs - )* - - mod meta_in_service { - use super::*; - - #[derive(__ServiceTypeInfo)] - #[scale_info(crate = #scale_info_path)] - pub enum CommandsMeta { - #(#commands_meta_variants),* - } - - #[derive(__ServiceTypeInfo)] - #[scale_info(crate = #scale_info_path)] - pub enum QueriesMeta { - #(#queries_meta_variants),* - } - - #[derive(__ServiceTypeInfo)] - #[scale_info(crate = #scale_info_path )] - pub enum #no_events_type {} - - pub type EventsMeta = #events_type; - } - ) -} - -// Generates function for accessing event listeners map in non-wasm code. -fn generate_event_listeners(sails_path: &Path) -> TokenStream { - quote!( - type __EventlistenersMap = #sails_path::collections::BTreeMap; - type __Mutex = #sails_path::spin::Mutex; - - #[cfg(not(target_arch = "wasm32"))] - fn event_listeners() -> &'static __Mutex<__EventlistenersMap> { - static EVENT_LISTENERS: __Mutex<__EventlistenersMap> = - __Mutex::new(__EventlistenersMap::new()); - &EVENT_LISTENERS - } - - #[cfg(not(target_arch = "wasm32"))] - pub struct EventListenerGuard<'a> { - service_ptr: usize, - listener_ptr: usize, - _phantom: core::marker::PhantomData<&'a ()>, - } - - #[cfg(not(target_arch = "wasm32"))] - impl<'a> Drop for EventListenerGuard<'a> { - fn drop(&mut self) { - let mut event_listeners = event_listeners().lock(); - let listener_ptr = event_listeners.remove(&self.service_ptr); - if listener_ptr != Some(self.listener_ptr) { - panic!("event listener is being removed out of order"); - } - } - } - ) -} - -fn generate_exposure_drop() -> TokenStream { - quote!( - #[cfg(not(target_arch = "wasm32"))] - impl Drop for Exposure { - fn drop(&mut self) { - let service_ptr = self.inner_ptr as usize; - let mut event_listeners = event_listeners().lock(); - if event_listeners.remove(&service_ptr).is_some() { - panic!("there should be no any event listeners left by this time"); - } - } - } - ) -} - -fn generate_exposure_set_event_listener(events_type: &Path, lifetime: Lifetime) -> TokenStream { - quote!( - #[cfg(not(target_arch = "wasm32"))] - // Immutable so one can set it via AsRef when used with extending - pub fn set_event_listener<#lifetime>( - &self, - listener: impl FnMut(& #events_type ) + #lifetime, - ) -> EventListenerGuard<#lifetime> { - if core::mem::size_of_val(self.inner.as_ref()) == 0 { - panic!("setting event listener on a zero-sized service is not supported for now"); - } - let service_ptr = self.inner_ptr as usize; - let listener: Box = Box::new(listener); - let listener = Box::new(listener); - let listener_ptr = Box::into_raw(listener) as usize; - let mut event_listeners = event_listeners().lock(); - if event_listeners.contains_key(&service_ptr) { - panic!("event listener is already set"); - } - event_listeners.insert(service_ptr, listener_ptr); - EventListenerGuard { - service_ptr, - listener_ptr, - _phantom: core::marker::PhantomData, - } - } - ) -} - -fn discover_service_handlers(service_impl: &ItemImpl) -> BTreeMap { - shared::discover_invocation_targets(service_impl, |fn_item| { - matches!(fn_item.vis, Visibility::Public(_)) && fn_item.sig.receiver().is_some() - }) -} - -struct HandlerGenerator<'a> { - handler: Func<'a>, - result_type: Type, - reply_with_value: bool, - is_query: bool, -} - -impl<'a> HandlerGenerator<'a> { - fn from(handler: Func<'a>) -> Self { - // process result type to extact value and replace any lifetime with 'static - let (result_type, reply_with_value) = - shared::extract_reply_type_with_value(handler.result()) - .map_or_else(|| (handler.result().clone(), false), |t| (t, true)); - let result_type = shared::replace_any_lifetime_with_static(result_type); - let is_query = handler.receiver().map_or(true, |r| r.mutability.is_none()); - - if reply_with_value && is_query { - abort!( - handler.result().span(), - "using `CommandReply` type in a query is not allowed" - ); - } - - Self { - handler, - result_type, - reply_with_value, - is_query, - } - } - - fn params_struct_ident(&self) -> Ident { - Ident::new( - &format!( - "__{}Params", - self.handler.ident().to_string().to_case(Case::Pascal) - ), - Span::call_site(), - ) - } - - fn result_type(&self) -> &Type { - &self.result_type - } - - fn handler_func_ident(&self) -> Ident { - self.handler.ident().clone() - } - - fn invocation_func_ident(&self) -> Ident { - Ident::new( - &format!("__{}", self.handler_func_ident()), - Span::call_site(), - ) - } - - fn is_query(&self) -> bool { - self.is_query - } - - fn reply_with_value(&self) -> bool { - self.reply_with_value - } - - fn params_struct(&self) -> TokenStream { - let params_struct_ident = self.params_struct_ident(); - let params_struct_members = self.handler.params().iter().map(|item| { - let arg_ident = item.0; - let arg_type = item.1; - quote!(#arg_ident: #arg_type) - }); - - quote!( - pub struct #params_struct_ident { - #(#params_struct_members),* - } - ) - } - - fn invocation_func(&self) -> TokenStream { - let invocation_func_ident = self.invocation_func_ident(); - let receiver = self.handler.receiver(); - let params_struct_ident = self.params_struct_ident(); - let handler_func_ident = self.handler_func_ident(); - let handler_func_params = self.handler.params().iter().map(|item| { - let param_ident = item.0; - quote!(request.#param_ident) - }); - - let result_type = self.result_type(); - let await_token = self.handler.is_async().then(|| quote!(.await)); - let handle_token = if self.reply_with_value() { - quote! { - let command_reply: CommandReply<#result_type> = self.#handler_func_ident(#(#handler_func_params),*)#await_token.into(); - let (result, value) = command_reply.to_tuple(); - } - } else { - quote! { - let result = self.#handler_func_ident(#(#handler_func_params),*)#await_token; - let value = 0u128; - } - }; - - quote!( - async fn #invocation_func_ident(#receiver, mut input: &[u8]) -> (Vec, u128) - { - let request = #params_struct_ident::decode(&mut input).expect("Failed to decode request"); - #handle_token - return (result.encode(), value); - } - ) - } -} +// This file is part of Gear. + +// Copyright (C) 2021-2023 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Supporting functions and structures for the `gservice` macro. + +use crate::{ + sails_paths, + shared::{self, Func}, +}; +use args::ServiceArgs; +use convert_case::{Case, Casing}; +use parity_scale_codec::Encode; +use proc_macro2::{Span, TokenStream}; +use proc_macro_error::abort; +use quote::quote; +use std::collections::BTreeMap; +use syn::{ + parse_quote, punctuated::Punctuated, spanned::Spanned, token::Comma, Ident, ImplItemFn, + ItemImpl, Lifetime, Path, Type, Visibility, +}; + +mod args; + +static mut SERVICE_SPANS: BTreeMap = BTreeMap::new(); + +pub fn gservice(args: TokenStream, service_impl: TokenStream) -> TokenStream { + let service_impl = parse_gservice_impl(service_impl); + ensure_single_gservice_on_impl(&service_impl); + ensure_single_gservice_by_name(&service_impl); + generate_gservice(args, service_impl) +} + +#[doc(hidden)] +pub fn __gservice_internal(args: TokenStream, service_impl: TokenStream) -> TokenStream { + let service_impl = parse_gservice_impl(service_impl); + generate_gservice(args, service_impl) +} + +fn parse_gservice_impl(service_impl_tokens: TokenStream) -> ItemImpl { + syn::parse2(service_impl_tokens).unwrap_or_else(|err| { + abort!( + err.span(), + "`service` attribute can be applied to impls only: {}", + err + ) + }) +} + +fn ensure_single_gservice_on_impl(service_impl: &ItemImpl) { + let attr_gservice = service_impl.attrs.iter().find(|attr| { + attr.meta + .path() + .segments + .last() + .map(|s| s.ident == "service") + .unwrap_or(false) + }); + if attr_gservice.is_some() { + abort!( + service_impl, + "multiple `service` attributes on the same impl are not allowed", + ) + } +} + +fn ensure_single_gservice_by_name(service_impl: &ItemImpl) { + let (path, ..) = shared::impl_type(service_impl); + let type_ident = path.path.segments.last().unwrap().ident.to_string(); + if unsafe { SERVICE_SPANS.get(&type_ident) }.is_some() { + abort!( + service_impl, + "multiple `service` attributes on a type with the same name are not allowed" + ) + } + unsafe { SERVICE_SPANS.insert(type_ident, service_impl.span()) }; +} + +fn generate_gservice(args: TokenStream, service_impl: ItemImpl) -> TokenStream { + let service_args = syn::parse2::(args).unwrap_or_else(|err| { + abort!( + err.span(), + "failed to parse `service` attribute arguments: {}", + err + ) + }); + let sails_path = service_args.sails_path(); + let scale_codec_path = sails_paths::scale_codec_path(&sails_path); + let scale_info_path = sails_paths::scale_info_path(&sails_path); + + let (service_type_path, service_type_args) = shared::impl_type(&service_impl); + let (generics, service_type_constraints) = shared::impl_constraints(&service_impl); + + let service_handlers = discover_service_handlers(&service_impl); + + if service_handlers.is_empty() && service_args.base_types().is_empty() { + abort!( + service_impl, + "`service` attribute requires impl to define at least one public method or extend another service" + ); + } + + let events_type = service_args.events_type().as_ref(); + + let mut service_impl = service_impl.clone(); + if let Some(events_type) = events_type { + service_impl.items.push(parse_quote!( + fn notify_on(&mut self, event: #events_type ) -> #sails_path::errors::Result<()> { + #[cfg(not(target_arch = "wasm32"))] + { + let self_ptr = self as *const _ as usize; + let event_listeners = event_listeners().lock(); + if let Some(event_listener_ptr) = event_listeners.get(&self_ptr) { + let event_listener = + unsafe { &mut *(*event_listener_ptr as *mut Box) }; + core::mem::drop(event_listeners); + event_listener(&event); + } + } + #sails_path::gstd::events::__notify_on(event) + } + )); + } + + let inner_ident = Ident::new("inner", Span::call_site()); + let inner_ptr_ident = Ident::new("inner_ptr", Span::call_site()); + let input_ident = Ident::new("input", Span::call_site()); + + let mut exposure_funcs = Vec::with_capacity(service_handlers.len()); + let mut invocation_params_structs = Vec::with_capacity(service_handlers.len()); + let mut invocation_funcs = Vec::with_capacity(service_handlers.len()); + let mut invocation_dispatches = Vec::with_capacity(service_handlers.len()); + let mut commands_meta_variants = Vec::with_capacity(service_handlers.len()); + let mut queries_meta_variants = Vec::with_capacity(service_handlers.len()); + + for (handler_route, (handler_fn, ..)) in &service_handlers { + // We propagate only known attributes as we don't know the consequences of unknown ones + let handler_allow_attrs = handler_fn + .attrs + .iter() + .filter(|attr| attr.path().is_ident("allow")); + let handler_docs_attrs = handler_fn + .attrs + .iter() + .filter(|attr| attr.path().is_ident("doc")); + + let handler_fn = &handler_fn.sig; + let handler_func = Func::from(handler_fn); + let handler_generator = HandlerGenerator::from(handler_func.clone()); + let invocation_func_ident = handler_generator.invocation_func_ident(); + + exposure_funcs.push({ + let handler_ident = handler_func.ident(); + let handler_params = handler_func.params().iter().map(|item| item.0); + let handler_await_token = handler_func.is_async().then(|| quote!(.await)); + quote!( + #( #handler_allow_attrs )* + pub #handler_fn { + let exposure_scope = #sails_path::gstd::services::ExposureCallScope::new(self); + self. #inner_ident . #handler_ident (#(#handler_params),*) #handler_await_token + } + ) + }); + invocation_params_structs.push(handler_generator.params_struct()); + invocation_funcs.push(handler_generator.invocation_func()); + invocation_dispatches.push({ + let handler_route_bytes = handler_route.encode(); + let handler_route_len = handler_route_bytes.len(); + quote!( + if #input_ident.starts_with(& [ #(#handler_route_bytes),* ]) { + let (output, value) = self.#invocation_func_ident(&#input_ident[#handler_route_len..]).await; + static INVOCATION_ROUTE: [u8; #handler_route_len] = [ #(#handler_route_bytes),* ]; + return Some(([INVOCATION_ROUTE.as_ref(), &output].concat(), value)); + } + ) + }); + + let handler_meta_variant = { + let params_struct_ident = handler_generator.params_struct_ident(); + let result_type = handler_generator.result_type(); + let handler_route_ident = Ident::new(handler_route, Span::call_site()); + + quote!( + #( #handler_docs_attrs )* + #handler_route_ident(#params_struct_ident, #result_type) + ) + }; + if handler_generator.is_query() { + queries_meta_variants.push(handler_meta_variant); + } else { + commands_meta_variants.push(handler_meta_variant); + } + } + + let message_id_ident = Ident::new("message_id", Span::call_site()); + let route_ident = Ident::new("route", Span::call_site()); + + let code_for_base_types = service_args.base_types().iter() + .enumerate() + .map(|(idx, base_type)| { + let base_ident = Ident::new(&format!("base_{}", idx), Span::call_site()); + let as_base_ident = Ident::new(&format!("as_base_{}", idx), Span::call_site()); + + let base_exposure_accessor = quote!( + pub fn #as_base_ident (&self) -> &< #base_type as #sails_path::gstd::services::Service>::Exposure { + &self. #base_ident + } + ); + + let base_exposure_member = quote!( + #base_ident : < #base_type as #sails_path::gstd::services::Service>::Exposure, + ); + + let base_exposure_instantiation = quote!( + #base_ident : < #base_type as Clone>::clone(AsRef::< #base_type >::as_ref( #inner_ident )) + .expose( #message_id_ident , #route_ident ), + ); + + let base_exposure_invocation = quote!( + if let Some((output, value)) = self. #base_ident .try_handle(#input_ident).await { + return Some((output, value)); + } + ); + + let base_service_meta = quote!(#sails_path::meta::AnyServiceMeta::new::< #base_type >()); + + (base_exposure_accessor, base_exposure_member, base_exposure_instantiation, base_exposure_invocation, base_service_meta) + }); + + let base_exposure_accessors = code_for_base_types + .clone() + .map(|(base_exposure_accessor, ..)| base_exposure_accessor); + + let base_exposures_members = code_for_base_types + .clone() + .map(|(_, base_exposure_member, ..)| base_exposure_member); + + let base_exposures_instantiations = code_for_base_types + .clone() + .map(|(_, _, base_exposure_instantiation, ..)| base_exposure_instantiation); + + let base_exposures_invocations = code_for_base_types + .clone() + .map(|(_, _, _, base_exposure_invocation, ..)| base_exposure_invocation); + + let base_services_meta = + code_for_base_types.map(|(_, _, _, _, base_service_meta)| base_service_meta); + + let events_listeners_code = events_type.map(|_| generate_event_listeners(&sails_path)); + + let lifetimes = shared::extract_lifetime_names(&service_type_args); + let exposure_set_event_listener_code = events_type.map(|t| { + // get non conflicting lifetime name + + let mut lt = "__elg".to_owned(); + while lifetimes.contains(<) { + lt = format!("_{}", lt); + } + let lifetime_name = format!("'{0}", lt); + generate_exposure_set_event_listener(t, Lifetime::new(&lifetime_name, Span::call_site())) + }); + + let exposure_drop_code = events_type.map(|_| generate_exposure_drop()); + + let no_events_type = Path::from(Ident::new("NoEvents", Span::call_site())); + let events_type = events_type.unwrap_or(&no_events_type); + + let unexpected_route_panic = + shared::generate_unexpected_input_panic(&input_ident, "Unknown request", &sails_path); + + let mut exposure_lifetimes: Punctuated = Punctuated::new(); + if !service_args.base_types().is_empty() { + for lt in lifetimes.iter().map(|lt| { + let lt = format!("'{lt}"); + Lifetime::new(<, Span::call_site()) + }) { + exposure_lifetimes.push(lt); + } + }; + + let exposure_generic_args = if exposure_lifetimes.is_empty() { + quote! { T } + } else { + quote! { #exposure_lifetimes, T } + }; + let exposure_args = if exposure_lifetimes.is_empty() { + quote! { #service_type_path } + } else { + quote! { #exposure_lifetimes, #service_type_path } + }; + + // We propagate only known attributes as we don't know the consequences of unknown ones + let exposure_allow_attrs = service_impl + .attrs + .iter() + .filter(|attr| matches!(attr.path().get_ident(), Some(ident) if ident == "allow")); + + quote!( + #service_impl + + pub struct Exposure<#exposure_generic_args> { + #message_id_ident : #sails_path::MessageId, + #route_ident : &'static [u8], + #[cfg(not(target_arch = "wasm32"))] + #inner_ident : Box, // Ensure service is not movable + #[cfg(not(target_arch = "wasm32"))] + #inner_ptr_ident : *const T, // Prevent exposure being Send + Sync + #[cfg(target_arch = "wasm32")] + #inner_ident : T, + #( #base_exposures_members )* + } + + #exposure_drop_code + + #( #exposure_allow_attrs )* + impl #generics Exposure< #exposure_args > #service_type_constraints { + #( #exposure_funcs )* + + #( #base_exposure_accessors )* + + pub async fn handle(&mut self, #input_ident: &[u8]) -> (Vec, u128) { + self.try_handle( #input_ident ).await.unwrap_or_else(|| { + #unexpected_route_panic + }) + } + + pub async fn try_handle(&mut self, #input_ident : &[u8]) -> Option<(Vec, u128)> { + #( #invocation_dispatches )* + #( #base_exposures_invocations )* + None + } + + #( #invocation_funcs )* + + #exposure_set_event_listener_code + } + + impl #generics #sails_path::gstd::services::Exposure for Exposure< #exposure_args > #service_type_constraints { + fn message_id(&self) -> #sails_path::MessageId { + self. #message_id_ident + } + + fn route(&self) -> &'static [u8] { + self. #route_ident + } + } + + impl #generics #sails_path::gstd::services::Service for #service_type_path #service_type_constraints { + type Exposure = Exposure< #exposure_args >; + + fn expose(self, #message_id_ident : #sails_path::MessageId, #route_ident : &'static [u8]) -> Self::Exposure { + #[cfg(not(target_arch = "wasm32"))] + let inner_box = Box::new(self); + #[cfg(not(target_arch = "wasm32"))] + let #inner_ident = inner_box.as_ref(); + #[cfg(target_arch = "wasm32")] + let #inner_ident = &self; + Self::Exposure { + #message_id_ident , + #route_ident , + #( #base_exposures_instantiations )* + #[cfg(not(target_arch = "wasm32"))] + #inner_ptr_ident : inner_box.as_ref() as *const Self, + #[cfg(not(target_arch = "wasm32"))] + #inner_ident : inner_box , + #[cfg(target_arch = "wasm32")] + #inner_ident : self, + } + } + } + + impl #generics #sails_path::meta::ServiceMeta for #service_type_path #service_type_constraints { + fn commands() -> #scale_info_path ::MetaType { + #scale_info_path ::MetaType::new::() + } + + fn queries() -> #scale_info_path ::MetaType { + #scale_info_path ::MetaType::new::() + } + + fn events() -> #scale_info_path ::MetaType { + #scale_info_path ::MetaType::new::() + } + + fn base_services() -> impl Iterator { + [ + #( #base_services_meta ),* + ].into_iter() + } + } + + #events_listeners_code + + use #sails_path ::Decode as __ServiceDecode; + use #sails_path ::Encode as __ServiceEncode; + use #sails_path ::TypeInfo as __ServiceTypeInfo; + + #( + #[derive(__ServiceDecode, __ServiceTypeInfo)] + #[codec(crate = #scale_codec_path )] + #[scale_info(crate = #scale_info_path )] + #invocation_params_structs + )* + + mod meta_in_service { + use super::*; + + #[derive(__ServiceTypeInfo)] + #[scale_info(crate = #scale_info_path)] + pub enum CommandsMeta { + #(#commands_meta_variants),* + } + + #[derive(__ServiceTypeInfo)] + #[scale_info(crate = #scale_info_path)] + pub enum QueriesMeta { + #(#queries_meta_variants),* + } + + #[derive(__ServiceTypeInfo)] + #[scale_info(crate = #scale_info_path )] + pub enum #no_events_type {} + + pub type EventsMeta = #events_type; + } + ) +} + +// Generates function for accessing event listeners map in non-wasm code. +fn generate_event_listeners(sails_path: &Path) -> TokenStream { + quote!( + type __EventlistenersMap = #sails_path::collections::BTreeMap; + type __Mutex = #sails_path::spin::Mutex; + + #[cfg(not(target_arch = "wasm32"))] + fn event_listeners() -> &'static __Mutex<__EventlistenersMap> { + static EVENT_LISTENERS: __Mutex<__EventlistenersMap> = + __Mutex::new(__EventlistenersMap::new()); + &EVENT_LISTENERS + } + + #[cfg(not(target_arch = "wasm32"))] + pub struct EventListenerGuard<'a> { + service_ptr: usize, + listener_ptr: usize, + _phantom: core::marker::PhantomData<&'a ()>, + } + + #[cfg(not(target_arch = "wasm32"))] + impl<'a> Drop for EventListenerGuard<'a> { + fn drop(&mut self) { + let mut event_listeners = event_listeners().lock(); + let listener_ptr = event_listeners.remove(&self.service_ptr); + if listener_ptr != Some(self.listener_ptr) { + panic!("event listener is being removed out of order"); + } + } + } + ) +} + +fn generate_exposure_drop() -> TokenStream { + quote!( + #[cfg(not(target_arch = "wasm32"))] + impl Drop for Exposure { + fn drop(&mut self) { + let service_ptr = self.inner_ptr as usize; + let mut event_listeners = event_listeners().lock(); + if event_listeners.remove(&service_ptr).is_some() { + panic!("there should be no any event listeners left by this time"); + } + } + } + ) +} + +fn generate_exposure_set_event_listener(events_type: &Path, lifetime: Lifetime) -> TokenStream { + quote!( + #[cfg(not(target_arch = "wasm32"))] + // Immutable so one can set it via AsRef when used with extending + pub fn set_event_listener<#lifetime>( + &self, + listener: impl FnMut(& #events_type ) + #lifetime, + ) -> EventListenerGuard<#lifetime> { + if core::mem::size_of_val(self.inner.as_ref()) == 0 { + panic!("setting event listener on a zero-sized service is not supported for now"); + } + let service_ptr = self.inner_ptr as usize; + let listener: Box = Box::new(listener); + let listener = Box::new(listener); + let listener_ptr = Box::into_raw(listener) as usize; + let mut event_listeners = event_listeners().lock(); + if event_listeners.contains_key(&service_ptr) { + panic!("event listener is already set"); + } + event_listeners.insert(service_ptr, listener_ptr); + EventListenerGuard { + service_ptr, + listener_ptr, + _phantom: core::marker::PhantomData, + } + } + ) +} + +fn discover_service_handlers(service_impl: &ItemImpl) -> BTreeMap { + shared::discover_invocation_targets(service_impl, |fn_item| { + matches!(fn_item.vis, Visibility::Public(_)) && fn_item.sig.receiver().is_some() + }) +} + +struct HandlerGenerator<'a> { + handler: Func<'a>, + result_type: Type, + reply_with_value: bool, + is_query: bool, +} + +impl<'a> HandlerGenerator<'a> { + fn from(handler: Func<'a>) -> Self { + // process result type to extact value and replace any lifetime with 'static + let (result_type, reply_with_value) = + shared::extract_reply_type_with_value(handler.result()) + .map_or_else(|| (handler.result().clone(), false), |t| (t, true)); + let result_type = shared::replace_any_lifetime_with_static(result_type); + let is_query = handler.receiver().map_or(true, |r| r.mutability.is_none()); + + if reply_with_value && is_query { + abort!( + handler.result().span(), + "using `CommandReply` type in a query is not allowed" + ); + } + + Self { + handler, + result_type, + reply_with_value, + is_query, + } + } + + fn params_struct_ident(&self) -> Ident { + Ident::new( + &format!( + "__{}Params", + self.handler.ident().to_string().to_case(Case::Pascal) + ), + Span::call_site(), + ) + } + + fn result_type(&self) -> &Type { + &self.result_type + } + + fn handler_func_ident(&self) -> Ident { + self.handler.ident().clone() + } + + fn invocation_func_ident(&self) -> Ident { + Ident::new( + &format!("__{}", self.handler_func_ident()), + Span::call_site(), + ) + } + + fn is_query(&self) -> bool { + self.is_query + } + + fn reply_with_value(&self) -> bool { + self.reply_with_value + } + + fn params_struct(&self) -> TokenStream { + let params_struct_ident = self.params_struct_ident(); + let params_struct_members = self.handler.params().iter().map(|item| { + let arg_ident = item.0; + let arg_type = item.1; + quote!(#arg_ident: #arg_type) + }); + + quote!( + pub struct #params_struct_ident { + #(#params_struct_members),* + } + ) + } + + fn invocation_func(&self) -> TokenStream { + let invocation_func_ident = self.invocation_func_ident(); + let receiver = self.handler.receiver(); + let params_struct_ident = self.params_struct_ident(); + let handler_func_ident = self.handler_func_ident(); + let handler_func_params = self.handler.params().iter().map(|item| { + let param_ident = item.0; + quote!(request.#param_ident) + }); + + let result_type = self.result_type(); + let await_token = self.handler.is_async().then(|| quote!(.await)); + let handle_token = if self.reply_with_value() { + quote! { + let command_reply: CommandReply<#result_type> = self.#handler_func_ident(#(#handler_func_params),*)#await_token.into(); + let (result, value) = command_reply.to_tuple(); + } + } else { + quote! { + let result = self.#handler_func_ident(#(#handler_func_params),*)#await_token; + let value = 0u128; + } + }; + + quote!( + async fn #invocation_func_ident(#receiver, mut input: &[u8]) -> (Vec, u128) + { + let request = #params_struct_ident::decode(&mut input).expect("Failed to decode request"); + #handle_token + return (result.encode(), value); + } + ) + } +} diff --git a/templates/program/CargoTemplate.toml b/templates/program/CargoTemplate.toml index 426a7e63..8e0be7f4 100644 --- a/templates/program/CargoTemplate.toml +++ b/templates/program/CargoTemplate.toml @@ -1,26 +1,26 @@ -[workspace] -{% if with-client %} -members = ["client"] -{% endif %} - -[package] -name = "{{ project-name }}" -version = "0.1.0" -edition = "2021" - -[dependencies] -{{ app-project-name }} = { path = "app" } - -[build-dependencies] -{{ app-project-name }} = { path = "app" } -sails-rs = { version = "{{ sails-rs-version }}", features = ["wasm-builder"] } -sails-idl-gen = "{{ sails-rs-version }}" -{% if with-client and with-gtest %} -[dev-dependencies] -{{ project-name }} = { path = ".", features = ["wasm-binary"] } -{{ client-project-name }} = { path = "client" } -sails-rs = { version = "{{ sails-rs-version }}", features = ["gtest"] } -tokio = { version = "{{ tokio-version }}", features = ["rt", "macros"] } -{% endif %} -[features] -wasm-binary = [] +[workspace] +{% if with-client %} +members = ["client"] +{% endif %} + +[package] +name = "{{ project-name }}" +version = "0.1.0" +edition = "2021" + +[dependencies] +{{ app-project-name }} = { path = "app" } + +[build-dependencies] +{{ app-project-name }} = { path = "app" } +sails-rs = { version = "{{ sails-rs-version }}", features = ["wasm-builder"] } +sails-idl-gen = "{{ sails-rs-version }}" +{% if with-client and with-gtest %} +[dev-dependencies] +{{ project-name }} = { path = ".", features = ["wasm-binary"] } +{{ client-project-name }} = { path = "client" } +sails-rs = { version = "{{ sails-rs-version }}", features = ["gtest"] } +tokio = { version = "{{ tokio-version }}", features = ["rt", "macros"] } +{% endif %} +[features] +wasm-binary = [] From ce67abc3ccb61c561d706902e8599f85889d8cf5 Mon Sep 17 00:00:00 2001 From: vobradovich Date: Tue, 22 Oct 2024 15:25:34 +0200 Subject: [PATCH 12/13] get list of packages from root, add deps_level param --- rs/cli/src/idlgen.rs | 88 +++++++++++++++++++++++++++++++------------- rs/cli/src/main.rs | 6 ++- 2 files changed, 67 insertions(+), 27 deletions(-) diff --git a/rs/cli/src/idlgen.rs b/rs/cli/src/idlgen.rs index 0c367cba..ab6e55ea 100644 --- a/rs/cli/src/idlgen.rs +++ b/rs/cli/src/idlgen.rs @@ -1,6 +1,7 @@ use anyhow::Context; -use cargo_metadata::{camino::*, Package}; +use cargo_metadata::{camino::*, Package, PackageId}; use std::{ + collections::{HashMap, HashSet}, env, fs, path::{Path, PathBuf}, process::{Command, ExitStatus}, @@ -9,10 +10,15 @@ use std::{ pub struct CrateIdlGenerator { manifest_path: Utf8PathBuf, target_dir: Option, + deps_level: Option, } impl CrateIdlGenerator { - pub fn new(manifest_path: Option, target_dir: Option) -> Self { + pub fn new( + manifest_path: Option, + target_dir: Option, + deps_level: Option, + ) -> Self { Self { manifest_path: Utf8PathBuf::from_path_buf( manifest_path.unwrap_or_else(|| env::current_dir().unwrap().join("Cargo.toml")), @@ -22,6 +28,7 @@ impl CrateIdlGenerator { .and_then(|p| p.canonicalize().ok()) .map(Utf8PathBuf::from_path_buf) .and_then(|t| t.ok()), + deps_level, } } @@ -39,35 +46,16 @@ impl CrateIdlGenerator { .filter(|&p| p.name == "sails-rs") .collect::>(); - let resolve = &metadata - .resolve - .context("failed to get resolve from metadata")?; - let root_pacakge_id = resolve - .root - .as_ref() - .context("failed to find root package")?; - let root_node = resolve - .nodes - .iter() - .find(|&node| &node.id == root_pacakge_id) - .context("failed to find root package")?; - - // find root package and it dependencies in workspace members - let package_list = &metadata - .packages - .iter() - .filter(|&p| { - &p.id == root_pacakge_id - || (metadata.workspace_members.contains(&p.id) - && root_node.dependencies.contains(&p.id)) - }) - .collect::>(); - let target_dir = self .target_dir .as_ref() .unwrap_or(&metadata.target_directory); + let package_list = get_package_list(&metadata, self.deps_level.unwrap_or(1))?; + println!( + "...looking for Program implemetation in {} packages", + package_list.len() + ); for program_package in package_list { let idl_gen = PackageIdlGenerator::new( program_package, @@ -207,6 +195,54 @@ impl<'a> PackageIdlGenerator<'a> { } } +/// Get list of packages from the root package and its dependencies +fn get_package_list( + metadata: &cargo_metadata::Metadata, + deps_level: usize, +) -> Result, anyhow::Error> { + let resolve = metadata + .resolve + .as_ref() + .context("failed to get resolve from metadata")?; + let root_package_id = resolve + .root + .as_ref() + .context("failed to find root package")?; + let node_map = resolve + .nodes + .iter() + .map(|n| (&n.id, n)) + .collect::>(); + let package_map = metadata + .packages + .iter() + .map(|p| (&p.id, p)) + .collect::>(); + + let mut deps_set: HashSet<&PackageId> = HashSet::new(); + deps_set.insert(root_package_id); + + let mut deps = vec![root_package_id]; + for _ in 0..deps_level { + deps = deps + .iter() + .filter_map(|id| node_map.get(id)) + .flat_map(|&n| &n.dependencies) + .filter(|&id| metadata.workspace_members.contains(id)) + .collect(); + if deps.is_empty() { + break; + } + deps_set.extend(deps.iter()); + } + let package_list: Vec<&Package> = deps_set + .iter() + .filter_map(|id| package_map.get(id)) + .copied() + .collect(); + Ok(package_list) +} + fn try_get_trait_implementation_path( idx: &rustdoc_types::Item, program_meta_id: &rustdoc_types::Id, diff --git a/rs/cli/src/main.rs b/rs/cli/src/main.rs index ad2d00a7..ca05be55 100644 --- a/rs/cli/src/main.rs +++ b/rs/cli/src/main.rs @@ -60,6 +60,9 @@ enum SailsCommands { /// Directory for all generated artifacts #[arg(long, value_hint = clap::ValueHint::DirPath)] target_dir: Option, + /// Level of dependencies to look for program implementation. Default: 1 + #[arg(long)] + deps_level: Option, }, } @@ -120,7 +123,8 @@ fn main() -> Result<(), i32> { SailsCommands::IdlGen { manifest_path, target_dir, - } => CrateIdlGenerator::new(manifest_path, target_dir).generate(), + deps_level, + } => CrateIdlGenerator::new(manifest_path, target_dir, deps_level).generate(), }; if let Err(e) = result { From 5e3bc5c83e619633b7d71dce33e6621f9644f827 Mon Sep 17 00:00:00 2001 From: vobradovich Date: Wed, 23 Oct 2024 12:38:40 +0200 Subject: [PATCH 13/13] fixes, diff idl --- .github/workflows/ci.yml | 1 + Cargo.toml | 2 +- rs/cli/src/idlgen.rs | 86 ++++++++++++++++++++-------------------- 3 files changed, 46 insertions(+), 43 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ce3fa25..eb3fe3bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -151,3 +151,4 @@ jobs: - name: Generate IDL from MyDemo via CLI run: | ./target/debug/cargo-sails sails idl --manifest-path ~/tmp/my-demo/Cargo.toml + diff ~/tmp/my-demo/target/my-demo-app.idl ~/tmp/my-demo/target/wasm32-unknown-unknown/debug/my_demo.idl diff --git a/Cargo.toml b/Cargo.toml index d5916856..e8872567 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,7 +71,7 @@ parity-scale-codec = { version = "3.6", default-features = false } prettyplease = "0.2" proc-macro-error = "1.0" proc-macro2 = { version = "1", default-features = false } -rustdoc-types = { version = "0.29.1" } # replace with official `rustdoc_json_types` after stable release +rustdoc-types = "=0.29.1" quote = "1.0" scale-info = { version = "2.11", default-features = false } serde = "1.0" diff --git a/rs/cli/src/idlgen.rs b/rs/cli/src/idlgen.rs index ab6e55ea..92534128 100644 --- a/rs/cli/src/idlgen.rs +++ b/rs/cli/src/idlgen.rs @@ -10,7 +10,7 @@ use std::{ pub struct CrateIdlGenerator { manifest_path: Utf8PathBuf, target_dir: Option, - deps_level: Option, + deps_level: usize, } impl CrateIdlGenerator { @@ -28,7 +28,7 @@ impl CrateIdlGenerator { .and_then(|p| p.canonicalize().ok()) .map(Utf8PathBuf::from_path_buf) .and_then(|t| t.ok()), - deps_level, + deps_level: deps_level.unwrap_or(1), } } @@ -51,9 +51,9 @@ impl CrateIdlGenerator { .as_ref() .unwrap_or(&metadata.target_directory); - let package_list = get_package_list(&metadata, self.deps_level.unwrap_or(1))?; + let package_list = get_package_list(&metadata, self.deps_level)?; println!( - "...looking for Program implemetation in {} packages", + "...looking for Program implemetation in {} package(s)", package_list.len() ); for program_package in package_list { @@ -63,7 +63,7 @@ impl CrateIdlGenerator { target_dir, &metadata.workspace_root, ); - match idl_gen.get_program_struct_path_from_doc() { + match get_program_struct_path_from_doc(program_package, target_dir) { Ok((program_struct_path, meta_path_version)) => { println!("...found Program implemetation: {}", program_struct_path); let file_path = idl_gen @@ -103,43 +103,6 @@ impl<'a> PackageIdlGenerator<'a> { } } - fn get_program_struct_path_from_doc(&self) -> anyhow::Result<(String, MetaPathVersion)> { - let program_package_file_name = &self.program_package.name.to_lowercase().replace('-', "_"); - println!( - "...running doc generation for `{}`", - &self.program_package.manifest_path - ); - // run `cargo doc` - _ = cargo_doc(&self.program_package.manifest_path, self.target_dir)?; - // read doc - let docs_path = &self - .target_dir - .join("doc") - .join(format!("{}.json", &program_package_file_name)); - println!("...reading doc: {}", docs_path); - let json_string = std::fs::read_to_string(docs_path)?; - let doc_crate: rustdoc_types::Crate = serde_json::from_str(&json_string)?; - - // find `sails_rs::meta::ProgramMeta` path id - let (program_meta_id, meta_path_version) = doc_crate - .paths - .iter() - .find_map(|(id, summary)| MetaPathVersion::matches(&summary.path).map(|v| (id, v))) - .context("failed to find `sails_rs::meta::ProgramMeta` definition in dependencies")?; - // find struct implementing `sails_rs::meta::ProgramMeta` - let program_struct_path = doc_crate - .index - .values() - .find_map(|idx| try_get_trait_implementation_path(idx, program_meta_id)) - .context("failed to find `sails_rs::meta::ProgramMeta` implemetation")?; - let program_struct = doc_crate - .paths - .get(&program_struct_path.id) - .context("failed to get Program struct by id")?; - let program_struct_path = program_struct.path.join("::"); - Ok((program_struct_path, meta_path_version)) - } - fn try_generate_for_package( &self, program_struct_path: &str, @@ -243,6 +206,45 @@ fn get_package_list( Ok(package_list) } +fn get_program_struct_path_from_doc( + program_package: &Package, + target_dir: &Utf8Path, +) -> anyhow::Result<(String, MetaPathVersion)> { + let program_package_file_name = program_package.name.to_lowercase().replace('-', "_"); + println!( + "...running doc generation for `{}`", + program_package.manifest_path + ); + // run `cargo doc` + _ = cargo_doc(&program_package.manifest_path, target_dir)?; + // read doc + let docs_path = target_dir + .join("doc") + .join(format!("{}.json", &program_package_file_name)); + println!("...reading doc: {}", docs_path); + let json_string = std::fs::read_to_string(docs_path)?; + let doc_crate: rustdoc_types::Crate = serde_json::from_str(&json_string)?; + + // find `sails_rs::meta::ProgramMeta` path id + let (program_meta_id, meta_path_version) = doc_crate + .paths + .iter() + .find_map(|(id, summary)| MetaPathVersion::matches(&summary.path).map(|v| (id, v))) + .context("failed to find `sails_rs::meta::ProgramMeta` definition in dependencies")?; + // find struct implementing `sails_rs::meta::ProgramMeta` + let program_struct_path = doc_crate + .index + .values() + .find_map(|idx| try_get_trait_implementation_path(idx, program_meta_id)) + .context("failed to find `sails_rs::meta::ProgramMeta` implemetation")?; + let program_struct = doc_crate + .paths + .get(&program_struct_path.id) + .context("failed to get Program struct by id")?; + let program_struct_path = program_struct.path.join("::"); + Ok((program_struct_path, meta_path_version)) +} + fn try_get_trait_implementation_path( idx: &rustdoc_types::Item, program_meta_id: &rustdoc_types::Id,