Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bevy run #120

Merged
merged 8 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
669 changes: 658 additions & 11 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,10 @@ toml_edit = { version = "0.22.21", default-features = false, features = [

# Understanding package versions
semver = { version = "1.0.23", features = ["serde"] }

# Serving the app for the browser
actix-files = "0.6.6"
actix-web = "4.9.0"

# Opening the app in the browser
webbrowser = "1.0.2"
163 changes: 163 additions & 0 deletions assets/web/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Bevy App</title>
TimJentzsch marked this conversation as resolved.
Show resolved Hide resolved
<style>
/* Styles for the loading screen */
:root {
--web-bg-color: #282828;
}

* {
margin: 0;
padding: 0;
border: 0;
}

html,
body {
width: 100%;
height: 100%;
}

.center {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}

#game {
background-color: var(--web-bg-color);
}

.spinner {
width: 128px;
height: 128px;
border: 64px solid transparent;
border-bottom-color: #ececec;
border-right-color: #b2b2b2;
border-top-color: #787878;
border-radius: 50%;
box-sizing: border-box;
animation: spin 1.2s linear infinite;
}

@keyframes spin {
0% {
transform: rotate(0deg);
}

100% {
transform: rotate(360deg);
}
}

#bevy {
/* Hide Bevy app before it loads */
height: 0;
}
</style>
</head>

<body>
<div id="game" class="center">
<div id="loading-screen" class="center">
<span class="spinner"></span>
</div>

<canvas id="bevy">Javascript and canvas support is required</canvas>
</div>

<script type="module">
// Starting the game
import init from "./build/bevy_app.js";
init().catch((error) => {
if (
!error.message.startsWith(
"Using exceptions for control flow, don't mind me. This isn't actually an error!"
)
) {
throw error;
}
});
</script>

<script type="module">
// Hide loading screen when the game starts.
const loading_screen = document.getElementById("loading-screen");
const bevy = document.getElementById("bevy");
const observer = new MutationObserver(() => {
if (bevy.height > 1) {
loading_screen.style.display = "none";
observer.disconnect();
}
});
observer.observe(bevy, { attributeFilter: ["height"] });
</script>

<script type="module">
// Script to restart the audio context
// Taken from https://developer.chrome.com/blog/web-audio-autoplay/#moving-forward
(function () {
// An array of all contexts to resume on the page
const audioContextList = [];

// An array of various user interaction events we should listen for
const userInputEventNames = [
"click",
"contextmenu",
"auxclick",
"dblclick",
"mousedown",
"mouseup",
"pointerup",
"touchend",
"keydown",
"keyup",
];

// A proxy object to intercept AudioContexts and
// add them to the array for tracking and resuming later
self.AudioContext = new Proxy(self.AudioContext, {
construct(target, args) {
const result = new target(...args);
audioContextList.push(result);
return result;
},
});

// To resume all AudioContexts being tracked
function resumeAllContexts(event) {
let count = 0;

audioContextList.forEach((context) => {
if (context.state !== "running") {
context.resume();
} else {
count++;
}
});

// If all the AudioContexts have now resumed then we
// unbind all the event listeners from the page to prevent
// unnecessary resume attempts
if (count == audioContextList.length) {
userInputEventNames.forEach((eventName) => {
document.removeEventListener(eventName, resumeAllContexts);
});
}
}

// We bind the resume function for each user interaction
// event on the page
userInputEventNames.forEach((eventName) => {
document.addEventListener(eventName, resumeAllContexts);
});
})();
</script>
</body>
</html>
9 changes: 4 additions & 5 deletions src/bin/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use anyhow::Result;
use bevy_cli::build::BuildArgs;
use bevy_cli::{build::BuildArgs, run::RunArgs};
use clap::{Args, Parser, Subcommand};

fn main() -> Result<()> {
Expand All @@ -11,6 +11,7 @@ fn main() -> Result<()> {
}
Subcommands::Lint { args } => bevy_cli::lint::lint(args)?,
Subcommands::Build(args) => bevy_cli::build::build(&args)?,
Subcommands::Run(args) => bevy_cli::run::run(&args)?,
}

Ok(())
Expand All @@ -29,16 +30,14 @@ 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),
/// Run your Bevy app.
Run(RunArgs),
/// 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
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ pub mod build;
pub mod external_cli;
pub mod lint;
pub mod manifest;
pub mod run;
pub mod template;
48 changes: 48 additions & 0 deletions src/run/args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use clap::{ArgAction, Args, Subcommand};

use crate::external_cli::{arg_builder::ArgBuilder, cargo::run::CargoRunArgs};

#[derive(Debug, Args)]
pub struct RunArgs {
/// The subcommands available for the run command.
#[command(subcommand)]
pub subcommand: Option<RunSubcommands>,

/// Commands to forward to `cargo run`.
#[clap(flatten)]
pub cargo_args: CargoRunArgs,
}

impl RunArgs {
/// Whether to run the app in the browser.
pub(crate) fn is_web(&self) -> bool {
matches!(self.subcommand, Some(RunSubcommands::Web(_)))
}

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

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

#[derive(Debug, Subcommand)]
pub enum RunSubcommands {
/// Run your app in the browser.
Web(RunWebArgs),
}

#[derive(Debug, Args)]
pub struct RunWebArgs {
/// The port to run the web server on.
#[arg(short, long, default_value_t = 4000)]
pub port: u16,

/// Open the app in the browser.
#[arg(short = 'o', long = "open", action = ArgAction::SetTrue, default_value_t = false)]
pub do_open: bool,
TimJentzsch marked this conversation as resolved.
Show resolved Hide resolved
TimJentzsch marked this conversation as resolved.
Show resolved Hide resolved
}
48 changes: 48 additions & 0 deletions src/run/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use args::RunSubcommands;

use crate::{
build::ensure_web_setup,
external_cli::{cargo, wasm_bindgen, CommandHelpers},
manifest::package_name,
};

pub use self::args::RunArgs;

mod args;
mod serve;

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

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

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

println!("Bundling for the web...");
wasm_bindgen::bundle(&package_name()?, args.profile())?;

let port = web_args.port;
let url = format!("http://127.0.0.1:{port}");
TimJentzsch marked this conversation as resolved.
Show resolved Hide resolved

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've had situations where https on localhost was necessary for development of a project. Do you think it would be nice to have the option here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my experience, setting up HTTPS is not worth it for a development server such as this. The primary purpose of TLS is to secure communication across untrusted networks, but 127.0.0.1 is completely local, so there's no network to worry about. bevy run web is not meant for production. In those cases you build everything ahead of time and serve it with something like Nginx, which implements TLS already.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to create a valid certificate for localhost to enable HTTPS?
I'd say that when we need it, we can create a follow-up issue to enable that functionality :)

Copy link

@BenjaminBrienen BenjaminBrienen Oct 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, assuming openssl or the like is installed. We can leave it to the user to create their own self signed cert. It's a common step for web developers and is usually its own step outside of other tooling.


// Serving the app is blocking, so we open the page first
if web_args.do_open {
if webbrowser::open(&url).is_err() {
println!("Failed to open the browser automatically, open the app on <{url}>");
} else {
println!("Your app is running on <{url}>");
}
TimJentzsch marked this conversation as resolved.
Show resolved Hide resolved
} else {
println!("Open your app on <{url}>");
TimJentzsch marked this conversation as resolved.
Show resolved Hide resolved
}

serve::serve(port, args.profile())?;
} else {
// For native builds, wrap `cargo run`
cargo::run::command().args(cargo_args).ensure_status()?;
}

Ok(())
}
60 changes: 60 additions & 0 deletions src/run/serve.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//! Serving the app locally for the browser.
use actix_web::{rt, web, App, HttpResponse, HttpServer, Responder};
use std::path::Path;

use crate::external_cli::wasm_bindgen;

/// If the user didn't provide an `index.html`, serve a default one.
async fn serve_default_index() -> impl Responder {
let content = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/assets/web/index.html"
));

// Build the HTTP response with appropriate headers to serve the content as a file
HttpResponse::Ok()
.insert_header((
actix_web::http::header::CONTENT_TYPE,
"text/html; charset=utf-8",
))
.body(content)
}

/// Launch a web server running the Bevy app.
pub(crate) fn serve(port: u16, profile: &str) -> anyhow::Result<()> {
let profile = profile.to_string();

rt::System::new().block_on(
HttpServer::new(move || {
let mut app = App::new();

// Serve the build artifacts at the `/build/*` route
// A custom `index.html` will have to call `/build/bevy_app.js`
let js_path = Path::new("bevy_app.js");
let wasm_path = Path::new("bevy_app_bg.wasm");
TimJentzsch marked this conversation as resolved.
Show resolved Hide resolved
app = app.service(
actix_files::Files::new("/build", wasm_bindgen::get_target_folder(&profile))
.path_filter(move |path, _| path == js_path || path == wasm_path),
);

// If the app has an assets folder, serve it under `/assets`
if Path::new("assets").exists() {
app = app.service(actix_files::Files::new("/assets", "./assets"))
}

if Path::new("web").exists() {
// Serve the contents of the `web` folder under `/`, if it exists
app = app.service(actix_files::Files::new("/", "./web").index_file("index.html"));
} else {
// If the user doesn't provide a custom web setup, serve a default `index.html`
app = app.route("/", web::get().to(serve_default_index))
}

app
})
.bind(("127.0.0.1", port))?
.run(),
)?;

Ok(())
}