diff --git a/README.md b/README.md index 2730b8b2..b1e48dd9 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ cargo component build --release The resulting WASM component implements the **stub interface** corresponding to the source interface, found in the target directory's -`wit/_stub.wit` file. This WASM component is to be composed together with another component that calls the original +`wit/stub.wit` file. This WASM component is to be composed together with another component that calls the original interface via WASM RPC. ## Build diff --git a/wasm-rpc-stubgen/README.md b/wasm-rpc-stubgen/README.md index fa03622c..ffd44d10 100644 --- a/wasm-rpc-stubgen/README.md +++ b/wasm-rpc-stubgen/README.md @@ -33,7 +33,7 @@ cargo component build --release The resulting WASM component implements the **stub interface** corresponding to the source interface, found in the target directory's -`wit/_stub.wit` file. This WASM component is to be composed together with another component that calls the original +`wit/stub.wit` file. This WASM component is to be composed together with another component that calls the original interface via WASM RPC. ## Build diff --git a/wasm-rpc-stubgen/src/commands/app.rs b/wasm-rpc-stubgen/src/commands/app.rs index 743c9537..68bc6afd 100644 --- a/wasm-rpc-stubgen/src/commands/app.rs +++ b/wasm-rpc-stubgen/src/commands/app.rs @@ -6,8 +6,8 @@ use crate::log::{ set_log_output, LogColorize, LogIndent, Output, }; use crate::model::app::{ - includes_from_yaml_file, Application, ComponentName, ComponentPropertiesExtensions, - ProfileName, DEFAULT_CONFIG_FILE_NAME, + includes_from_yaml_file, AppBuildStep, Application, ComponentName, + ComponentPropertiesExtensions, ProfileName, DEFAULT_CONFIG_FILE_NAME, }; use crate::model::app_raw; use crate::model::app_raw::ExternalCommand; @@ -26,7 +26,7 @@ use golem_wasm_rpc::WASM_RPC_VERSION; use itertools::Itertools; use std::cell::OnceCell; use std::cmp::Ordering; -use std::collections::{BTreeSet, HashMap}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::fmt::Write; use std::marker::PhantomData; use std::path::{Path, PathBuf}; @@ -41,6 +41,17 @@ pub struct Config { pub offline: bool, pub extensions: PhantomData, pub log_output: Output, + pub steps_filter: HashSet, +} + +impl Config { + pub fn should_run_step(&self, step: AppBuildStep) -> bool { + if self.steps_filter.is_empty() { + true + } else { + self.steps_filter.contains(&step) + } + } } #[derive(Debug, Clone)] @@ -306,17 +317,10 @@ impl ApplicationContext { } } -pub async fn pre_component_build( - config: Config, -) -> anyhow::Result<()> { - let mut ctx = ApplicationContext::new(config)?; - pre_component_build_ctx(&mut ctx).await -} - -async fn pre_component_build_ctx( +async fn gen_rpc( ctx: &mut ApplicationContext, ) -> anyhow::Result<()> { - log_action("Executing", "pre-component-build steps"); + log_action("Generating", "RPC artifacts"); let _indent = LogIndent::new(); { @@ -346,19 +350,9 @@ async fn pre_component_build_ctx( Ok(()) } -pub fn component_build( - config: Config, -) -> anyhow::Result<()> { - let ctx = ApplicationContext::new(config)?; - component_build_ctx(&ctx) -} - -fn component_build_ctx( +fn componentize( ctx: &ApplicationContext, ) -> anyhow::Result<()> { - log_action("Executing", "component-build steps"); - let _indent = LogIndent::new(); - log_action("Building", "components"); let _indent = LogIndent::new(); @@ -392,17 +386,10 @@ fn component_build_ctx( Ok(()) } -pub async fn post_component_build( - config: Config, -) -> anyhow::Result<()> { - let ctx = ApplicationContext::new(config)?; - post_component_build_ctx(&ctx).await -} - -async fn post_component_build_ctx( +async fn link_rpc( ctx: &ApplicationContext, ) -> anyhow::Result<()> { - log_action("Executing", "post-component-build steps"); + log_action("Linking", "RPC"); let _indent = LogIndent::new(); for component_name in ctx.application.component_names() { @@ -483,9 +470,15 @@ async fn post_component_build_ctx( pub async fn build(config: Config) -> anyhow::Result<()> { let mut ctx = ApplicationContext::::new(config)?; - pre_component_build_ctx(&mut ctx).await?; - component_build_ctx(&ctx)?; - post_component_build_ctx(&ctx).await?; + if ctx.config.should_run_step(AppBuildStep::GenRpc) { + gen_rpc(&mut ctx).await?; + } + if ctx.config.should_run_step(AppBuildStep::Componentize) { + componentize(&ctx)?; + } + if ctx.config.should_run_step(AppBuildStep::LinkRpc) { + link_rpc(&ctx).await?; + } Ok(()) } @@ -582,9 +575,11 @@ pub fn clean(config: Config) -> anyhow: Ok(()) } -pub fn available_custom_commands( +pub fn collect_custom_commands( config: Config, -) -> anyhow::Result> { +) -> anyhow::Result>> { + set_log_output(config.log_output); + let app = to_anyhow( config.log_output, "Failed to load application manifest(s), see problems above", @@ -593,9 +588,19 @@ pub fn available_custom_commands( let all_profiles = app.all_option_profiles(); - let mut commands = BTreeSet::::new(); + let mut commands = BTreeMap::>::new(); for profile in &all_profiles { - commands.extend(app.all_custom_commands(profile.as_ref())) + for command in app.all_custom_commands(profile.as_ref()) { + if !commands.contains_key(command.as_str()) { + commands.insert(command.clone(), BTreeSet::new()); + } + profile.iter().for_each(|profile| { + commands + .get_mut(command.as_str()) + .unwrap() + .insert(profile.clone()); + }); + } } Ok(commands) @@ -603,12 +608,17 @@ pub fn available_custom_commands( pub fn custom_command( config: Config, - command: String, + args: Vec, ) -> anyhow::Result<()> { + if args.len() != 1 { + bail!("Invalid number of arguments for custom command, expected exactly one argument"); + } + let command = &args[0]; + let ctx = ApplicationContext::new(config)?; let all_custom_commands = ctx.application.all_custom_commands(ctx.profile()); - if !all_custom_commands.contains(&command) { + if !all_custom_commands.contains(command) { if all_custom_commands.is_empty() { bail!( "Custom command {} not found, no custom command is available", @@ -636,7 +646,7 @@ pub fn custom_command( let properties = &ctx .application .component_properties(component_name, ctx.profile()); - if let Some(custom_command) = properties.custom_commands.get(&command) { + if let Some(custom_command) = properties.custom_commands.get(command) { log_action( "Executing", format!( @@ -731,7 +741,7 @@ fn collect_sources(mode: &ApplicationSourceMode) -> ValidatedResult }) } } - None => ValidatedResult::from_error("No config file found!".to_string()), + None => ValidatedResult::from_error("No application manifest found!".to_string()), }, ApplicationSourceMode::Explicit(sources) => { let non_unique_source_warns: Vec<_> = sources @@ -1133,9 +1143,10 @@ async fn build_stub( target_root: target_root.clone(), selected_world: None, stub_crate_version: WASM_RPC_VERSION.to_string(), + // NOTE: these overrides are deliberately not part of cli flags or the app manifest, at least for now wasm_rpc_override: WasmRpcOverride { - wasm_rpc_path_override: None, - wasm_rpc_version_override: None, + wasm_rpc_path_override: std::env::var("WASM_RPC_PATH_OVERRIDE").ok(), + wasm_rpc_version_override: std::env::var("WASM_RPC_VERSION_OVERRIDE").ok(), }, extract_source_interface_package: false, seal_cargo_workspace: true, diff --git a/wasm-rpc-stubgen/src/lib.rs b/wasm-rpc-stubgen/src/lib.rs index 57541ecc..960b0911 100644 --- a/wasm-rpc-stubgen/src/lib.rs +++ b/wasm-rpc-stubgen/src/lib.rs @@ -27,14 +27,18 @@ pub mod wit_encode; pub mod wit_generate; pub mod wit_resolve; -use crate::log::Output; -use crate::model::app::{ComponentPropertiesExtensions, ComponentPropertiesExtensionsAny}; +use crate::log::{LogColorize, Output}; +use crate::model::app::{AppBuildStep, ComponentPropertiesExtensions}; use crate::stub::{StubConfig, StubDefinition}; use crate::wit_generate::UpdateCargoToml; use anyhow::Context; use clap::{Parser, Subcommand}; +use colored::Colorize; +use itertools::Itertools; +use std::collections::HashSet; use std::marker::PhantomData; use std::path::PathBuf; +use std::process::exit; use tempfile::TempDir; #[cfg(test)] @@ -43,22 +47,23 @@ test_r::enable!(); #[derive(Parser, Debug)] #[command(name = "wasm-rpc-stubgen", version)] pub enum Command { - /// Generate a Rust RPC stub crate for a WASM component + /// [DEPRECATED] Generate a Rust RPC stub crate for a WASM component Generate(GenerateArgs), - /// Build an RPC stub for a WASM component + /// [DEPRECATED] Build an RPC stub for a WASM component Build(BuildArgs), - /// Adds a generated stub as a dependency to another WASM component + /// [DEPRECATED] Adds a generated stub as a dependency to another WASM component AddStubDependency(AddStubDependencyArgs), - /// Compose a WASM component with a generated stub WASM + /// [DEPRECATED] Compose a WASM component with a generated stub WASM Compose(ComposeArgs), - /// Initializes a Golem-specific cargo-make configuration in a Cargo workspace for automatically + /// [DEPRECATED] Initializes a Golem-specific cargo-make configuration in a Cargo workspace for automatically /// generating stubs and composing results. InitializeWorkspace(InitializeWorkspaceArgs), /// Build components with application manifests #[cfg(feature = "app-command")] + #[group(skip)] App { - #[command(subcommand)] - subcommand: App, + #[clap(flatten)] + command: App, }, } @@ -87,7 +92,7 @@ pub struct GenerateArgs { /// it from the stub WIT. This is useful for example with ComponentizeJS currently where otherwise /// the original component's interface would be added as an import to the final WASM. #[clap(long, default_value_t = false)] - pub always_inline_types: bool, // TODO: deprecated + pub always_inline_types: bool, } #[derive(clap::Args, Debug, Clone)] @@ -106,7 +111,7 @@ pub struct WasmRpcOverride { /// /// The resulting WASM component implements the **stub interface** corresponding to the source interface, found in the /// target directory's -/// `wit/_stub.wit` file. This WASM component is to be composed together with another component that calls the original +/// `wit/stub.wit` file. This WASM component is to be composed together with another component that calls the original /// interface via WASM RPC. #[derive(clap::Args, Debug)] #[command(version, about, long_about = None)] @@ -133,7 +138,7 @@ pub struct BuildArgs { /// it from the stub WIT. This is useful for example with ComponentizeJS currently where otherwise /// the original component's interface would be added as an import to the final WASM. #[clap(long, default_value_t = false)] - pub always_inline_types: bool, // TODO: deprecated + pub always_inline_types: bool, } /// Adds a generated stub as a dependency to another WASM component @@ -191,12 +196,28 @@ pub struct InitializeWorkspaceArgs { pub wasm_rpc_override: WasmRpcOverride, } +#[derive(clap::Parser, Debug)] +pub struct App { + /// List of application manifests, can be defined multiple times + #[clap(long, short)] + pub app: Vec, + /// Selects a build profile + #[clap(long, short)] + pub build_profile: Option, + /// When set to true will use offline mode where applicable (e.g. stub cargo builds), defaults to false + #[clap(long, short, default_value = "false")] + pub offline: bool, + + #[clap(subcommand)] + subcommand: Option, +} + #[derive(Subcommand, Debug)] -pub enum App { +pub enum AppSubCommand { /// Runs component build steps Build(AppBuildArgs), /// Clean outputs - Clean(AppCleanArgs), + Clean, /// Run custom command #[clap(external_subcommand)] CustomCommand(Vec), @@ -205,26 +226,12 @@ pub enum App { #[derive(clap::Args, Debug)] #[command(version, about, long_about = None)] pub struct AppBuildArgs { - /// List of application manifests, can be defined multiple times + /// Selects specific build steps, can be defined multiple times #[clap(long, short)] - pub app: Vec, + pub step: Vec, /// When set to true will skip modification time based up-to-date checks, defaults to false #[clap(long, short, default_value = "false")] pub force_build: bool, - /// Selects a build profile - #[clap(long, short)] - pub profile: Option, - /// When set to true will use offline mode where applicable (e.g. stub cargo builds), defaults to false - #[clap(long, short, default_value = "false")] - pub offline: bool, -} - -#[derive(clap::Args, Debug)] -#[command(version, about, long_about = None)] -pub struct AppCleanArgs { - /// List of application manifests, can be defined multiple times - #[clap(long, short)] - pub app: Vec, } #[derive(clap::Args, Debug)] @@ -300,34 +307,44 @@ pub fn initialize_workspace( } pub async fn run_app_command( + mut clap_command: clap::Command, command: App, ) -> anyhow::Result<()> { - match command { - App::Build(args) => { - commands::app::build(commands::app::Config { - app_resolve_mode: app_manifest_sources_to_resolve_mode(args.app), - skip_up_to_date_checks: args.force_build, - profile: args.profile.map(|profile| profile.into()), - offline: args.offline, - extensions: PhantomData::, - log_output: Output::Stdout, - }) - .await + let (mut config, subcommand) = app_command_to_config_and_subcommand::(command); + match subcommand { + Some(subcommand) => match subcommand { + AppSubCommand::Build(args) => { + config.skip_up_to_date_checks = args.force_build; + config.steps_filter = args.step.into_iter().collect(); + commands::app::build(config).await + } + AppSubCommand::Clean => commands::app::clean(config), + AppSubCommand::CustomCommand(args) => commands::app::custom_command(config, args), + }, + None => { + clap_command.print_help()?; + println!(); + print_app_custom_commands_help(config); + exit(2); } - App::Clean(args) => commands::app::clean(commands::app::Config { - app_resolve_mode: app_manifest_sources_to_resolve_mode(args.app), + } +} + +fn app_command_to_config_and_subcommand( + command: App, +) -> (commands::app::Config, Option) { + ( + commands::app::Config { + app_resolve_mode: app_manifest_sources_to_resolve_mode(command.app), skip_up_to_date_checks: false, - profile: None, - offline: false, - extensions: PhantomData::, + profile: command.build_profile.map(|profile| profile.into()), + offline: command.offline, + extensions: PhantomData::, log_output: Output::Stdout, - }), - App::CustomCommand(_args) => { - // TODO: parse app manifest / profile args - // commands::app::custom_command(app_args_to_config(args.args), args.command) - Ok(()) - } - } + steps_filter: HashSet::new(), + }, + command.subcommand, + ) } fn app_manifest_sources_to_resolve_mode( @@ -339,3 +356,35 @@ fn app_manifest_sources_to_resolve_mode( commands::app::ApplicationSourceMode::Explicit(sources) } } + +fn print_app_custom_commands_help( + mut config: commands::app::Config, +) { + config.log_output = Output::None; + match commands::app::collect_custom_commands(config) { + Ok(commands) => { + if !commands.is_empty() { + println!("{}", "Custom commands:".bold().underline()); + for (command, profiles) in commands { + if profiles.is_empty() { + println!(" {}", command); + } else { + println!( + " {} ({})", + command, + profiles.iter().map(|s| s.to_string()).join(", ") + ); + } + } + println!(); + } + } + Err(err) => { + println!( + "{}\n{:?}", + "Cannot show custom commands:".log_color_warn(), + err + ); + } + } +} diff --git a/wasm-rpc-stubgen/src/main.rs b/wasm-rpc-stubgen/src/main.rs index 7e6694ec..1cb4cf5f 100644 --- a/wasm-rpc-stubgen/src/main.rs +++ b/wasm-rpc-stubgen/src/main.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use clap::Parser; +use clap::{CommandFactory, FromArgMatches}; use colored::Colorize; use golem_wasm_rpc_stubgen::*; use std::process::ExitCode; @@ -24,7 +24,39 @@ use golem_wasm_rpc_stubgen::model::app::ComponentPropertiesExtensionsNone; async fn main() -> ExitCode { pretty_env_logger::init(); - let result = match Command::parse() { + #[cfg(feature = "app-command")] + let mut clap_command = Command::command(); + #[cfg(not(feature = "app-command"))] + let clap_command = Command::command(); + + // Based on Command::parse, but using cloned command, so we avoid creating clap_command twice + let parsed_command = { + let mut matches = clap_command.clone().get_matches(); + let res = Command::from_arg_matches_mut(&mut matches) + .map_err(|err| err.format(&mut Command::command())); + res.unwrap_or_else(|e| e.exit()) + }; + + #[cfg(feature = "app-command")] + let show_warning = !matches!(parsed_command, Command::App { .. }); + #[cfg(not(feature = "app-command"))] + let show_warning = true; + + if show_warning { + eprintln!( + "{}", + "WARNING: THIS COMMAND IS DEPRECATED AND MIGHT MODIFY SOURCE WIT FILES!".yellow() + ); + eprintln!( + "{}", + format!( + "\nThe recommended new way to handle wasm-rpc stub generation and linking is the {} command.\n", + "wasm-rpc-stubgen app".bold().underline(), + ).yellow(), + ) + } + + let result = match parsed_command { Command::Generate(generate_args) => generate(generate_args), Command::Build(build_args) => build(build_args).await, Command::AddStubDependency(add_stub_dependency_args) => { @@ -35,8 +67,25 @@ async fn main() -> ExitCode { initialize_workspace(init_workspace_args, "wasm-rpc-stubgen", &[]) } #[cfg(feature = "app-command")] - Command::App { subcommand } => { - run_app_command::(subcommand).await + Command::App { command } => { + run_app_command::( + { + // TODO: it would be nice to use the same logic which is used by default for handling help, + // and that way include the current context (bin name and parent commands), + // but that seems to be using errors, error formating and exit directly; + // and quite different code path compared to calling print_help + clap_command + .find_subcommand_mut("app") + .unwrap() + .clone() + .override_usage(format!( + "{} [OPTIONS] [COMMAND]", + "wasm-rpc-stubgen app".bold() + )) + }, + command, + ) + .await } }; diff --git a/wasm-rpc-stubgen/src/model/app.rs b/wasm-rpc-stubgen/src/model/app.rs index 8d7ec04e..047c7466 100644 --- a/wasm-rpc-stubgen/src/model/app.rs +++ b/wasm-rpc-stubgen/src/model/app.rs @@ -19,6 +19,14 @@ use wit_parser::PackageName; pub const DEFAULT_CONFIG_FILE_NAME: &str = "golem.yaml"; +#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[clap(rename_all = "kebab_case")] +pub enum AppBuildStep { + GenRpc, + Componentize, + LinkRpc, +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ComponentName(String); @@ -634,6 +642,17 @@ impl Application { } } + let reserved_commands = BTreeSet::from(["build", "clean"]); + + for custom_command in properties.custom_commands.keys() { + if reserved_commands.contains(custom_command.as_str()) { + validation.add_error(format!("Cannot use {} as custom command name, reserved command names: {}", + custom_command.log_color_error_highlight(), + reserved_commands.iter().map(|s| s.log_color_highlight()).join(", ") + )); + } + } + properties.extensions = CPE::convert_and_validate( source, validation, @@ -875,7 +894,7 @@ impl Application { .unwrap_or_default(); ComponentEffectivePropertySource { template_name: template_name.as_ref(), - profile, + profile: Some(effective_profile), is_requested_profile, any_template_overrides, } diff --git a/wasm-rpc-stubgen/src/naming.rs b/wasm-rpc-stubgen/src/naming.rs index 8860dc90..800a41a6 100644 --- a/wasm-rpc-stubgen/src/naming.rs +++ b/wasm-rpc-stubgen/src/naming.rs @@ -6,8 +6,8 @@ pub mod wit { pub static DEPS_DIR: &str = "deps"; pub static WIT_DIR: &str = "wit"; - pub static STUB_WIT_FILE_NAME: &str = "_stub.wit"; - pub static INTERFACE_WIT_FILE_NAME: &str = "_interface.wit"; + pub static STUB_WIT_FILE_NAME: &str = "stub.wit"; + pub static INTERFACE_WIT_FILE_NAME: &str = "interface.wit"; pub fn stub_package_name(package_name: &wit_parser::PackageName) -> wit_parser::PackageName { wit_parser::PackageName {