A crate on top of benilla adds its own Bevy plugins through benilla_app::run_with, and a client started through it reads extended in its build line (#495)
A crate built on top of benilla now adds its own Bevy plugins through `benilla_app::run_with(build, extend)`: `extend` gets the client fully built, after the game plugins, the asset loaders and the `ExtractSchedule` executor flip, and before the probe fleet, which sees what it adds. `run` is `run_with` with nothing added, so the stock client is unchanged. - A client started through `run_with` carries `extended` in its build id (`BuildId::summary`), so the startup banner, the crash report and the debug panel's footer all say so, and a report from an extended client reads apart from stock. The entry point sets the flag; a launcher leaves it to `..Default::default()`. - `crates/benilla-app/examples/extended_launcher.rs` is a launcher with a plugin of its own, compiled by the gates. Its header is the shape a crate of its own copies: the manifest (benilla-app without default features and `dev` forwarded, the workspace's bevy, and the root's `[patch]` and `[profile.dev]`, which cargo reads from the root manifest only), the build script's stamp, and a `Cargo.lock` started from benilla's. - `tests/launcher_shape.rs` holds that documented manifest to the root `Cargo.toml` and the `benilla` launcher's, so a patch or profile added later cannot leave the documented shape building a client that is not benilla's. - `docs/CONTRIBUTING.md`: a feature 1.12.1 does not have lives in its own crate on top of benilla, started through `run_with`. Fixes #483 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
476da9b2f0
commit
8d31e9e91e
@@ -0,0 +1,82 @@
|
||||
//! `extended_launcher`: the client with a plugin of its own on top, started through
|
||||
//! [`benilla_app::run_with`] as a crate built on benilla starts it. A feature 1.12.1 does not have
|
||||
//! lives in a crate like this one, never in benilla.
|
||||
//!
|
||||
//! ```text
|
||||
//! cargo run -p benilla-app --example extended_launcher
|
||||
//! ```
|
||||
//!
|
||||
//! The banner reads `benilla build … · extended`, and the plugin logs one line at `Startup`.
|
||||
//!
|
||||
//! A crate of its own copies the `benilla` launcher (`crates/benilla`): a build script that stamps
|
||||
//! its commit, a `main` like this one that reads the stamp back as `crates/benilla/src/main.rs`
|
||||
//! does, and a manifest that takes benilla as the workspace does:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [features]
|
||||
//! default = ["dev"]
|
||||
//! # The instruments; the player build is `--no-default-features`.
|
||||
//! dev = ["benilla-app/dev"]
|
||||
//!
|
||||
//! [dependencies]
|
||||
//! # `default-features = false`, or `--no-default-features` still builds benilla-app with `dev`.
|
||||
//! benilla-app = { git = "https://github.com/samwhosung/benilla", default-features = false }
|
||||
//! # benilla's version, adding no features: bevy's defaults bring its audio stack back.
|
||||
//! bevy = { version = "0.18.1", default-features = false }
|
||||
//!
|
||||
//! [build-dependencies]
|
||||
//! benilla-buildstamp = { git = "https://github.com/samwhosung/benilla" }
|
||||
//!
|
||||
//! # Cargo reads `[patch]` and `[profile]` from the root manifest only, so benilla's are repeated:
|
||||
//! # without the patches the addon Lua and the mixer are not benilla's.
|
||||
//! [patch.crates-io]
|
||||
//! lua-src = { git = "https://github.com/samwhosung/benilla" }
|
||||
//! kira = { git = "https://github.com/samwhosung/benilla" }
|
||||
//!
|
||||
//! [profile.dev]
|
||||
//! opt-level = 1
|
||||
//!
|
||||
//! [profile.dev.package."*"]
|
||||
//! opt-level = 3
|
||||
//! ```
|
||||
//!
|
||||
//! ```text
|
||||
//! // build.rs
|
||||
//! fn main() {
|
||||
//! benilla_buildstamp::emit();
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Its `Cargo.lock` starts as a copy of benilla's: cargo reads only the root's lockfile too, and
|
||||
//! without it resolves every crate to its newest compatible version, not the one benilla is built
|
||||
//! with. A dev build looks for the `WoW` link and `benilla-config/` at benilla's repo root, which
|
||||
//! for a git dependency is cargo's checkout, so `WOW_DATA` and `BENILLA_HOME` name them instead.
|
||||
|
||||
use benilla_app::{AppExit, BuildId};
|
||||
use bevy::prelude::*;
|
||||
|
||||
/// The crate's own plugin: one line at `Startup`, to show it runs inside the client.
|
||||
struct HelloPlugin;
|
||||
|
||||
impl Plugin for HelloPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(Startup, || info!("a plugin on top of benilla is running"));
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> AppExit {
|
||||
// A launcher of its own passes the stamp its build script made; an example has no build
|
||||
// script, so this names the version and profile alone.
|
||||
let build = BuildId {
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
profile: if cfg!(debug_assertions) {
|
||||
"debug"
|
||||
} else {
|
||||
"release"
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
benilla_app::run_with(build, |app| {
|
||||
app.add_plugins(HelloPlugin);
|
||||
})
|
||||
}
|
||||
@@ -16,8 +16,9 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use crate::BuildId;
|
||||
|
||||
/// Chains the crash-report hook after the current one. Called once from [`crate::run`], before
|
||||
/// the `App` exists, so a panic while plugins build is covered.
|
||||
/// Chains the crash-report hook after the current one. Called once as the client launches
|
||||
/// ([`crate::run`], [`crate::run_with`]), before the `App` exists, so a panic while plugins build
|
||||
/// is covered.
|
||||
pub(crate) fn install(build: BuildId) {
|
||||
let started = Instant::now();
|
||||
let previous = std::panic::take_hook();
|
||||
@@ -139,12 +140,13 @@ mod tests {
|
||||
short: "0123456",
|
||||
date: "2026-09-16",
|
||||
profile: "debug",
|
||||
extended: false,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn a_report_carries_the_build_the_place_the_payload_and_the_log() {
|
||||
let log = vec!["+ 1.000s INFO benilla_app::net: entered world".to_owned()];
|
||||
let text = render(&Report {
|
||||
let mut report = Report {
|
||||
build: BUILD,
|
||||
unix: 1_800_000_000,
|
||||
uptime: Duration::from_millis(12_345),
|
||||
@@ -153,10 +155,10 @@ mod tests {
|
||||
payload: "index out of bounds: the len is 3 but the index is 7",
|
||||
backtrace: " 0: benilla_app::net::apply::apply_net_updates",
|
||||
log: &log,
|
||||
});
|
||||
};
|
||||
let text = render(&report);
|
||||
for needle in [
|
||||
"sha 0123456789abcdef0123456789abcdef01234567",
|
||||
"0.2.0+3 · 0123456 · 2026-09-16 · debug",
|
||||
"build: 0.2.0+3 · 0123456 · 2026-09-16 · debug (sha 0123456789abcdef0123456789abcdef01234567)",
|
||||
"12.3 s after launch",
|
||||
"thread: main",
|
||||
"at: crates/benilla-app/src/net/apply.rs:100:5",
|
||||
@@ -167,6 +169,13 @@ mod tests {
|
||||
] {
|
||||
assert!(text.contains(needle), "missing {needle:?} in:\n{text}");
|
||||
}
|
||||
// A client a crate on top of benilla extended says so in the build line.
|
||||
report.build.extended = true;
|
||||
let extended = render(&report);
|
||||
assert!(
|
||||
extended.contains("build: 0.2.0+3 · 0123456 · 2026-09-16 · debug · extended (sha "),
|
||||
"{extended}"
|
||||
);
|
||||
|
||||
let dir = std::env::temp_dir().join(format!("benilla-crash-test-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
|
||||
@@ -190,8 +190,8 @@ mod world_state_ui;
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
// The `benilla` launcher shim stamps the build id at compile time and hands it to [`run`];
|
||||
// re-exported so the shim needs no bevy dependency of its own.
|
||||
// A launcher stamps the build id at compile time and hands it to [`run`] or [`run_with`];
|
||||
// re-exported so the `benilla` shim needs no bevy dependency of its own.
|
||||
pub use benilla_world::build_id::BuildId;
|
||||
/// The world viewer's entry point, the engine with no game attached, called by the
|
||||
/// `benilla-worldview` shim.
|
||||
@@ -201,6 +201,21 @@ pub use bevy::app::AppExit;
|
||||
/// Builds and runs the client app. `build` is the launcher's compile-time git stamp, passed in as
|
||||
/// data so the sha lives in the shim's fingerprint and a commit does not recompile this crate.
|
||||
pub fn run(build: BuildId) -> AppExit {
|
||||
launch(build, None)
|
||||
}
|
||||
|
||||
/// [`run`], plus `extend` adding a crate's own plugins: how a feature 1.12.1 lacks is built on top
|
||||
/// of benilla (`examples/extended_launcher.rs`), the build marked `extended` wherever it is named.
|
||||
/// `extend` gets the client fully built and runs before the probe fleet, which sees what it adds.
|
||||
pub fn run_with(build: BuildId, extend: impl FnOnce(&mut App)) -> AppExit {
|
||||
launch(build, Some(Box::new(extend)))
|
||||
}
|
||||
|
||||
/// What a crate on top of benilla adds to the built app ([`run_with`]).
|
||||
type Extension<'a> = Box<dyn FnOnce(&mut App) + 'a>;
|
||||
|
||||
/// The one body behind [`run`] and [`run_with`], not generic, so a launcher compiles none of it.
|
||||
fn launch(build: BuildId, extend: Option<Extension<'_>>) -> AppExit {
|
||||
// `WOW_HOVER_LOG_REPORT=<csv>` and `WOW_CAPTURE=list` print and exit before any setup.
|
||||
if let Ok(path) = std::env::var("WOW_HOVER_LOG_REPORT") {
|
||||
dev::report_recorded_hover_log(&path);
|
||||
@@ -216,6 +231,12 @@ pub fn run(build: BuildId) -> AppExit {
|
||||
return AppExit::Success;
|
||||
}
|
||||
|
||||
// Every line that names the build says whether a crate on top extended it.
|
||||
let build = BuildId {
|
||||
extended: extend.is_some(),
|
||||
..build
|
||||
};
|
||||
|
||||
// From here a panic leaves `benilla-config/Diagnostics/crash-<unix>.txt`; armed before the
|
||||
// `App` exists, so a panic while plugins build is reported too.
|
||||
crash::install(build);
|
||||
@@ -404,6 +425,11 @@ pub fn run(build: BuildId) -> AppExit {
|
||||
}
|
||||
}
|
||||
|
||||
// A crate on top of benilla adds its plugins here ([`run_with`]).
|
||||
if let Some(extend) = extend {
|
||||
extend(&mut app);
|
||||
}
|
||||
|
||||
// The probe fleet, last so it observes the fully-built app; compiled out by
|
||||
// `--no-default-features`.
|
||||
app.add_plugins(dev::DevProbesPlugin);
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
//! The launcher `examples/extended_launcher.rs` documents for a crate on top of benilla stays in
|
||||
//! step with the workspace: cargo reads `[patch]` and `[profile]` from the root manifest only, so
|
||||
//! the header repeats the root's, a missing patch builds a client that is silently not benilla's,
|
||||
//! and a bevy other than the workspace's is a second bevy. The rest mirrors the `benilla` launcher.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
/// A manifest by its path from this crate's root.
|
||||
fn manifest(rel: &str) -> toml::Table {
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(rel);
|
||||
std::fs::read_to_string(&path)
|
||||
.unwrap_or_else(|e| panic!("{}: {e}", path.display()))
|
||||
.parse()
|
||||
.unwrap_or_else(|e| panic!("{}: {e}", path.display()))
|
||||
}
|
||||
|
||||
/// The example header's ```toml block, parsed.
|
||||
fn documented() -> toml::Table {
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/extended_launcher.rs");
|
||||
let text = std::fs::read_to_string(&path).unwrap();
|
||||
let block: Vec<&str> = text
|
||||
.lines()
|
||||
.map(|l| {
|
||||
l.strip_prefix("//!")
|
||||
.map(|l| l.strip_prefix(' ').unwrap_or(l))
|
||||
})
|
||||
.skip_while(|l| *l != Some("```toml"))
|
||||
.skip(1)
|
||||
.take_while(|l| *l != Some("```"))
|
||||
.map(|l| l.expect("the ```toml block ends inside the header"))
|
||||
.collect();
|
||||
assert!(!block.is_empty(), "no ```toml block in {}", path.display());
|
||||
block
|
||||
.join("\n")
|
||||
.parse()
|
||||
.expect("the documented manifest parses")
|
||||
}
|
||||
|
||||
/// `t[k1][k2]…`, or a panic naming the path.
|
||||
fn at<'a>(t: &'a toml::Table, keys: &[&str]) -> &'a toml::Value {
|
||||
let (first, rest) = keys.split_first().unwrap();
|
||||
let mut v = t.get(*first).unwrap_or_else(|| panic!("no `{first}`"));
|
||||
for k in rest {
|
||||
v = v
|
||||
.get(*k)
|
||||
.unwrap_or_else(|| panic!("no `{}`", keys.join(".")));
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_documented_launcher_repeats_the_root_patches_profile_and_bevy() {
|
||||
let doc = documented();
|
||||
let root = manifest("../../Cargo.toml");
|
||||
let patched = |t: &toml::Table| -> Vec<String> {
|
||||
let table = at(t, &["patch", "crates-io"]).as_table().unwrap();
|
||||
table.keys().cloned().collect()
|
||||
};
|
||||
assert_eq!(patched(&doc), patched(&root), "[patch.crates-io]");
|
||||
for keys in [
|
||||
&["profile", "dev", "opt-level"][..],
|
||||
&["profile", "dev", "package", "*", "opt-level"],
|
||||
] {
|
||||
assert_eq!(at(&doc, keys), at(&root, keys), "{}", keys.join("."));
|
||||
}
|
||||
assert_eq!(
|
||||
at(&doc, &["dependencies", "bevy", "version"]),
|
||||
at(&root, &["workspace", "dependencies", "bevy", "version"]),
|
||||
"bevy's version"
|
||||
);
|
||||
assert_eq!(
|
||||
at(&doc, &["dependencies", "bevy", "default-features"]).as_bool(),
|
||||
Some(false),
|
||||
"bevy adds no features"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_documented_launcher_takes_benilla_app_as_the_benilla_launcher_does() {
|
||||
let doc = documented();
|
||||
let shim = manifest("../benilla/Cargo.toml");
|
||||
for keys in [
|
||||
&["features", "dev"][..],
|
||||
&["dependencies", "benilla-app", "default-features"],
|
||||
] {
|
||||
assert_eq!(at(&doc, keys), at(&shim, keys), "{}", keys.join("."));
|
||||
}
|
||||
assert!(
|
||||
at(&doc, &["build-dependencies"])
|
||||
.get("benilla-buildstamp")
|
||||
.is_some(),
|
||||
"the build script's stamp"
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
//! Which build is running: the release and the commit the binary was built from, stamped at
|
||||
//! compile time by the launcher shims' build script (`benilla-buildstamp`) and passed to `run` as
|
||||
//! the [`BuildId`] resource. It shows in the startup log line ([`banner`]), in crash reports and in
|
||||
//! the debug panel's footer.
|
||||
//! the [`BuildId`] resource, and whether a crate on top of benilla extended it. It shows in the
|
||||
//! startup log line ([`banner`]), in crash reports and in the debug panel's footer.
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
/// The launcher shim's compile-time stamp. The git fields are empty when the build had no
|
||||
/// checkout or no `git` on `PATH`, which [`BuildId::summary`] reports as an unknown commit.
|
||||
#[derive(Resource, Clone, Copy)]
|
||||
#[derive(Resource, Clone, Copy, Default)]
|
||||
pub struct BuildId {
|
||||
/// The workspace version from `Cargo.toml`, which every release bumps.
|
||||
pub version: &'static str,
|
||||
@@ -22,6 +22,9 @@ pub struct BuildId {
|
||||
pub date: &'static str,
|
||||
/// The cargo profile directory: `debug`, `release` or `ship`.
|
||||
pub profile: &'static str,
|
||||
/// Whether a crate on top of benilla added plugins through `benilla_app::run_with`. The entry
|
||||
/// point sets it, so a launcher leaves it to `..Default::default()`.
|
||||
pub extended: bool,
|
||||
}
|
||||
|
||||
impl BuildId {
|
||||
@@ -35,9 +38,10 @@ impl BuildId {
|
||||
}
|
||||
}
|
||||
|
||||
/// The one-line build id: release, short sha, commit date and profile.
|
||||
/// The one-line build id: release, short sha, commit date and profile, then `extended` when a
|
||||
/// crate on top added plugins, so a report from that client reads apart from stock.
|
||||
pub fn summary(&self) -> String {
|
||||
if self.short.is_empty() {
|
||||
let stamp = if self.short.is_empty() {
|
||||
format!(
|
||||
"{} · unknown commit (built without a git checkout) · {}",
|
||||
self.release(),
|
||||
@@ -51,6 +55,11 @@ impl BuildId {
|
||||
self.date,
|
||||
self.profile
|
||||
)
|
||||
};
|
||||
if self.extended {
|
||||
stamp + " · extended"
|
||||
} else {
|
||||
stamp
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,6 +91,7 @@ mod tests {
|
||||
short,
|
||||
date: "2026-09-27",
|
||||
profile: "release",
|
||||
extended: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,4 +137,26 @@ mod tests {
|
||||
"0.2.0 · unknown commit (built without a git checkout) · release"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_extended_client_says_so_and_stock_reads_as_before() {
|
||||
let stock = build("0.2.0", "v0.2.0-12-gb17be27", "b17be27");
|
||||
assert_eq!(stock.summary(), "0.2.0+12 · b17be27 · 2026-09-27 · release");
|
||||
assert_eq!(
|
||||
BuildId {
|
||||
extended: true,
|
||||
..stock
|
||||
}
|
||||
.summary(),
|
||||
"0.2.0+12 · b17be27 · 2026-09-27 · release · extended"
|
||||
);
|
||||
assert_eq!(
|
||||
BuildId {
|
||||
extended: true,
|
||||
..build("0.2.0", "", "")
|
||||
}
|
||||
.summary(),
|
||||
"0.2.0 · unknown commit (built without a git checkout) · release · extended"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,5 +12,6 @@ fn main() -> benilla_world::AppExit {
|
||||
short: env!("BENILLA_GIT_SHORT"),
|
||||
date: env!("BENILLA_GIT_DATE"),
|
||||
profile: env!("BENILLA_PROFILE"),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,5 +11,6 @@ fn main() -> benilla_app::AppExit {
|
||||
short: env!("BENILLA_GIT_SHORT"),
|
||||
date: env!("BENILLA_GIT_DATE"),
|
||||
profile: env!("BENILLA_PROFILE"),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,9 +23,10 @@ taken.
|
||||
|
||||
## What does not
|
||||
|
||||
- Features 1.12.1 does not have, and behaviour changed because it seems better. A deviation
|
||||
from the reference is the maintainer's call and is recorded where it lives; a pull request is
|
||||
not the place to propose one.
|
||||
- Features 1.12.1 does not have, and behaviour changed because it seems better. Such a feature
|
||||
lives in its own crate on top of benilla, started through `benilla_app::run_with` as in
|
||||
`crates/benilla-app/examples/extended_launcher.rs`. A deviation from the reference is the
|
||||
maintainer's call and is recorded where it lives; a pull request is not the place to propose one.
|
||||
- Anything from a WoW install: art, models, sounds, maps, data. The one exception is interface
|
||||
code (FrameXML and GlueXML), and only through the migration recipe in `docs/METHOD.md`.
|
||||
- Big or mixed changes. One change per pull request, small enough to read in one sitting.
|
||||
|
||||
Reference in New Issue
Block a user