Skip to content

Commit

Permalink
Bevy build (#102)
Browse files Browse the repository at this point in the history
This PR adds the remaining pieces needed for the `bevy build` command.

The command is a wrapper around `cargo build`, but with an additional
`web` sub command which makes it a lot easier to build an app for the
browser.
I also plan to add additional features in the feature, like #68.

The PR contains the following changes:

- Added `cargo install` wrapper to install a program if it's missing
(prompting the user).
- Added helper to obtain the current package name via `Cargo.toml`.
There are likely better ways to do this (perhaps via `cargo metadata`),
but I already had this implemented and we can improve it in a follow-up
if needed.
- Added helper to determine the active compilation profile, which is
needed to determine the path to the build artifact.
- Implemented the build command itself.
  • Loading branch information
TimJentzsch authored Sep 29, 2024
1 parent 12afdf9 commit 5cd2719
Show file tree
Hide file tree
Showing 13 changed files with 204 additions and 5 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,10 @@ serde_json = "1.0.128"
reqwest = { features = ["blocking", "json"], version = "0.12.7" }
regex = "1.10.6"

# Understanding Cargo.toml
toml_edit = { version = "0.22.21", default-features = false, features = [
"parse",
] }

# Understanding package versions
semver = { version = "1.0.23", features = ["serde"] }
8 changes: 8 additions & 0 deletions src/bin/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::Result;
use bevy_cli::build::BuildArgs;
use clap::{Args, Parser, Subcommand};

fn main() -> Result<()> {
Expand All @@ -9,6 +10,7 @@ fn main() -> Result<()> {
bevy_cli::template::generate_template(&new.name, &new.template, &new.branch)?;
}
Subcommands::Lint { args } => bevy_cli::lint::lint(args)?,
Subcommands::Build(args) => bevy_cli::build::build(&args)?,
}

Ok(())
Expand All @@ -27,10 +29,16 @@ pub struct Cli {
}

/// Available subcommands for `bevy`.
#[expect(
clippy::large_enum_variant,
reason = "Only constructed once, not expected to have a performance impact."
)]
#[derive(Subcommand)]
pub enum Subcommands {
/// Create a new Bevy project from a specified template.
New(NewArgs),
/// Build your Bevy app.
Build(BuildArgs),
/// Check the current project using Bevy-specific lints.
///
/// This command requires `bevy_lint` to be installed, and will fail if it is not. Please see
Expand Down
37 changes: 37 additions & 0 deletions src/build/args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use clap::{Args, Subcommand};

use crate::external_cli::{arg_builder::ArgBuilder, cargo::build::CargoBuildArgs};

#[derive(Debug, Args)]
pub struct BuildArgs {
/// The subcommands available for the build command.
#[clap(subcommand)]
pub subcommand: Option<BuildSubcommands>,

/// Arguments to forward to `cargo build`.
#[clap(flatten)]
pub cargo_args: CargoBuildArgs,
}

impl BuildArgs {
/// Determine if the app is being built for the web.
pub(crate) fn is_web(&self) -> bool {
matches!(self.subcommand, Some(BuildSubcommands::Web))
}

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

/// Generate arguments to forward to `cargo build`.
pub(crate) fn cargo_args_builder(&self) -> ArgBuilder {
self.cargo_args.args_builder(self.is_web())
}
}

#[derive(Debug, Subcommand)]
pub enum BuildSubcommands {
/// Build your app for the browser.
Web,
}
36 changes: 36 additions & 0 deletions src/build/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use crate::{
external_cli::{cargo, rustup, wasm_bindgen, CommandHelpers},
manifest::package_name,
};

pub use self::args::BuildArgs;

mod args;

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

if args.is_web() {
ensure_web_setup()?;

println!("Building for WASM...");
cargo::build::command().args(cargo_args).ensure_status()?;

println!("Bundling for the web...");
wasm_bindgen::bundle(&package_name()?, args.profile())
.expect("Failed to bundle for the web");
} else {
cargo::build::command().args(cargo_args).ensure_status()?;
}

Ok(())
}

pub(crate) fn ensure_web_setup() -> anyhow::Result<()> {
// `wasm32-unknown-unknown` compilation target
rustup::install_target_if_needed("wasm32-unknown-unknown")?;
// `wasm-bindgen-cli` for bundling
cargo::install::if_needed(wasm_bindgen::PROGRAM, wasm_bindgen::PACKAGE, true, false)?;

Ok(())
}
56 changes: 56 additions & 0 deletions src/external_cli/cargo/install.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use std::process::{exit, Command};

use dialoguer::Confirm;

/// Check if the given program is installed on the system.
///
/// This assumes that the program offers a `--version` flag.
fn is_installed(program: &str) -> bool {
let output = Command::new(program).arg("--version").output();

if let Ok(output) = output {
output.status.success()
} else {
false
}
}

/// Checks if the program is installed and installs it if it isn't.
///
/// Returns `true` if the program needed to be installed.
pub(crate) fn if_needed(
program: &str,
package: &str,
ask_user: bool,
hidden: bool,
) -> anyhow::Result<bool> {
if is_installed(program) {
return Ok(false);
}

// Abort if the user doesn't want to install it
if ask_user
&& !Confirm::new()
.with_prompt(format!(
"`{program}` is missing, should I install it for you?"
))
.interact()?
{
exit(1);
}

let mut cmd = Command::new(super::program());
cmd.arg("install").arg(package);

let status = if hidden {
cmd.output()?.status
} else {
cmd.status()?
};

if !status.success() {
Err(anyhow::anyhow!("Failed to install `{program}`."))
} else {
Ok(true)
}
}
1 change: 1 addition & 0 deletions src/external_cli/cargo/metadata.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#![expect(dead_code, reason = "Will be used for bevy bump and perhaps bevy run")]
use std::{ffi::OsStr, path::PathBuf, process::Command};

use semver::{Version, VersionReq};
Expand Down
15 changes: 14 additions & 1 deletion src/external_cli/cargo/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
#![expect(dead_code, reason = "Will be used for `bevy build` and `bevy run`")]
use std::{env, ffi::OsString};

use clap::{ArgAction, Args};

use super::arg_builder::ArgBuilder;

pub(crate) mod build;
pub(crate) mod install;
pub(crate) mod metadata;
pub(crate) mod run;

Expand Down Expand Up @@ -67,6 +67,19 @@ pub struct CargoCompilationArgs {
}

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 {
profile
} else {
"debug"
}
}

pub(crate) fn args_builder(&self, is_web: bool) -> ArgBuilder {
// web takes precedence over --target <TRIPLE>
let target = if is_web {
Expand Down
20 changes: 20 additions & 0 deletions src/external_cli/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
//! Wrappers and utilities to deal with external CLI applications, like `cargo`.

use std::process::{Command, ExitStatus};

pub mod arg_builder;
pub(crate) mod cargo;
pub(crate) mod rustup;
pub(crate) mod wasm_bindgen;

pub trait CommandHelpers {
fn ensure_status(&mut self) -> anyhow::Result<ExitStatus>;
}

impl CommandHelpers for Command {
/// Ensure that the command exits with a successful status code.
fn ensure_status(&mut self) -> anyhow::Result<ExitStatus> {
let status = self.status()?;
anyhow::ensure!(
status.success(),
"Command {} exited with status code {}",
self.get_program().to_str().unwrap_or_default(),
status
);
Ok(status)
}
}
2 changes: 0 additions & 2 deletions src/external_cli/rustup.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
//! Utilities for the `rustup` CLI tool.

#![expect(dead_code, reason = "Will be used for build/run commands")]

use std::{env, ffi::OsString, process::Command};

use dialoguer::Confirm;
Expand Down
2 changes: 0 additions & 2 deletions src/external_cli/wasm_bindgen.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#![expect(dead_code, reason = "Will be used for the build/run commands")]

use std::process::Command;

use super::arg_builder::ArgBuilder;
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! The library backend for the Bevy CLI.

pub mod build;
pub mod external_cli;
pub mod lint;
pub mod manifest;
pub mod template;
24 changes: 24 additions & 0 deletions src/manifest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use std::{fs::File, io::Read as _};

use toml_edit::{DocumentMut, Item, Value};

/// Get the contents of the manifest file.
fn get_cargo_toml(folder_name: &str) -> anyhow::Result<DocumentMut> {
let mut file = File::open(format!("{folder_name}/Cargo.toml"))?;

let mut content = String::new();
file.read_to_string(&mut content)?;

Ok(content.parse()?)
}

/// Determine the name of the cargo package.
pub(crate) fn package_name() -> anyhow::Result<String> {
let cargo_toml = get_cargo_toml("./")?;

if let Item::Value(Value::String(name)) = &cargo_toml["package"]["name"] {
Ok(name.value().clone())
} else {
Err(anyhow::anyhow!("No package name defined in Cargo.toml"))
}
}

0 comments on commit 5cd2719

Please sign in to comment.