diff --git a/Cargo.lock b/Cargo.lock index abbd4f3f..eedd6f28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2663,6 +2663,7 @@ dependencies = [ "include_dir", "indicatif", "reqwest", + "serde", "toml 0.8.19", "zip", ] @@ -3819,9 +3820,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] @@ -3849,9 +3850,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 2ae2e126..7e1bc8bf 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -18,3 +18,4 @@ toml = "0.8.19" zip = "2.2.1" reqwest = { version= "0.12.9", features = ["blocking"] } indicatif = "0.17.9" +serde = { version = "1.0.216", features = ["derive"] } diff --git a/cli/src/build.rs b/cli/src/build.rs index 2ee62b37..0050b3cc 100644 --- a/cli/src/build.rs +++ b/cli/src/build.rs @@ -1,15 +1,18 @@ use std::collections::HashMap; +use std::collections::HashSet; use std::env; -use std::fs; +use anyhow::Error; use dialoguer::theme::ColorfulTheme; use dialoguer::Confirm; use dialoguer::MultiSelect; use dialoguer::Select; use include_dir::include_dir; use include_dir::Dir; -use toml::Value; +use crate::config::read_config; +use crate::config::write_config; +use crate::config::Config; use crate::create::utils::copy_embedded_dir; use crate::print::print_build_success_message; use crate::style; @@ -47,6 +50,17 @@ pub fn build_project( return Ok(()); }; + // Detect `Config.toml` + let config_path = current_dir.join("Config.toml"); + + // Check if the config file exist + if !config_path.exists() { + return Err(Error::msg( + "Config.toml does exists. Please run 'mopro init'", + )); + } + let mut config = read_config(&config_path)?; + let mode: String = match arg_mode.as_deref() { None => select_mode()?, Some(m) => { @@ -59,10 +73,10 @@ pub fn build_project( } }; - let platforms: Vec = match arg_platforms { - None => select_platforms()?, + let selected_platforms: HashSet = match arg_platforms { + None => select_platforms(&config)?, Some(p) => { - let mut valid_platforms: Vec = p + let mut valid_platforms: HashSet = p .iter() .filter(|&platform| PLATFORMS.contains(&platform.as_str())) .map(|platform| platform.to_owned()) @@ -72,7 +86,7 @@ pub fn build_project( style::print_yellow( "No platforms selected. Please select at least one platform.".to_string(), ); - valid_platforms = select_platforms()?; + valid_platforms = select_platforms(&config)?; } else if valid_platforms.len() != p.len() { style::print_yellow( format!( @@ -87,53 +101,51 @@ pub fn build_project( } }; - let mut selected_architectures: HashMap> = HashMap::new(); - - for platform in &platforms { - let archs = match platform.as_str() { - "ios" => select_architectures("iOS", &IOS_ARCHS)?, - "android" => select_architectures("Android", &ANDROID_ARCHS)?, - _ => vec![], - }; - - selected_architectures.insert(platform.clone(), archs); - } - - if platforms.is_empty() { + if selected_platforms.is_empty() { style::print_yellow("No platform selected. Use space to select platform(s).".to_string()); build_project(&Some(mode), &None)?; } else { - // Check 'Cargo.toml' file contains adaptor in the features table. - let feature_table = get_table_cargo_toml("features".to_string()).unwrap(); - let feature_array = feature_table - .get("default") - .and_then(|v| v.as_array()) - .unwrap(); - - let selected_adaptors: Vec<&str> = - feature_array.iter().filter_map(|v| v.as_str()).collect(); - - // Supported adaptors and platforms: + // Supported adapters and platforms: // | Platforms | Circom | Halo2 | // |-----------|--------|-------| // | iOS | Yes | Yes | // | Android | Yes | Yes | // | Web | No | Yes | // - // Note: 'Yes' indicates that the adaptor is compatible with the platform. + // Note: 'Yes' indicates that the adapter is compatible with the platform. - // If 'Circom' is the only selected adaptor and 'Web' is the only selected platform, + // Initialize target platform for preventing add more platforms when the user build again + let selected_adapters = config.target_adapters.clone(); + + // If 'Circom' is the only selected adapter and 'Web' is the only selected platform, // Restart the build step as this combination is not supported. - if selected_adaptors == vec!["mopro-ffi/circom"] && platforms == vec!["web"] { + if selected_adapters == HashSet::from(["circom".to_string()]) + && selected_platforms == HashSet::from(["web".to_string()]) + { style::print_yellow( "Web platform is not support Circom only, choose different platform".to_string(), ); build_project(&Some(mode.clone()), &None)?; } - // Notification when the user selects the 'Halo2' adaptor and the 'Web' platform. - if selected_adaptors.contains(&"mopro-ffi/halo2") && platforms.contains(&"web".to_string()) + // Notification when the user selects the 'circom' adapter and includes the 'web' platform in the selection. + if selected_adapters == HashSet::from(["circom".to_string()]) + && selected_platforms.contains("web") { + let confirm = Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt("WASM code for Circom will not be generated for the web platform due to lack of support. Do you want to continue?") + .default(true) + .interact()?; + + if !confirm { + build_project(&Some(mode.clone()), &None)?; + } + + copy_mopro_wasm_lib()?; + } + + // Notification when the user selects the 'halo2' adapter and includes the 'web' platform in the selection. + if selected_adapters.contains("halo2") && selected_platforms.contains("web") { let confirm = Confirm::with_theme(&ColorfulTheme::default()) .with_prompt("Halo2 WASM code will only be generated for the web platform. Do you want to continue?") .default(true) @@ -141,19 +153,26 @@ pub fn build_project( if !confirm { style::print_yellow("Aborted build for web platform".to_string()); - return Err(anyhow::anyhow!("")); + std::process::exit(0); } - let cwd = std::env::current_dir().unwrap(); - let target_dir = &cwd.join("mopro-wasm-lib"); - if !target_dir.exists() { - const WASM_TEMPLATE_DIR: Dir = - include_dir!("$CARGO_MANIFEST_DIR/src/template/mopro-wasm-lib"); - copy_embedded_dir(&WASM_TEMPLATE_DIR, target_dir)?; - } + copy_mopro_wasm_lib()?; } - for platform in platforms.clone() { + // Archtecture selection for iOS or Andriod + let mut selected_architectures: HashMap> = HashMap::new(); + + for platform in &selected_platforms { + let archs = match platform.as_str() { + "ios" => select_architectures("iOS", &IOS_ARCHS)?, + "android" => select_architectures("Android", &ANDROID_ARCHS)?, + _ => vec![], + }; + + selected_architectures.insert(platform.clone(), archs); + } + + for platform in selected_platforms.clone() { let arch_key: &str = match platform.as_str() { "ios" => "IOS_ARCHS", "android" => "ANDROID_ARCHS", @@ -174,7 +193,10 @@ pub fn build_project( .env(arch_key, selected_arch) .status()?; - if !status.success() { + // Add only successfully compiled platforms to the config. + if status.success() { + config.target_platforms.insert(platform.to_string()); + } else { // Return a custom error if the command fails return Err(anyhow::anyhow!( "Output with status code {}", @@ -183,7 +205,10 @@ pub fn build_project( } } - print_binding_message(platforms)?; + // Save the updated config to the file + write_config(&config_path, &config)?; + + print_binding_message(selected_platforms)?; } Ok(()) @@ -198,17 +223,25 @@ fn select_mode() -> anyhow::Result { Ok(MODES[idx].to_owned()) } -fn select_platforms() -> anyhow::Result> { +fn select_platforms(config: &Config) -> anyhow::Result> { let theme = create_custom_theme(); + + // defaults based on previous selections. + let defaults: Vec = PLATFORMS + .iter() + .map(|&platform| config.target_platforms.contains(platform)) + .collect(); + let selected_platforms = MultiSelect::with_theme(&theme) .with_prompt("Select platform(s) to build for (multiple selection with space)") .items(&PLATFORMS) + .defaults(&defaults) .interact()?; Ok(selected_platforms .iter() .map(|&idx| PLATFORMS[idx].to_owned()) - .collect()) + .collect::>()) } fn select_architectures(platform: &str, archs: &[&str]) -> anyhow::Result> { @@ -238,7 +271,7 @@ fn select_architectures(platform: &str, archs: &[&str]) -> anyhow::Result) -> anyhow::Result<()> { +fn print_binding_message(platforms: HashSet) -> anyhow::Result<()> { let current_dir = env::current_dir()?; print_green_bold("✨ Bindings Built Successfully! ✨".to_string()); println!("The Mopro bindings have been successfully generated and are available in the following directories:\n"); @@ -258,19 +291,15 @@ fn print_binding_message(platforms: Vec) -> anyhow::Result<()> { Ok(()) } -fn get_table_cargo_toml(table_name: String) -> anyhow::Result { - let current_dir: std::path::PathBuf = env::current_dir()?; - let cargo_toml_path = current_dir.join("Cargo.toml"); +fn copy_mopro_wasm_lib() -> anyhow::Result<()> { + let cwd = std::env::current_dir()?; + let target_dir = cwd.join("mopro-wasm-lib"); - let project_toml = fs::read_to_string(cargo_toml_path)?; - let parsed_cargo: Value = toml::from_str(&project_toml).unwrap(); - - if let Some(features) = parsed_cargo.get(table_name.clone()) { - Ok(features.clone()) - } else { - Err(anyhow::anyhow!( - "[{:?}] not found in 'Cargo.toml', Check current directory", - table_name - )) + if !target_dir.exists() { + const WASM_TEMPLATE_DIR: Dir = + include_dir!("$CARGO_MANIFEST_DIR/src/template/mopro-wasm-lib"); + copy_embedded_dir(&WASM_TEMPLATE_DIR, &target_dir)?; } + + Ok(()) } diff --git a/cli/src/config.rs b/cli/src/config.rs new file mode 100644 index 00000000..0eb1740b --- /dev/null +++ b/cli/src/config.rs @@ -0,0 +1,29 @@ +use std::collections::HashSet; +use std::fs::File; +use std::io::Read; +use std::io::Write; +use std::path::PathBuf; + +use anyhow::Result; + +// Storing user selections while interating with mopro cli +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct Config { + pub(crate) target_adapters: HashSet, + pub(crate) target_platforms: HashSet, +} + +pub fn read_config(file_path: &PathBuf) -> Result { + let mut file = File::open(file_path)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + let config: Config = toml::from_str(&contents)?; + Ok(config) +} + +pub fn write_config(file_path: &PathBuf, config: &Config) -> Result<()> { + let toml_string = toml::to_string_pretty(config)?; + let mut file = File::create(file_path)?; + file.write_all(toml_string.as_bytes())?; + Ok(()) +} diff --git a/cli/src/create.rs b/cli/src/create.rs index 3e4541ab..a08b0f98 100644 --- a/cli/src/create.rs +++ b/cli/src/create.rs @@ -1,8 +1,9 @@ use std::env; +use std::fmt::Display; use std::path::PathBuf; -use crate::style; use anyhow::Error; +use dialoguer::console::Term; use dialoguer::theme::ColorfulTheme; use dialoguer::Select; @@ -15,6 +16,8 @@ use web::Web; mod flutter; use flutter::Flutter; mod react_native; +use crate::config::read_config; +use crate::style; use react_native::ReactNative; pub mod utils; @@ -24,6 +27,7 @@ trait Create { fn print_message(); } +#[derive(Clone, PartialEq, Eq)] pub enum Framework { Ios, Android, @@ -32,6 +36,19 @@ pub enum Framework { ReactNative, } +impl Display for Framework { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let as_str = match self { + Framework::Ios => "ios", + Framework::Android => "android", + Framework::Web => "web", + Framework::Flutter => "flutter", + Framework::ReactNative => "react-native", + }; + write!(f, "{}", as_str) + } +} + impl From for Framework { fn from(app: String) -> Self { match app.to_lowercase().as_str() { @@ -57,23 +74,29 @@ impl From for &str { } } -const TEMPLATES: [&str; 5] = ["ios", "android", "web", "flutter", "react-native"]; +const FRAMEWORKS: [Framework; 5] = [ + Framework::Ios, + Framework::Android, + Framework::Web, + Framework::Flutter, + Framework::ReactNative, +]; -pub fn create_project(arg_platform: &Option) -> anyhow::Result<()> { - let platform: String = match arg_platform.as_deref() { - None => select_template()?, +pub fn create_project(arg_framework: &Option) -> anyhow::Result<()> { + let framework: String = match arg_framework.as_deref() { + None => select_framework()?, Some(m) => { - if TEMPLATES.contains(&m) { + if FRAMEWORKS.contains(&Framework::from(m.to_string())) { m.to_string() } else { style::print_yellow("Invalid template selected. Please choose a valid template (e.g., 'ios', 'android', 'web', 'react-native', 'flutter').".to_string()); - select_template()? + select_framework()? } } }; let project_dir = env::current_dir()?; - match platform.into() { + match framework.into() { Framework::Ios => Ios::create(project_dir)?, Framework::Android => Android::create(project_dir)?, Framework::Web => Web::create(project_dir)?, @@ -84,11 +107,69 @@ pub fn create_project(arg_platform: &Option) -> anyhow::Result<()> { Ok(()) } -fn select_template() -> anyhow::Result { +fn select_framework() -> anyhow::Result { + let (items, unselectable) = get_target_platforms_with_status()?; + let idx = Select::with_theme(&ColorfulTheme::default()) .with_prompt("Create template") - .items(&TEMPLATES) - .interact()?; + .items(&items) + .interact_on_opt(&Term::stderr())?; + + if let Some(selected_idx) = idx { + if unselectable[selected_idx] { + style::print_yellow(format!( + "Cannot create {} template - build binding first", + &FRAMEWORKS[selected_idx] + )); + return select_framework(); + } + Ok(items[selected_idx].to_owned()) // Only available items will be matched with 'platform' + } else { + Err(Error::msg("Template selection failed")) + } +} + +fn get_target_platforms_with_status() -> anyhow::Result<(Vec, Vec)> { + let current_dir = env::current_dir()?; + let config = read_config(¤t_dir.join("Config.toml"))?; + + let mut items = Vec::new(); + let mut unselectable = Vec::new(); + + for framework in FRAMEWORKS.iter() { + match framework { + Framework::Flutter | Framework::ReactNative => { + // Adding more information to the list + let requires = ["ios", "android"]; + let missing: Vec<&str> = requires + .iter() + .filter(|&&req| !config.target_platforms.contains(req)) + .cloned() + .collect(); + + if !missing.is_empty() { + items.push(format!( + "{:<12} - Requires {} binding(s)", + framework, + missing.join("/") + )); + unselectable.push(true); + } else { + items.push(framework.to_string()); + unselectable.push(false); + } + } + _ => { + if config.target_platforms.contains(&framework.to_string()) { + items.push(framework.to_string()); + unselectable.push(false); + } else { + items.push(format!("{:<12} - Require binding", framework)); + unselectable.push(true); + } + } + } + } - Ok(TEMPLATES[idx].to_owned()) + Ok((items, unselectable)) } diff --git a/cli/src/init.rs b/cli/src/init.rs index ba0bc19a..6e7f9623 100644 --- a/cli/src/init.rs +++ b/cli/src/init.rs @@ -1,15 +1,21 @@ -use crate::print::print_init_instructions; -use crate::style; -use crate::style::create_custom_theme; +use std::collections::HashSet; +use std::env; +use std::fs; +use std::io::Write; +use std::path::Path; + use dialoguer::theme::ColorfulTheme; use dialoguer::Input; use dialoguer::MultiSelect; use include_dir::include_dir; use include_dir::Dir; -use std::env; -use std::fs; -use std::io::Write; -use std::path::Path; + +use crate::config::read_config; +use crate::config::write_config; +use crate::config::Config; +use crate::print::print_init_instructions; +use crate::style; +use crate::style::create_custom_theme; pub fn init_project( arg_adapter: &Option, @@ -91,9 +97,30 @@ pub fn init_project( } } + // Store selection + let config_path = project_dir.join("Config.toml"); + + // Check if the config file exists, if not create a default one + if !config_path.exists() { + let default_config = Config { + target_adapters: HashSet::new(), + target_platforms: HashSet::new(), + }; + write_config(&config_path, &default_config)?; + } + // Read & Write config for selected adapter + let mut config = read_config(&config_path)?; + for adapter_idx in selection { + config + .target_adapters + .insert(adapters[adapter_idx].to_owned()); + } + write_config(&config_path, &config)?; + // Print out the instructions print_init_instructions(project_name); } + Ok(()) } @@ -191,7 +218,9 @@ fn circom_build_template(file_path: &str) -> anyhow::Result<()> { fn halo2_dependencies_template(file_path: &str) -> anyhow::Result<()> { let replacement = - "plonk-fibonacci = { package = \"plonk-fibonacci\", git = \"https://github.com/sifnoc/plonkish-fibonacci-sample.git\" }"; + "plonk-fibonacci = { package = \"plonk-fibonacci\", git = \"https://github.com/sifnoc/plonkish-fibonacci-sample.git\" } +hyperplonk-fibonacci = { package = \"hyperplonk-fibonacci\", git = \"https://github.com/sifnoc/plonkish-fibonacci-sample.git\" } +gemini-fibonacci = { package = \"gemini-fibonacci\", git = \"https://github.com/sifnoc/plonkish-fibonacci-sample.git\" }"; let target = "# HALO2_DEPENDENCIES"; replace_string_in_file(file_path, target, replacement) } diff --git a/cli/src/main.rs b/cli/src/main.rs index 6f79cddc..e770dc2a 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -2,6 +2,7 @@ use clap::Parser; use clap::Subcommand; mod build; +mod config; mod create; mod init; mod print; @@ -37,7 +38,7 @@ enum Commands { /// Create templates for the specified platform Create { #[arg(long, help = "Specify the platform")] - template: Option, + framework: Option, }, } @@ -56,7 +57,7 @@ fn main() { Ok(_) => {} Err(e) => style::print_red_bold(format!("Failed to build project: {:?}", e)), }, - Commands::Create { template } => match create::create_project(template) { + Commands::Create { framework } => match create::create_project(framework) { Ok(_) => {} Err(e) => style::print_red_bold(format!("Failed to create template: {:?}", e)), }, diff --git a/cli/src/template/init/README.md b/cli/src/template/init/README.md index 046e9229..55933b3c 100644 --- a/cli/src/template/init/README.md +++ b/cli/src/template/init/README.md @@ -16,7 +16,7 @@ cd mopro/cli cargo install --path . ``` -### 1. Initialize Adaptor +### 1. Initialize adapter Navigate to the Mopro example app directory and initialize setup by running: