Skip to content

Commit

Permalink
New default compilation profiles for web commands (#199)
Browse files Browse the repository at this point in the history
# Objective

Closes #197.

The `bevy build web` and `bevy run web` commands currently use the same
default profiles as the `cargo` commands, so either `dev` or `release`
if the `--release` flag is provided.
However, the demands of web apps can be vastly different than native
apps when it comes to compilation.
For example, it's a lot more effective to optimize for low binary size
on the web, because it significantly reduces the loading time of the
app.

This causes two problems with the current implementation:
- The defaults are bad. If the user doesn't know about the differences,
they will probably have high loading times for their apps or potentially
longer compile times.
- Fixing the problem results in boilerplate. Once you set up custom
profiles for a better configuration, you will have to provide the
`--profile={profile_name}` argument *every* time you execute `bevy run
web`... which is both a hassle and easy to forget.

# Solution

The web commands now use the `web` or `web-release` profiles by default.
This means we can provide better defaults, while still allowing the user
to fully customize the configuration (by just defining these profiles
themselves in `Cargo.toml`).
It also means the user doesn't have to pass the `--profile` flag to the
web commands, in most cases.

Of course, by default there won't be any definitions for the `web` and
`web-release` profiles and we don't want to edit the user's `Cargo.toml`
directly. So if we configured `--profile=web` we would get an error.
We circumvent this problem with [`--config` flag
overrides](https://doc.rust-lang.org/cargo/reference/config.html#command-line-overrides),
which essentially allow you to add additional entries to the
`Cargo.toml`.
For example, `--config profile.web.inherits="dev"` allows us to add the
`web` profile. It can be further customized with default settings in a
similar way.

I just set up very basic default configurations for the new profiles,
optimizing them will be a task for a future PR.
  • Loading branch information
TimJentzsch authored Dec 31, 2024
1 parent d6034dd commit d5f6bf7
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 26 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,16 @@ actix-web = "4.9.0"
# Opening the app in the browser
webbrowser = "1.0.2"

# Parsing the Cargo manifest
toml_edit = "0.22.22"

# Copying directories
fs_extra = "1.3.0"

# Optimizing Wasm binaries
wasm-opt = { version = "0.116.1", optional = true }

[build-dependencies]
# We don't use `cc` directly, but our dependency `wasm-opt-sys` fails to compile on Windows when using a newer version.
# This can be removed when https://github.com/rust-lang/cc-rs/issues/1324 is fixed.
cc = "=1.2.2"
2 changes: 1 addition & 1 deletion src/build/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ impl BuildArgs {

/// The profile used to compile the app.
pub(crate) fn profile(&self) -> &str {
self.cargo_args.compilation_args.profile()
self.cargo_args.compilation_args.profile(self.is_web())
}

/// The targeted platform.
Expand Down
19 changes: 12 additions & 7 deletions src/build/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,23 @@ use args::BuildSubcommands;
use crate::{
external_cli::{cargo, rustup, wasm_bindgen, CommandHelpers},
run::select_run_binary,
web::bundle::{create_web_bundle, PackedBundle, WebBundle},
web::{
bundle::{create_web_bundle, PackedBundle, WebBundle},
profiles::configure_default_web_profiles,
},
};

pub use self::args::BuildArgs;

mod args;

pub fn build(args: &BuildArgs) -> anyhow::Result<()> {
let cargo_args = args.cargo_args_builder();
let mut cargo_args = args.cargo_args_builder();

if let Some(BuildSubcommands::Web(web_args)) = &args.subcommand {
ensure_web_setup(args.skip_prompts)?;

let metadata = cargo::metadata::metadata_with_args(["--no-deps"])?;

println!("Compiling to WebAssembly...");
cargo::build::command().args(cargo_args).ensure_status()?;

println!("Bundling JavaScript bindings...");
let bin_target = select_run_binary(
&metadata,
args.cargo_args.package_args.package.as_deref(),
Expand All @@ -30,6 +28,13 @@ pub fn build(args: &BuildArgs) -> anyhow::Result<()> {
args.target().as_deref(),
args.profile(),
)?;

cargo_args = cargo_args.append(configure_default_web_profiles(&metadata)?);

println!("Compiling to WebAssembly...");
cargo::build::command().args(cargo_args).ensure_status()?;

println!("Bundling JavaScript bindings...");
wasm_bindgen::bundle(&bin_target)?;

#[cfg(feature = "wasm-opt")]
Expand Down
7 changes: 4 additions & 3 deletions src/external_cli/cargo/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,16 @@ pub struct Metadata {
/// List of members of the workspace.
///
/// Each entry is the Package ID for the package.
pub workspace_members: Option<Vec<String>>,
pub workspace_members: Vec<String>,
/// List of default members of the workspace.
///
/// Each entry is the Package ID for the package.
pub workspace_default_members: Option<Vec<String>>,
pub workspace_default_members: Vec<String>,
/// The absolute path to the build directory where Cargo places its output.
pub target_directory: PathBuf,
/// The absolute path to the root of the workspace.
pub workspace_root: Option<PathBuf>,
/// This will be the root of the package if no workspace is used.
pub workspace_root: PathBuf,
}

#[derive(Debug, Deserialize)]
Expand Down
17 changes: 11 additions & 6 deletions src/external_cli/cargo/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,17 @@ impl CargoCompilationArgs {
/// The profile used to compile the app.
///
/// This is determined by the `--release` and `--profile` arguments.
pub(crate) fn profile(&self) -> &str {
if self.is_release {
"release"
} else if let Some(profile) = &self.profile {
pub(crate) fn profile(&self, is_web: bool) -> &str {
if let Some(profile) = &self.profile {
profile
} else if is_web {
if self.is_release {
"web-release"
} else {
"web"
}
} else if self.is_release {
"release"
} else {
"debug"
}
Expand All @@ -97,8 +103,7 @@ impl CargoCompilationArgs {
};

ArgBuilder::new()
.add_flag_if("--release", self.is_release)
.add_opt_value("--profile", &self.profile)
.add_with_value("--profile", self.profile(is_web))
.add_opt_value("--jobs", &self.jobs.map(|jobs| jobs.to_string()))
.add_flag_if("--keep-going", self.is_keep_going)
.add_opt_value("--target", &target)
Expand Down
2 changes: 1 addition & 1 deletion src/run/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ impl RunArgs {

/// The profile used to compile the app.
pub(crate) fn profile(&self) -> &str {
self.cargo_args.compilation_args.profile()
self.cargo_args.compilation_args.profile(self.is_web())
}

/// The targeted platform.
Expand Down
21 changes: 13 additions & 8 deletions src/run/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ use crate::{
cargo::{self, metadata::Metadata},
wasm_bindgen, CommandHelpers,
},
web::bundle::{create_web_bundle, PackedBundle, WebBundle},
web::{
bundle::{create_web_bundle, PackedBundle, WebBundle},
profiles::configure_default_web_profiles,
},
};

pub use self::args::RunArgs;
Expand All @@ -18,18 +21,12 @@ mod args;
mod serve;

pub fn run(args: &RunArgs) -> anyhow::Result<()> {
let cargo_args = args.cargo_args_builder();
let mut cargo_args = args.cargo_args_builder();

if let Some(RunSubcommands::Web(web_args)) = &args.subcommand {
ensure_web_setup(args.skip_prompts)?;

let metadata = cargo::metadata::metadata_with_args(["--no-deps"])?;

// If targeting the web, run a web server with the WASM build
println!("Compiling to WebAssembly...");
cargo::build::command().args(cargo_args).ensure_status()?;

println!("Bundling JavaScript bindings...");
let bin_target = select_run_binary(
&metadata,
args.cargo_args.package_args.package.as_deref(),
Expand All @@ -38,6 +35,14 @@ pub fn run(args: &RunArgs) -> anyhow::Result<()> {
args.target().as_deref(),
args.profile(),
)?;

cargo_args = cargo_args.append(configure_default_web_profiles(&metadata)?);

// If targeting the web, run a web server with the WASM build
println!("Compiling to WebAssembly...");
cargo::build::command().args(cargo_args).ensure_status()?;

println!("Bundling JavaScript bindings...");
wasm_bindgen::bundle(&bin_target)?;

#[cfg(feature = "wasm-opt")]
Expand Down
1 change: 1 addition & 0 deletions src/web/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Utilities for building and running the app in the browser.
pub(crate) mod bundle;
pub(crate) mod profiles;
#[cfg(feature = "wasm-opt")]
pub(crate) mod wasm_opt;
104 changes: 104 additions & 0 deletions src/web/profiles.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use std::{collections::HashMap, fs};

use anyhow::Context as _;
use toml_edit::DocumentMut;

use crate::external_cli::{arg_builder::ArgBuilder, cargo::metadata::Metadata};

/// Create `--config` args to configure the default profiles to use when compiling for the web.
pub(crate) fn configure_default_web_profiles(metadata: &Metadata) -> anyhow::Result<ArgBuilder> {
let manifest = fs::read_to_string(metadata.workspace_root.join("Cargo.toml"))
.context("failed to read workspace manifest")?
.parse::<DocumentMut>()
.context("failed to parse workspace manifest")?;

let mut args = ArgBuilder::new();

if !is_profile_defined_in_manifest(&manifest, "web") {
args = args.append(configure_web_profile());
}

if !is_profile_defined_in_manifest(&manifest, "web-release") {
args = args.append(configure_web_release_profile());
}

Ok(args)
}

fn is_profile_defined_in_manifest(manifest: &DocumentMut, profile: &str) -> bool {
manifest
.get("profile")
.is_some_and(|profiles| profiles.get(profile).is_some())
}

/// Configure the default profile for web debug builds.
///
/// It is optimized for fast iteration speeds.
fn configure_web_profile() -> ArgBuilder {
configure_profile("web", "dev", HashMap::new())
}

/// Configure the default profile for web release builds.
///
/// It is optimized both for run time performance and loading times.
fn configure_web_release_profile() -> ArgBuilder {
let config = HashMap::from_iter([
// Optimize for size, greatly reducing loading times
("opt-level", "s"),
// Remove debug information, reducing file size further
("strip", "debuginfo"),
]);
configure_profile("web-release", "release", config)
}

/// Create `--config` args for `cargo` to configure a new compilation profile.
///
/// Equivalent to a `Cargo.toml` like this:
///
/// ```toml
/// [profile.{profile}]
/// inherits = "{inherits}"
/// # config
/// key = "value"
/// ```
fn configure_profile(profile: &str, inherits: &str, config: HashMap<&str, &str>) -> ArgBuilder {
let mut args = ArgBuilder::new().add_with_value(
"--config",
format!(r#"profile.{profile}.inherits="{inherits}""#),
);

for (key, value) in config {
args = args.add_with_value("--config", format!(r#"profile.{profile}.{key}="{value}""#));
}

args
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn should_detect_defined_profile() {
let manifest = r#"
[profile.web]
inherits = "dev"
"#
.parse()
.unwrap();

assert!(is_profile_defined_in_manifest(&manifest, "web"));
}

#[test]
fn should_detect_missing_profile() {
let manifest = r#"
[profile.foo]
inherits = "dev"
"#
.parse()
.unwrap();

assert!(!is_profile_defined_in_manifest(&manifest, "web"));
}
}

0 comments on commit d5f6bf7

Please sign in to comment.