From 79278a62f66895cbe3bfaf822e3703309f8847b5 Mon Sep 17 00:00:00 2001 From: glihm Date: Thu, 18 Jan 2024 10:01:59 -0600 Subject: [PATCH] feat: bindgen crate (#1425) * feat: add first version of plugin-like integration for bindgen * feat: add unity backend template * refacto: move the BackendBuilder trait into backends module * docs: adjust function docs * fix: rename all to plugin for clarity * docs: update README * docs: fix typos * fix: ensure only dojo contracts are excluded from bindgen * feat: add DojoMetadata with info about models * fix: use hashmap instead of vec for models in metadata * fix: run cairo test fix * fix: remove unused model * docs: fix docs * tests: add tests * fix: bump cainome and work on tests * tests: fix tests * tests: ensure correct path for test file * feat: add ensure_abi method into model generated contract * feat: add generate_models_bindings setup for builtin plugins * fix: improve code parsing and plugin API * feat: identify systems and use new cainome tokenized abi * tests: fix building with dojo-test-utils + fix tests * fix: clean example to have correct class hash * fix: fix tests * chore: bump cainome to 0.2.2 to fix composite details in functions * fix: comment out testing until stack error on windows is investigated --- Cargo.lock | 88 ++++- Cargo.toml | 1 + crates/dojo-bindgen/Cargo.toml | 22 ++ crates/dojo-bindgen/README.md | 19 + crates/dojo-bindgen/src/error.rs | 16 + crates/dojo-bindgen/src/lib.rs | 346 ++++++++++++++++++ crates/dojo-bindgen/src/plugins/mod.rs | 23 ++ .../src/plugins/typescript/mod.rs | 32 ++ crates/dojo-bindgen/src/plugins/unity/mod.rs | 32 ++ .../dojo-bindgen/src/test_data/spawn-and-move | 1 + crates/dojo-lang/src/scarb_internal/mod.rs | 12 +- crates/sozo/Cargo.toml | 1 + crates/sozo/src/commands/build.rs | 38 +- 13 files changed, 620 insertions(+), 11 deletions(-) create mode 100644 crates/dojo-bindgen/Cargo.toml create mode 100644 crates/dojo-bindgen/README.md create mode 100644 crates/dojo-bindgen/src/error.rs create mode 100644 crates/dojo-bindgen/src/lib.rs create mode 100644 crates/dojo-bindgen/src/plugins/mod.rs create mode 100644 crates/dojo-bindgen/src/plugins/typescript/mod.rs create mode 100644 crates/dojo-bindgen/src/plugins/unity/mod.rs create mode 120000 crates/dojo-bindgen/src/test_data/spawn-and-move diff --git a/Cargo.lock b/Cargo.lock index ccabe33673..1bb69e2124 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1108,14 +1108,47 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663" +[[package]] +name = "cainome" +version = "0.1.5" +source = "git+https://github.com/cartridge-gg/cainome?tag=v0.2.2#46c9fa734b396632cb5f986294d05532ada80f9a" +dependencies = [ + "anyhow", + "async-trait", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.2)", + "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.2)", + "cainome-rs 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.2)", + "camino", + "clap", + "clap_complete", + "convert_case 0.6.0", + "serde", + "serde_json", + "starknet", + "thiserror", + "tokio", + "tracing", + "tracing-subscriber", + "url", +] + [[package]] name = "cainome" version = "0.1.5" source = "git+https://github.com/cartridge-gg/cainome?rev=950e487#950e4871b735a1b4a7ba7e7561b9a15f5a43dbed" dependencies = [ - "cainome-cairo-serde", - "cainome-parser", - "cainome-rs", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=950e487)", + "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=950e487)", + "cainome-rs 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=950e487)", +] + +[[package]] +name = "cainome-cairo-serde" +version = "0.1.0" +source = "git+https://github.com/cartridge-gg/cainome?tag=v0.2.2#46c9fa734b396632cb5f986294d05532ada80f9a" +dependencies = [ + "starknet", + "thiserror", ] [[package]] @@ -1127,6 +1160,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cainome-parser" +version = "0.1.0" +source = "git+https://github.com/cartridge-gg/cainome?tag=v0.2.2#46c9fa734b396632cb5f986294d05532ada80f9a" +dependencies = [ + "quote", + "serde_json", + "starknet", + "syn 2.0.41", + "thiserror", +] + [[package]] name = "cainome-parser" version = "0.1.0" @@ -1139,14 +1184,30 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cainome-rs" +version = "0.1.0" +source = "git+https://github.com/cartridge-gg/cainome?tag=v0.2.2#46c9fa734b396632cb5f986294d05532ada80f9a" +dependencies = [ + "anyhow", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.2)", + "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.2)", + "proc-macro2", + "quote", + "serde_json", + "starknet", + "syn 2.0.41", + "thiserror", +] + [[package]] name = "cainome-rs" version = "0.1.0" source = "git+https://github.com/cartridge-gg/cainome?rev=950e487#950e4871b735a1b4a7ba7e7561b9a15f5a43dbed" dependencies = [ "anyhow", - "cainome-cairo-serde", - "cainome-parser", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=950e487)", + "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=950e487)", "proc-macro2", "quote", "serde_json", @@ -2659,6 +2720,20 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "dojo-bindgen" +version = "0.5.0" +dependencies = [ + "async-trait", + "cainome 0.1.5 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.2)", + "camino", + "convert_case 0.6.0", + "serde", + "serde_json", + "starknet", + "thiserror", +] + [[package]] name = "dojo-core" version = "0.5.1-alpha.0" @@ -2792,7 +2867,7 @@ dependencies = [ "assert_fs", "assert_matches", "async-trait", - "cainome", + "cainome 0.1.5 (git+https://github.com/cartridge-gg/cainome?rev=950e487)", "cairo-lang-filesystem", "cairo-lang-project", "cairo-lang-starknet", @@ -8632,6 +8707,7 @@ dependencies = [ "clap-verbosity-flag", "clap_complete", "console", + "dojo-bindgen", "dojo-lang", "dojo-test-utils", "dojo-types", diff --git a/Cargo.toml b/Cargo.toml index 1977495f19..b0a8ef8aba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "crates/benches", + "crates/dojo-bindgen", "crates/dojo-core", "crates/dojo-lang", "crates/dojo-language-server", diff --git a/crates/dojo-bindgen/Cargo.toml b/crates/dojo-bindgen/Cargo.toml new file mode 100644 index 0000000000..c0e183f233 --- /dev/null +++ b/crates/dojo-bindgen/Cargo.toml @@ -0,0 +1,22 @@ +[package] +description = "Dojo specific bindings generator based on Cainome." +edition.workspace = true +license-file.workspace = true +name = "dojo-bindgen" +repository.workspace = true +version.workspace = true + +[dependencies] +async-trait.workspace = true +camino.workspace = true +convert_case.workspace = true +starknet.workspace = true +serde.workspace = true +serde_json.workspace = true +thiserror.workspace = true + +# Some issue with CI on windows, need to be investigated. +# https://github.com/dojoengine/dojo/actions/runs/7548423990/job/20550444492?pr=1425#step:6:1644 +#dojo-test-utils = { path = "../dojo-test-utils", features = [ "build-examples" ] } + +cainome = { git = "https://github.com/cartridge-gg/cainome", tag = "v0.2.2" } diff --git a/crates/dojo-bindgen/README.md b/crates/dojo-bindgen/README.md new file mode 100644 index 0000000000..9fcbe27f6e --- /dev/null +++ b/crates/dojo-bindgen/README.md @@ -0,0 +1,19 @@ +# Dojo bindings generator + +This crate contains the Dojo bindings generator modules which leverage [cainome](https://github.com/cartridge-gg/cainome) to parse Cairo ABI. + +## Architecture + +`dojo-bindgen` aims at decoupling at most the knowledge required by `sozo` to output bindings along the contract artifacts. Cainome exposes the `parser` crate, which contains common functions to work with Cairo ABI and generate a list of tokens to have a intermediate representation of the ABI usable at runtime and build logic on top of it to generate the bindings. + +[PluginManager](./src/lib.rs): The `PluginManager` is the top level interface that `sozo` uses to request code generation. By providing the artifacts path and the list of plugins (more params in the future), `sozo` indicates which plugin must be invoke to generate the bindings. + +[BuiltinPlugin](./src/plugins/mod.rs): The `BuiltinPlugin` are a first lightweight and integrated plugins that are written in rust directly inside this crate. This also comes packaged into the dojo toolchain, ready to be used by developers. + +In the future, `dojo-bindgen` will expose a `Plugin` interface similar to protobuf to communicate with a user defined plugin using `stdin` for greater flexibility. + +## Builtin Plugins + +[Typescript](./src/plugins/typescript/mod.rs) + +[Unity](./src/plugins/unity/mod.rs) diff --git a/crates/dojo-bindgen/src/error.rs b/crates/dojo-bindgen/src/error.rs new file mode 100644 index 0000000000..95553874b8 --- /dev/null +++ b/crates/dojo-bindgen/src/error.rs @@ -0,0 +1,16 @@ +use cainome::parser::Error as CainomeError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + IO(#[from] std::io::Error), + #[error(transparent)] + SerdeJson(#[from] serde_json::Error), + #[error(transparent)] + Cainome(#[from] CainomeError), + #[error("Format error: {0}")] + Format(String), +} + +pub type BindgenResult = Result; diff --git a/crates/dojo-bindgen/src/lib.rs b/crates/dojo-bindgen/src/lib.rs new file mode 100644 index 0000000000..d2a23e1994 --- /dev/null +++ b/crates/dojo-bindgen/src/lib.rs @@ -0,0 +1,346 @@ +use std::collections::HashMap; +use std::fs; + +use cainome::parser::tokens::Token; +use cainome::parser::{AbiParser, TokenizedAbi}; +use camino::Utf8PathBuf; +use convert_case::{Case, Casing}; + +pub mod error; +use error::{BindgenResult, Error}; + +mod plugins; +use plugins::typescript::TypescriptPlugin; +use plugins::unity::UnityPlugin; +use plugins::BuiltinPlugin; +pub use plugins::BuiltinPlugins; + +#[derive(Debug, PartialEq)] +pub struct DojoModel { + /// PascalCase name of the model. + pub name: String, + /// Fully qualified path of the model type in cairo code. + pub qualified_path: String, + /// List of tokens found in the model contract ABI. + /// Only structs and enums are currently used. + pub tokens: TokenizedAbi, +} + +#[derive(Debug, PartialEq)] +pub struct DojoContract { + /// Contract's name. + pub contract_file_name: String, + /// Full ABI of the contract in case the plugin wants to make extra checks, + /// or generated other functions than the systems. + pub tokens: TokenizedAbi, + /// Functions that are identified as systems. + pub systems: Vec, +} + +#[derive(Debug)] +pub struct DojoData { + /// All contracts found in the project. + pub contracts: HashMap, + /// All the models contracts found in the project. + pub models: HashMap, +} + +// TODO: include the manifest to have more metadata when new manifest is available. +#[derive(Debug)] +pub struct PluginManager { + /// Path of contracts artifacts. + pub artifacts_path: Utf8PathBuf, + /// A list of builtin plugins to invoke. + pub builtin_plugins: Vec, + /// A list of custom plugins to invoke. + pub plugins: Vec, +} + +impl PluginManager { + /// Generates the bindings for all the given Plugin. + pub async fn generate(&self) -> BindgenResult<()> { + if self.builtin_plugins.is_empty() && self.plugins.is_empty() { + return Ok(()); + } + + let data = gather_dojo_data(&self.artifacts_path)?; + + for plugin in &self.builtin_plugins { + // Get the plugin builder from the plugin enum. + let builder: Box = match plugin { + BuiltinPlugins::Typescript => Box::new(TypescriptPlugin::new()), + BuiltinPlugins::Unity => Box::new(UnityPlugin::new()), + }; + + builder.generate_code(&data).await?; + } + Ok(()) + } +} + +/// Gathers dojo data from artifacts. +/// TODO: this should be modified later to use the new manifest structure. +/// it's currently done from the artifacts to decouple from the manifest. +/// +/// # Arguments +/// +/// * `artifacts_path` - Artifacts path where contracts were generated. +fn gather_dojo_data(artifacts_path: &Utf8PathBuf) -> BindgenResult { + let mut models = HashMap::new(); + let mut contracts = HashMap::new(); + + for entry in fs::read_dir(artifacts_path)? { + let entry = entry?; + let path = entry.path(); + + if path.is_file() { + if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) { + let file_content = fs::read_to_string(&path)?; + + // Models and Contracts must have a valid ABI. + if let Ok(tokens) = + AbiParser::tokens_from_abi_string(&file_content, &HashMap::new()) + { + // Contract. + if is_systems_contract(file_name, &file_content) { + // Identify the systems -> for now only take the functions from the + // interfaces. + let mut systems = vec![]; + let interface_blacklist = [ + "dojo::world::IWorldProvider", + "dojo::components::upgradeable::IUpgradeable", + ]; + + for (interface, funcs) in &tokens.interfaces { + if !interface_blacklist.contains(&interface.as_str()) { + systems.extend(funcs.clone()); + } + } + + contracts.insert( + file_name.to_string(), + DojoContract { + contract_file_name: file_name.to_string(), + tokens: tokens.clone(), + systems, + }, + ); + } + + // Model. + if is_model_contract(&tokens) { + if let Some(model_name) = model_name_from_artifact_filename(file_name) { + let model_pascal_case = + model_name.from_case(Case::Snake).to_case(Case::Pascal); + + let model = DojoModel { + name: model_pascal_case.clone(), + qualified_path: file_name + .replace(&model_name, &model_pascal_case) + .trim_end_matches(".json") + .to_string(), + tokens: filter_model_tokens(&tokens), + }; + + models.insert(model_pascal_case, model); + } else { + return Err(Error::Format(format!( + "Could not extract model name from file name `{file_name}`" + ))); + } + } + } + } + } + } + + Ok(DojoData { models, contracts }) +} + +/// Identifies if the given contract contains systems. +/// +/// For now the identification is very naive and don't use the manifest +/// as the manifest format will change soon. +/// TODO: use the new manifest files once available. +/// +/// # Arguments +/// +/// * `file_name` - Name of the contract file. +/// * `file_content` - Content of the contract artifact. +fn is_systems_contract(file_name: &str, file_content: &str) -> bool { + if file_name.starts_with("dojo::") || file_name == "manifest.json" { + return false; + } + + file_content.contains("IWorldDispatcher") +} + +/// Filters the model ABI to keep relevant types +/// to be generated for bindings. +fn filter_model_tokens(tokens: &TokenizedAbi) -> TokenizedAbi { + let mut structs = vec![]; + let mut enums = vec![]; + + // All types from introspect module can also be removed as the clients does not rely on them. + // Events are also always empty at model contract level. + fn skip_token(token: &Token) -> bool { + if token.type_path().starts_with("dojo::database::introspect") { + return true; + } + + if let Token::Composite(c) = token { + if c.is_event { + return true; + } + } + + false + } + + for s in &tokens.structs { + if !skip_token(s) { + structs.push(s.clone()); + } + } + + for e in &tokens.enums { + if !skip_token(e) { + enums.push(e.clone()); + } + } + + TokenizedAbi { structs, enums, ..Default::default() } +} + +/// Extracts a model name from the artifact file name. +/// +/// # Example +/// +/// The file name "dojo_examples::models::position.json" should return "position". +/// +/// # Arguments +/// +/// * `file_name` - Artifact file name. +fn model_name_from_artifact_filename(file_name: &str) -> Option { + let parts: Vec<&str> = file_name.split("::").collect(); + + if let Some(last_part) = parts.last() { + // TODO: for now, we always reconstruct with PascalCase. + // Once manifest data are available, use the exact name instead. + // We may have errors here is the struct is named like myStruct and not MyStruct. + // Plugin dev should consider case insensitive comparison. + last_part.split_once(".json").map(|m_ext| m_ext.0.to_string()) + } else { + None + } +} + +/// Identifies if the given contract contains a model. +/// +/// The identification is based on the methods name. This must +/// be adjusted if the model attribute expansion change in the future. +/// +/// +/// # Arguments +/// +/// * `file_name` - Name of the contract file. +/// * `file_content` - Content of the contract artifact. +fn is_model_contract(tokens: &TokenizedAbi) -> bool { + let expected_funcs = ["name", "layout", "packed_size", "unpacked_size", "schema"]; + + let mut funcs_counts = 0; + + // This hashmap is not that good at devex level.. one must check the + // code to know the keys. + for f in &tokens.functions { + if expected_funcs.contains(&f.to_function().expect("Function expected").name.as_str()) { + funcs_counts += 1; + } + } + + funcs_counts == expected_funcs.len() +} + +// #[cfg(test)] +// mod tests { +// use super::*; +// +// #[test] +// fn is_system_contract_ok() { +// let file_name = "dojo_examples::actions::actions.json"; +// let file_content = include_str!( +// "test_data/spawn-and-move/target/dev/dojo_examples::actions::actions.json" +// ); +// +// assert!(is_systems_contract(file_name, file_content)); +// } +// +// #[test] +// fn is_system_contract_ignore_dojo_files() { +// let file_name = "dojo::world::world.json"; +// let file_content = ""; +// assert!(!is_systems_contract(file_name, file_content)); +// +// let file_name = "manifest.json"; +// assert!(!is_systems_contract(file_name, file_content)); +// } +// +// #[test] +// fn test_is_system_contract_ignore_models() { +// let file_name = "dojo_examples::models::position.json"; +// let file_content = include_str!( +// "test_data/spawn-and-move/target/dev/dojo_examples::models::position.json" +// ); +// assert!(!is_systems_contract(file_name, file_content)); +// } +// +// #[test] +// fn model_name_from_artifact_filename_ok() { +// let file_name = "dojo_examples::models::position.json"; +// assert_eq!(model_name_from_artifact_filename(file_name), Some("position".to_string())); +// } +// +// #[test] +// fn is_model_contract_ok() { +// let file_content = +// include_str!("test_data/spawn-and-move/target/dev/dojo_examples::models::moves.json"); +// let tokens = AbiParser::tokens_from_abi_string(file_content, &HashMap::new()).unwrap(); +// +// assert!(is_model_contract(&tokens)); +// } +// +// #[test] +// fn is_model_contract_ignore_systems() { +// let file_content = include_str!( +// "test_data/spawn-and-move/target/dev/dojo_examples::actions::actions.json" +// ); +// let tokens = AbiParser::tokens_from_abi_string(file_content, &HashMap::new()).unwrap(); +// +// assert!(!is_model_contract(&tokens)); +// } +// +// #[test] +// fn is_model_contract_ignore_dojo_files() { +// let file_content = +// include_str!("test_data/spawn-and-move/target/dev/dojo::world::world.json"); +// let tokens = AbiParser::tokens_from_abi_string(file_content, &HashMap::new()).unwrap(); +// +// assert!(!is_model_contract(&tokens)); +// } +// +// #[test] +// fn gather_data_ok() { +// let data = gather_dojo_data(&Utf8PathBuf::from("src/test_data/spawn-and-move/target/dev")) +// .unwrap(); +// +// assert_eq!(data.models.len(), 2); +// +// let pos = data.models.get("Position").unwrap(); +// assert_eq!(pos.name, "Position"); +// assert_eq!(pos.qualified_path, "dojo_examples::models::Position"); +// +// let moves = data.models.get("Moves").unwrap(); +// assert_eq!(moves.name, "Moves"); +// assert_eq!(moves.qualified_path, "dojo_examples::models::Moves"); +// } +// } diff --git a/crates/dojo-bindgen/src/plugins/mod.rs b/crates/dojo-bindgen/src/plugins/mod.rs new file mode 100644 index 0000000000..d1cd14ceef --- /dev/null +++ b/crates/dojo-bindgen/src/plugins/mod.rs @@ -0,0 +1,23 @@ +use async_trait::async_trait; + +use crate::error::BindgenResult; +use crate::DojoData; + +pub mod typescript; +pub mod unity; + +#[derive(Debug)] +pub enum BuiltinPlugins { + Typescript, + Unity, +} + +#[async_trait] +pub trait BuiltinPlugin { + /// Generates code by executing the plugin. + /// + /// # Arguments + /// + /// * `data` - Dojo data gathered from the compiled project. + async fn generate_code(&self, data: &DojoData) -> BindgenResult<()>; +} diff --git a/crates/dojo-bindgen/src/plugins/typescript/mod.rs b/crates/dojo-bindgen/src/plugins/typescript/mod.rs new file mode 100644 index 0000000000..65ca4e4b41 --- /dev/null +++ b/crates/dojo-bindgen/src/plugins/typescript/mod.rs @@ -0,0 +1,32 @@ +use async_trait::async_trait; + +use crate::error::BindgenResult; +use crate::plugins::BuiltinPlugin; +use crate::DojoData; + +pub struct TypescriptPlugin; + +impl TypescriptPlugin { + pub fn new() -> Self { + Self {} + } +} + +#[async_trait] +impl BuiltinPlugin for TypescriptPlugin { + async fn generate_code(&self, data: &DojoData) -> BindgenResult<()> { + println!("-> Typescript models bindings\n"); + + for (name, model) in &data.models { + println!("## Model: {}", name); + println!("{:?}\n", model); + } + + for (file_name, contract) in &data.contracts { + println!("## Contract: {}", file_name); + println!("{:?}\n", contract); + } + + Ok(()) + } +} diff --git a/crates/dojo-bindgen/src/plugins/unity/mod.rs b/crates/dojo-bindgen/src/plugins/unity/mod.rs new file mode 100644 index 0000000000..1560e1c45f --- /dev/null +++ b/crates/dojo-bindgen/src/plugins/unity/mod.rs @@ -0,0 +1,32 @@ +use async_trait::async_trait; + +use crate::error::BindgenResult; +use crate::plugins::BuiltinPlugin; +use crate::DojoData; + +pub struct UnityPlugin; + +impl UnityPlugin { + pub fn new() -> Self { + Self {} + } +} + +#[async_trait] +impl BuiltinPlugin for UnityPlugin { + async fn generate_code(&self, data: &DojoData) -> BindgenResult<()> { + println!("-> Unity models bindings\n"); + + for (name, model) in &data.models { + println!("## Model: {}", name); + println!("{:?}\n", model); + } + + for (file_name, contract) in &data.contracts { + println!("## Contract: {}", file_name); + println!("{:?}\n", contract); + } + + Ok(()) + } +} diff --git a/crates/dojo-bindgen/src/test_data/spawn-and-move b/crates/dojo-bindgen/src/test_data/spawn-and-move new file mode 120000 index 0000000000..0b85d0755f --- /dev/null +++ b/crates/dojo-bindgen/src/test_data/spawn-and-move @@ -0,0 +1 @@ +../../../../examples/spawn-and-move \ No newline at end of file diff --git a/crates/dojo-lang/src/scarb_internal/mod.rs b/crates/dojo-lang/src/scarb_internal/mod.rs index ec8cd9857a..a6c37d6dbd 100644 --- a/crates/dojo-lang/src/scarb_internal/mod.rs +++ b/crates/dojo-lang/src/scarb_internal/mod.rs @@ -12,6 +12,7 @@ use cairo_lang_project::{AllCratesConfig, SingleCrateConfig}; use cairo_lang_starknet::starknet_plugin_suite; use cairo_lang_test_plugin::test_plugin_suite; use cairo_lang_utils::ordered_hash_map::OrderedHashMap; +use camino::Utf8PathBuf; use scarb::compiler::CompilationUnit; use scarb::core::Config; use scarb::ops::CompileOpts; @@ -20,6 +21,10 @@ use tracing::trace; use crate::plugin::dojo_plugin_suite; +pub struct CompileInfo { + pub target_dir: Utf8PathBuf, +} + pub fn crates_config_for_compilation_unit(unit: &CompilationUnit) -> AllCratesConfig { let crates_config: OrderedHashMap = unit .components @@ -52,7 +57,7 @@ pub fn build_scarb_root_database(unit: &CompilationUnit) -> Result /// This function is an alternative to `ops::compile`, it's doing the same job. /// However, we can control the injection of the plugins, required to have dojo plugin present /// for each compilation. -pub fn compile_workspace(config: &Config, opts: CompileOpts) -> Result<()> { +pub fn compile_workspace(config: &Config, opts: CompileOpts) -> Result { let ws = scarb::ops::read_workspace(config.manifest_path(), config)?; let packages: Vec = ws.members().map(|p| p.id).collect(); let resolve = scarb::ops::resolve_workspace(&ws)?; @@ -73,7 +78,10 @@ pub fn compile_workspace(config: &Config, opts: CompileOpts) -> Result<()> { } } - Ok(()) + let target_dir = ws.target_dir().path_existent().unwrap(); + let target_dir = target_dir.join(ws.config().profile().as_str()); + + Ok(CompileInfo { target_dir }) } fn build_project_config(unit: &CompilationUnit) -> Result { diff --git a/crates/sozo/Cargo.toml b/crates/sozo/Cargo.toml index da7b252cc5..e5fce987c0 100644 --- a/crates/sozo/Cargo.toml +++ b/crates/sozo/Cargo.toml @@ -24,6 +24,7 @@ clap-verbosity-flag = "2.0.1" clap.workspace = true clap_complete.workspace = true console.workspace = true +dojo-bindgen = { path = "../dojo-bindgen" } dojo-lang = { path = "../dojo-lang" } dojo-types = { path = "../dojo-types" } dojo-world = { path = "../dojo-world", features = [ "contracts", "metadata", "migration" ] } diff --git a/crates/sozo/src/commands/build.rs b/crates/sozo/src/commands/build.rs index 6c5695fd0f..31107a2b2b 100644 --- a/crates/sozo/src/commands/build.rs +++ b/crates/sozo/src/commands/build.rs @@ -1,17 +1,49 @@ use anyhow::Result; use clap::Args; +use dojo_bindgen::{BuiltinPlugins, PluginManager}; use dojo_lang::scarb_internal::compile_workspace; use scarb::core::{Config, TargetKind}; use scarb::ops::CompileOpts; #[derive(Args, Debug)] -pub struct BuildArgs; +pub struct BuildArgs { + #[arg(long)] + #[arg(help = "Generate Typescript bindings.")] + pub typescript: bool, + + #[arg(long)] + #[arg(help = "Generate Unity bindings.")] + pub unity: bool, +} impl BuildArgs { pub fn run(self, config: &Config) -> Result<()> { - compile_workspace( + let compile_info = compile_workspace( config, CompileOpts { include_targets: vec![], exclude_targets: vec![TargetKind::TEST] }, - ) + )?; + + let mut builtin_plugins = vec![]; + if self.typescript { + builtin_plugins.push(BuiltinPlugins::Typescript); + } + + if self.unity { + builtin_plugins.push(BuiltinPlugins::Unity); + } + + // Custom plugins are always empty for now. + let bindgen = PluginManager { + artifacts_path: compile_info.target_dir, + plugins: vec![], + builtin_plugins, + }; + + tokio::runtime::Runtime::new() + .unwrap() + .block_on(bindgen.generate()) + .expect("Error generating bindings"); + + Ok(()) } }