diff --git a/packages/cli/src/build/builder.rs b/packages/cli/src/build/builder.rs index f29d877763..b1525d1468 100644 --- a/packages/cli/src/build/builder.rs +++ b/packages/cli/src/build/builder.rs @@ -417,6 +417,7 @@ impl AppBuilder { } } + #[allow(clippy::too_many_arguments)] pub(crate) async fn open( &mut self, devserver_ip: SocketAddr, @@ -425,6 +426,7 @@ impl AppBuilder { open_browser: bool, always_on_top: bool, build_id: BuildId, + args: &[String], ) -> Result<()> { let krate = &self.build; @@ -501,7 +503,7 @@ impl AppBuilder { | Platform::MacOS | Platform::Windows | Platform::Linux - | Platform::Liveview => self.open_with_main_exe(envs)?, + | Platform::Liveview => self.open_with_main_exe(envs, args)?, }; self.builds_opened += 1; @@ -732,12 +734,13 @@ impl AppBuilder { /// paths right now, but they will when we start to enable things like swift integration. /// /// Server/liveview/desktop are all basically the same, though - fn open_with_main_exe(&mut self, envs: Vec<(&str, String)>) -> Result<()> { + fn open_with_main_exe(&mut self, envs: Vec<(&str, String)>, args: &[String]) -> Result<()> { let main_exe = self.app_exe(); tracing::debug!("Opening app with main exe: {main_exe:?}"); let mut child = Command::new(main_exe) + .args(args) .envs(envs) .env_remove("CARGO_MANIFEST_DIR") // running under `dx` shouldn't expose cargo-only : .stderr(Stdio::piped()) diff --git a/packages/cli/src/build/request.rs b/packages/cli/src/build/request.rs index f821db20ba..ad04745385 100644 --- a/packages/cli/src/build/request.rs +++ b/packages/cli/src/build/request.rs @@ -374,6 +374,7 @@ pub(crate) struct BuildRequest { pub(crate) triple: Triple, pub(crate) device: bool, pub(crate) package: String, + pub(crate) main_target: String, pub(crate) features: Vec, pub(crate) extra_cargo_args: Vec, pub(crate) extra_rustc_args: Vec, @@ -463,7 +464,11 @@ impl BuildRequest { /// /// Note: Build requests are typically created only when the CLI is invoked or when significant /// changes are detected in the `Cargo.toml` (e.g., features added or removed). - pub(crate) async fn new(args: &TargetArgs, workspace: Arc) -> Result { + pub(crate) async fn new( + args: &TargetArgs, + main_target: Option, + workspace: Arc, + ) -> Result { let crate_package = workspace.find_main_package(args.package.clone())?; let config = workspace @@ -506,6 +511,10 @@ impl BuildRequest { }) .unwrap_or(workspace.krates[crate_package].name.clone()); + // Use the main_target for the client + server build if it is set, otherwise use the target name for this + // specific build + let main_target = main_target.unwrap_or(target_name.clone()); + let crate_target = main_package .targets .iter() @@ -727,6 +736,7 @@ impl BuildRequest { extra_cargo_args, release, package, + main_target, skip_assets: args.skip_assets, base_path: args.base_path.clone(), wasm_split: args.wasm_split, @@ -2737,7 +2747,7 @@ impl BuildRequest { /// target/dx/build/app/web/server.exe pub(crate) fn build_dir(&self, platform: Platform, release: bool) -> PathBuf { self.internal_out_dir() - .join(self.executable_name()) + .join(&self.main_target) .join(if release { "release" } else { "debug" }) .join(platform.build_folder_name()) } @@ -2748,7 +2758,7 @@ impl BuildRequest { /// target/dx/bundle/app/public/ pub(crate) fn bundle_dir(&self, platform: Platform) -> PathBuf { self.internal_out_dir() - .join(self.executable_name()) + .join(&self.main_target) .join("bundle") .join(platform.build_folder_name()) } diff --git a/packages/cli/src/cli/build.rs b/packages/cli/src/cli/build.rs index b748bf6458..a6a5a6525a 100644 --- a/packages/cli/src/cli/build.rs +++ b/packages/cli/src/cli/build.rs @@ -1,8 +1,7 @@ -use crate::{cli::*, AppBuilder, BuildRequest, Workspace, PROFILE_SERVER}; +use crate::{cli::*, AppBuilder, BuildRequest, Workspace}; use crate::{BuildMode, Platform}; -use target_lexicon::Triple; -use super::target::{TargetArgs, TargetCmd}; +use super::target::TargetArgs; /// Build the Rust Dioxus app and all of its assets. /// @@ -16,42 +15,9 @@ pub struct BuildArgs { #[clap(long)] pub(crate) fullstack: Option, - /// The feature to use for the client in a fullstack app [default: "web"] - #[clap(long)] - pub(crate) client_features: Vec, - - /// The feature to use for the server in a fullstack app [default: "server"] - #[clap(long)] - pub(crate) server_features: Vec, - - /// Build with custom profile for the fullstack server - #[clap(long, default_value_t = PROFILE_SERVER.to_string())] - pub(crate) server_profile: String, - - /// The target to build for the server. - /// - /// This can be different than the host allowing cross-compilation of the server. This is useful for - /// platforms like Cloudflare Workers where the server is compiled to wasm and then uploaded to the edge. - #[clap(long)] - pub(crate) server_target: Option, - /// Arguments for the build itself #[clap(flatten)] pub(crate) build_arguments: TargetArgs, - - /// A list of additional targets to build. - /// - /// Server and Client are special targets that integrate with `dx serve`, while `crate` is a generic. - /// - /// ```sh - /// dx serve \ - /// client --target aarch64-apple-darwin \ - /// server --target wasm32-unknown-unknown \ - /// crate --target aarch64-unknown-linux-gnu --package foo \ - /// crate --target x86_64-unknown-linux-gnu --package bar - /// ``` - #[command(subcommand)] - pub(crate) targets: Option, } pub struct BuildTargets { @@ -60,6 +26,40 @@ pub struct BuildTargets { } impl BuildArgs { + fn default_client(&self) -> &TargetArgs { + &self.build_arguments + } + + fn default_server(&self, client: &BuildRequest) -> Option<&TargetArgs> { + // Now resolve the builds that we need to. + // These come from the args, but we'd like them to come from the `TargetCmd` chained object + // + // The process here is as follows: + // + // - Create the BuildRequest for the primary target + // - If that BuildRequest is "fullstack", then add the client features + // - If that BuildRequest is "fullstack", then also create a BuildRequest for the server + // with the server features + // + // This involves modifying the BuildRequest to add the client features and server features + // only if we can properly detect that it's a fullstack build. Careful with this, since + // we didn't build BuildRequest to be generally mutable. + let default_server = client.enabled_platforms.contains(&Platform::Server); + + // Make sure we set the fullstack platform so we actually build the fullstack variant + // Users need to enable "fullstack" in their default feature set. + // todo(jon): fullstack *could* be a feature of the app, but right now we're assuming it's always enabled + // + // Now we need to resolve the client features + let fullstack = ((default_server || client.fullstack_feature_enabled()) + || self.fullstack.unwrap_or(false)) + && self.fullstack != Some(false); + + fullstack.then_some(&self.build_arguments) + } +} + +impl CommandWithPlatformOverrides { pub async fn build(self) -> Result { tracing::info!("Building project..."); @@ -92,89 +92,30 @@ impl BuildArgs { // do some logging to ensure dx matches the dioxus version since we're not always API compatible workspace.check_dioxus_version_against_cli(); - let mut server = None; + let client_args = match &self.client { + Some(client) => &client.build_arguments, + None => self.shared.default_client(), + }; + let client = BuildRequest::new(client_args, None, workspace.clone()).await?; - let client = match self.targets { - // A simple `dx serve` command with no explicit targets - None => { - // Now resolve the builds that we need to. - // These come from the args, but we'd like them to come from the `TargetCmd` chained object - // - // The process here is as follows: - // - // - Create the BuildRequest for the primary target - // - If that BuildRequest is "fullstack", then add the client features - // - If that BuildRequest is "fullstack", then also create a BuildRequest for the server - // with the server features - // - // This involves modifying the BuildRequest to add the client features and server features - // only if we can properly detect that it's a fullstack build. Careful with this, since - // we didn't build BuildRequest to be generally mutable. - let client = BuildRequest::new(&self.build_arguments, workspace.clone()).await?; - let default_server = client - .enabled_platforms - .iter() - .any(|p| *p == Platform::Server); - - // Make sure we set the fullstack platform so we actually build the fullstack variant - // Users need to enable "fullstack" in their default feature set. - // todo(jon): fullstack *could* be a feature of the app, but right now we're assuming it's always enabled - // - // Now we need to resolve the client features - let fullstack = ((default_server || client.fullstack_feature_enabled()) - || self.fullstack.unwrap_or(false)) - && self.fullstack != Some(false); - - if fullstack { - let mut build_args = self.build_arguments.clone(); - build_args.platform = Some(Platform::Server); - - let _server = BuildRequest::new(&build_args, workspace.clone()).await?; - - // ... todo: add the server features to the server build - // ... todo: add the client features to the client build - // // Make sure we have a server feature if we're building a fullstack app - if self.fullstack.unwrap_or_default() && self.server_features.is_empty() { - return Err(anyhow::anyhow!("Fullstack builds require a server feature on the target crate. Add a `server` feature to the crate and try again.").into()); - } - - server = Some(_server); - } - - client - } - - // A command in the form of: - // ``` - // dx serve \ - // client --package frontend \ - // server --package backend - // ``` - Some(cmd) => { - let mut client_args_ = None; - let mut server_args_ = None; - let mut cmd_outer = Some(Box::new(cmd)); - while let Some(cmd) = cmd_outer.take() { - match *cmd { - TargetCmd::Client(cmd_) => { - client_args_ = Some(cmd_.inner); - cmd_outer = cmd_.next; - } - TargetCmd::Server(cmd) => { - server_args_ = Some(cmd.inner); - cmd_outer = cmd.next; - } - } - } - - if let Some(server_args) = server_args_ { - server = Some(BuildRequest::new(&server_args, workspace.clone()).await?); - } - - BuildRequest::new(&client_args_.unwrap(), workspace.clone()).await? - } + let server_args = match &self.server { + Some(server) => Some(&server.build_arguments), + None => self.shared.default_server(&client), }; + let mut server = None; + // If there is a server, make sure we output in the same directory as the client build so we use the server + // to serve the web client + if let Some(server_args) = server_args { + // Copy the main target from the client to the server + let main_target = client.main_target.clone(); + let mut server_args = server_args.clone(); + // The platform in the server build is always set to Server + server_args.platform = Some(Platform::Server); + server = + Some(BuildRequest::new(&server_args, Some(main_target), workspace.clone()).await?); + } + Ok(BuildTargets { client, server }) } } diff --git a/packages/cli/src/cli/bundle.rs b/packages/cli/src/cli/bundle.rs index 60b5932c69..8f155ad1c1 100644 --- a/packages/cli/src/cli/bundle.rs +++ b/packages/cli/src/cli/bundle.rs @@ -41,7 +41,7 @@ pub struct Bundle { /// The arguments for the dioxus build #[clap(flatten)] - pub(crate) args: BuildArgs, + pub(crate) args: CommandWithPlatformOverrides, } impl Bundle { diff --git a/packages/cli/src/cli/check.rs b/packages/cli/src/cli/check.rs index 28abe05a78..14c4f89c69 100644 --- a/packages/cli/src/cli/check.rs +++ b/packages/cli/src/cli/check.rs @@ -19,7 +19,7 @@ pub(crate) struct Check { /// Information about the target to check #[clap(flatten)] - pub(crate) build_args: BuildArgs, + pub(crate) build_args: CommandWithPlatformOverrides, } impl Check { diff --git a/packages/cli/src/cli/mod.rs b/packages/cli/src/cli/mod.rs index d2f6c97c1d..edf6ed4d15 100644 --- a/packages/cli/src/cli/mod.rs +++ b/packages/cli/src/cli/mod.rs @@ -8,6 +8,7 @@ pub(crate) mod config; pub(crate) mod create; pub(crate) mod init; pub(crate) mod link; +pub(crate) mod platform_override; pub(crate) mod run; pub(crate) mod serve; pub(crate) mod target; @@ -20,6 +21,7 @@ pub(crate) use serve::*; pub(crate) use target::*; pub(crate) use verbosity::*; +use crate::platform_override::CommandWithPlatformOverrides; use crate::{error::Result, Error, StructuredOutput}; use clap::builder::styling::{AnsiColor, Effects, Style, Styles}; use clap::{Parser, Subcommand}; @@ -62,7 +64,7 @@ pub(crate) enum Commands { /// Build the Dioxus project and all of its assets. #[clap(name = "build")] - Build(build::BuildArgs), + Build(CommandWithPlatformOverrides), /// Run the project without any hotreloading. #[clap(name = "run")] diff --git a/packages/cli/src/cli/platform_override.rs b/packages/cli/src/cli/platform_override.rs new file mode 100644 index 0000000000..525c795343 --- /dev/null +++ b/packages/cli/src/cli/platform_override.rs @@ -0,0 +1,174 @@ +#![allow(dead_code)] +use clap::parser::ValueSource; +use clap::{ArgMatches, Args, CommandFactory, FromArgMatches, Parser, Subcommand}; + +/// Wraps a component with the subcommands `@server` and `@client` which will let you override the +/// base arguments for the client and server instances. +#[derive(Debug, Clone, Default)] +pub struct CommandWithPlatformOverrides { + /// The arguments that are shared between the client and server + pub shared: T, + /// The merged arguments for the server + pub server: Option, + /// The merged arguments for the client + pub client: Option, +} + +impl CommandWithPlatformOverrides { + pub(crate) fn with_client_or_shared<'a, O>(&'a self, f: impl FnOnce(&'a T) -> O) -> O { + match &self.client { + Some(client) => f(client), + None => f(&self.shared), + } + } + + pub(crate) fn with_server_or_shared<'a, O>(&'a self, f: impl FnOnce(&'a T) -> O) -> O { + match &self.server { + Some(server) => f(server), + None => f(&self.shared), + } + } +} + +impl Parser for CommandWithPlatformOverrides {} + +impl CommandFactory for CommandWithPlatformOverrides { + fn command() -> clap::Command { + T::command() + } + + fn command_for_update() -> clap::Command { + T::command_for_update() + } +} + +impl Args for CommandWithPlatformOverrides +where + T: Args, +{ + fn augment_args(cmd: clap::Command) -> clap::Command { + T::augment_args(cmd).defer(|cmd| { + PlatformOverrides::::augment_subcommands(cmd.disable_help_subcommand(true)) + }) + } + + fn augment_args_for_update(_cmd: clap::Command) -> clap::Command { + unimplemented!() + } +} + +fn merge_matches(base: &ArgMatches, platform: &ArgMatches) -> Result { + let mut base = T::from_arg_matches(base)?; + + let mut platform = platform.clone(); + let original_ids: Vec<_> = platform.ids().cloned().collect(); + for arg_id in original_ids { + let arg_name = arg_id.as_str(); + // Remove any default values from the platform matches + if platform.value_source(arg_name) == Some(ValueSource::DefaultValue) { + _ = platform.try_clear_id(arg_name); + } + } + + // Then merge the stripped platform matches into the base matches + base.update_from_arg_matches(&platform)?; + + Ok(base) +} + +impl FromArgMatches for CommandWithPlatformOverrides +where + T: Args, +{ + fn from_arg_matches(matches: &ArgMatches) -> Result { + let mut client = None; + let mut server = None; + let mut subcommand = matches.subcommand(); + while let Some((name, sub_matches)) = subcommand { + match name { + "@client" => { + client = Some(sub_matches); + } + "@server" => { + server = Some(sub_matches); + } + _ => {} + } + subcommand = sub_matches.subcommand(); + } + + let shared = T::from_arg_matches(matches)?; + let client = client + .map(|client| merge_matches::(matches, client)) + .transpose()?; + let server = server + .map(|server| merge_matches::(matches, server)) + .transpose()?; + + Ok(Self { + shared, + server, + client, + }) + } + + fn update_from_arg_matches(&mut self, _matches: &ArgMatches) -> Result<(), clap::Error> { + unimplemented!() + } +} + +/// Chain together multiple target commands +#[derive(Debug, Subcommand, Clone)] +#[command(subcommand_precedence_over_arg = true)] +pub(crate) enum PlatformOverrides { + /// Specify the arguments for the client build + #[clap(name = "@client")] + Client(ChainedCommand>), + + /// Specify the arguments for the server build + #[clap(name = "@server")] + Server(ChainedCommand>), +} + +// https://github.com/clap-rs/clap/issues/2222#issuecomment-2524152894 +// +// +/// `[Args]` wrapper to match `T` variants recursively in `U`. +#[derive(Debug, Clone)] +pub struct ChainedCommand { + /// Specific Variant. + pub inner: T, + + /// Enum containing `Self` variants, in other words possible follow-up commands. + pub next: Option>, +} + +impl Args for ChainedCommand +where + T: Args, + U: Subcommand, +{ + fn augment_args(cmd: clap::Command) -> clap::Command { + // We use the special `defer` method which lets us recursively call `augment_args` on the inner command + // and thus `from_arg_matches` + T::augment_args(cmd).defer(|cmd| U::augment_subcommands(cmd.disable_help_subcommand(true))) + } + + fn augment_args_for_update(_cmd: clap::Command) -> clap::Command { + unimplemented!() + } +} + +impl FromArgMatches for ChainedCommand +where + T: Args, + U: Subcommand, +{ + fn from_arg_matches(_: &ArgMatches) -> Result { + unimplemented!() + } + + fn update_from_arg_matches(&mut self, _matches: &ArgMatches) -> Result<(), clap::Error> { + unimplemented!() + } +} diff --git a/packages/cli/src/cli/serve.rs b/packages/cli/src/cli/serve.rs index a7c17897ed..1607e96674 100644 --- a/packages/cli/src/cli/serve.rs +++ b/packages/cli/src/cli/serve.rs @@ -50,10 +50,6 @@ pub(crate) struct ServeArgs { #[clap(long)] pub(crate) cross_origin_policy: bool, - /// Additional arguments to pass to the executable - #[clap(long)] - pub(crate) args: Vec, - /// Sets the interval in seconds that the CLI will poll for file changes on WSL. #[clap(long, default_missing_value = "2")] pub(crate) wsl_file_poll_interval: Option, @@ -76,8 +72,19 @@ pub(crate) struct ServeArgs { #[clap(long)] pub(crate) force_sequential: bool, + /// Platform-specific arguments for the build + #[clap(flatten)] + pub(crate) platform_args: CommandWithPlatformOverrides, +} + +#[derive(Clone, Debug, Default, Parser)] +pub(crate) struct PlatformServeArgs { #[clap(flatten)] pub(crate) targets: BuildArgs, + + /// Additional arguments to pass to the executable + #[clap(long, default_value = "")] + pub(crate) args: String, } impl ServeArgs { diff --git a/packages/cli/src/cli/target.rs b/packages/cli/src/cli/target.rs index 8eb5a573e0..3d33c2e627 100644 --- a/packages/cli/src/cli/target.rs +++ b/packages/cli/src/cli/target.rs @@ -1,6 +1,5 @@ use crate::cli::*; use crate::Platform; -use clap::{ArgMatches, Args, FromArgMatches, Subcommand}; use target_lexicon::Triple; /// A single target to build for @@ -96,77 +95,3 @@ pub(crate) struct TargetArgs { #[clap(long)] pub(crate) base_path: Option, } - -/// Chain together multiple target commands -#[derive(Debug, Subcommand, Clone)] -#[command(subcommand_precedence_over_arg = true)] -pub(crate) enum TargetCmd { - /// Specify the arguments for the client build - #[clap(name = "@client")] - Client(ChainedCommand), - - /// Specify the arguments for the server build - #[clap(name = "@server")] - Server(ChainedCommand), -} - -// https://github.com/clap-rs/clap/issues/2222#issuecomment-2524152894 -// -// -/// `[Args]` wrapper to match `T` variants recursively in `U`. -#[derive(Debug, Clone)] -pub struct ChainedCommand { - /// Specific Variant. - pub inner: T, - - /// Enum containing `Self` variants, in other words possible follow-up commands. - pub next: Option>, -} - -impl Args for ChainedCommand -where - T: Args, - U: Subcommand, -{ - fn augment_args(cmd: clap::Command) -> clap::Command { - // We use the special `defer` method which lets us recursively call `augment_args` on the inner command - // and thus `from_arg_matches` - T::augment_args(cmd).defer(|cmd| U::augment_subcommands(cmd.disable_help_subcommand(true))) - } - - fn augment_args_for_update(_cmd: clap::Command) -> clap::Command { - unimplemented!() - } -} - -impl FromArgMatches for ChainedCommand -where - T: Args, - U: Subcommand, -{ - fn from_arg_matches(matches: &ArgMatches) -> Result { - // Parse the first command before we try to parse the next one. - let inner = T::from_arg_matches(matches)?; - - // Try to parse the remainder of the command as a subcommand. - let next = match matches.subcommand() { - // Subcommand skips into the matched .subcommand, hence we need to pass *outer* matches, ignoring the inner matches - // (which in the average case should only match enumerated T) - // - // Here, we might want to eventually enable arbitrary names of subcommands if they're prefixed - // with a prefix like "@" ie `dx serve @dog-app/backend --args @dog-app/frontend --args` - // - // we are done, since sub-sub commands are matched in U:: - Some(_) => Some(Box::new(U::from_arg_matches(matches)?)), - - // no subcommand matched, we are done - None => None, - }; - - Ok(Self { inner, next }) - } - - fn update_from_arg_matches(&mut self, _matches: &ArgMatches) -> Result<(), clap::Error> { - unimplemented!() - } -} diff --git a/packages/cli/src/serve/runner.rs b/packages/cli/src/serve/runner.rs index cc1153ab76..6e3ffe0841 100644 --- a/packages/cli/src/serve/runner.rs +++ b/packages/cli/src/serve/runner.rs @@ -1,7 +1,8 @@ use super::{AppBuilder, ServeUpdate, WebServer}; use crate::{ - BuildArtifacts, BuildId, BuildMode, BuildTargets, Error, HotpatchModuleCache, Platform, Result, - ServeArgs, TailwindCli, TraceSrc, Workspace, + platform_override::CommandWithPlatformOverrides, BuildArtifacts, BuildId, BuildMode, + BuildTargets, Error, HotpatchModuleCache, Platform, Result, ServeArgs, TailwindCli, TraceSrc, + Workspace, }; use anyhow::Context; use dioxus_core::internal::{ @@ -73,6 +74,11 @@ pub(crate) struct AppServer { pub(crate) proxied_port: Option, pub(crate) cross_origin_policy: bool, + // The arguments that should be forwarded to the client app when it is opened + pub(crate) client_args: Vec, + // The arguments that should be forwarded to the server app when it is opened + pub(crate) server_args: Vec, + // Additional plugin-type tools pub(crate) tw_watcher: tokio::task::JoinHandle>, } @@ -93,6 +99,13 @@ impl AppServer { let force_sequential = args.force_sequential; let cross_origin_policy = args.cross_origin_policy; + // Find the launch args for the client and server + let split_args = |args: &str| args.split(' ').map(|s| s.to_string()).collect::>(); + let server_args = args.platform_args.with_server_or_shared(|c| &c.args); + let server_args = split_args(server_args); + let client_args = args.platform_args.with_client_or_shared(|c| &c.args); + let client_args = split_args(client_args); + // These come from the args but also might come from the workspace settings // We opt to use the manually specified args over the workspace settings let hot_reload = args @@ -125,7 +138,12 @@ impl AppServer { let (watcher_tx, watcher_rx) = futures_channel::mpsc::unbounded(); let watcher = create_notify_watcher(watcher_tx.clone(), wsl_file_poll_interval as u64); - let BuildTargets { client, server } = args.targets.into_targets().await?; + let target_args = CommandWithPlatformOverrides { + shared: args.platform_args.shared.targets, + server: args.platform_args.server.map(|s| s.targets), + client: args.platform_args.client.map(|c| c.targets), + }; + let BuildTargets { client, server } = target_args.into_targets().await?; // All servers will end up behind us (the devserver) but on a different port // This is so we can serve a loading screen as well as devtools without anything particularly fancy @@ -187,6 +205,8 @@ impl AppServer { cross_origin_policy, fullstack, tw_watcher, + server_args, + client_args, }; // Only register the hot-reload stuff if we're watching the filesystem @@ -546,6 +566,7 @@ impl AppServer { false, false, BuildId::SERVER, + &self.server_args, ) .await?; } @@ -560,6 +581,7 @@ impl AppServer { open_browser, self.always_on_top, BuildId::CLIENT, + &self.client_args, ) .await?;