diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index c7b489c..f595c8e 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -50,3 +50,50 @@ jobs: - name: Run unit tests run: cargo test --locked --lib -j 4 + + production-build: + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Check out source + uses: actions/checkout@v4 + + - name: Install Rust toolchain + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \ + sh -s -- -y --profile minimal + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + "$HOME/.cargo/bin/rustup" toolchain install nightly-2026-09-14 --profile minimal + + - name: Cache production Rust dependencies and build artifacts + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + prefix-key: v2-rust + shared-key: linux-nightly-production + cache-bin: false + + - name: Install Linux dependencies + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y \ + clang \ + libasound2-dev \ + libudev-dev \ + libusb-1.0-0-dev \ + libwayland-dev \ + libx11-dev \ + libxkbcommon-dev \ + libxkbcommon-x11-0 \ + mesa-vulkan-drivers \ + mold \ + pkg-config + + - name: Preprocess assets + run: cargo run --locked --bin process-assets --no-default-features --features debug_features -j 4 + + - name: Build production binary + run: cargo build --locked --profile production --no-default-features --features debug_features -j 4 + + - name: Verify production binary is not Bevy/Rust dynamically linked + run: | + ! readelf -d target/production/shinespark | grep -E 'libbevy_dylib|libstd-.*\.so' diff --git a/Cargo.toml b/Cargo.toml index 5dc950d..e77c261 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,8 +6,13 @@ authors = [""] [workspace] +[features] +default = ["development", "debug_features"] +development = ["bevy/dynamic_linking", "bevy/file_watcher"] +debug_features = ["bevy/free_camera"] + [dependencies] -bevy = { version = "0.19", features = ["asset_processor", "basis-universal", "dynamic_linking", "free_camera", "file_watcher", "bevy_settings"] } +bevy = { version = "0.19", features = ["asset_processor", "basis-universal", "bevy_settings"] } # Loads the scenes you author in the editor (assets/*.bsn) and builds # physics colliders from them. An ordinary dependency; remove it if you # stop using authored scenes. @@ -37,3 +42,9 @@ fixed_math = "0.4.1" # Enable a large amount of optimization in the dev profile for dependencies. [profile.dev.package."*"] opt-level = 3 + +[profile.production] +inherits = "release" +lto = "thin" +codegen-units = 1 +strip = "symbols" diff --git a/README.md b/README.md index 8deb6b7..8e6436a 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,21 @@ cargo run -- --debug-match --players 2 --local-port 7000 \ cargo run -- --debug-match --players 2 --local-port 7001 \ --net-player 0=127.0.0.1:7000 --net-player 1=local ``` + +## Production Build + +Production builds use the `production` Cargo profile and do not dynamically +link Bevy or Rust. Development builds retain Bevy dynamic linking for faster +iteration. Cargo stores the production profile under `target/production`, +separate from development artifacts under `target/debug`. + +```sh +cargo run --locked \ + --bin process-assets --no-default-features --features debug_features +cargo build --locked \ + --profile production --no-default-features --features debug_features +``` + +The asset preprocessing command must be run from the repository root. It +populates `imported_assets/Default`, which is then consumed by the production +game without running the asset processor at startup. diff --git a/src/bin/process-assets.rs b/src/bin/process-assets.rs new file mode 100644 index 0000000..6478298 --- /dev/null +++ b/src/bin/process-assets.rs @@ -0,0 +1,31 @@ +use bevy::{ + app::ScheduleRunnerPlugin, + asset::{AssetMode, AssetPlugin, processor::AssetProcessor, processor::ProcessorState}, + prelude::*, + tasks::block_on, +}; +use shinespark::AssetProcessingPlugin; +use std::{process::ExitCode, time::Duration}; + +fn main() -> ExitCode { + let mut app = App::new(); + app.add_plugins(MinimalPlugins.set(ScheduleRunnerPlugin::run_loop(Duration::from_millis(50)))) + .add_plugins(AssetPlugin { + mode: AssetMode::Processed, + use_asset_processor_override: Some(true), + ..default() + }) + .add_plugins(AssetProcessingPlugin) + .add_systems(Update, finish_when_processed); + + app.run(); + ExitCode::SUCCESS +} + +fn finish_when_processed(processor: Option>, mut exit: MessageWriter) { + if processor + .is_some_and(|processor| block_on(processor.get_state()) == ProcessorState::Finished) + { + exit.write(AppExit::Success); + } +} diff --git a/src/fighter/baked_animation/asset_baking.rs b/src/fighter/baked_animation/asset_baking.rs index d78a7ea..1e8cbd4 100644 --- a/src/fighter/baked_animation/asset_baking.rs +++ b/src/fighter/baked_animation/asset_baking.rs @@ -151,7 +151,10 @@ type FighterBakeProcessor = LoadTransformAndSave< >; pub struct BakedAnimationPlugin; -impl Plugin for BakedAnimationPlugin { + +pub struct BakedAnimationAssetProcessingPlugin; + +impl Plugin for BakedAnimationAssetProcessingPlugin { fn build(&self, app: &mut App) { app.init_asset::() .register_asset_loader(FighterBakeSourceLoader) @@ -160,7 +163,13 @@ impl Plugin for BakedAnimationPlugin { FighterBakeTransformer, BakedFighterAnimationsSaver, )) - .set_default_asset_processor::("fighterbake") + .set_default_asset_processor::("fighterbake"); + } +} + +impl Plugin for BakedAnimationPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(BakedAnimationAssetProcessingPlugin) .add_systems( bevy_ggrs::prelude::GgrsSchedule, super::runtime::update_fighter_bone_matrices diff --git a/src/fighter/baked_animation/mod.rs b/src/fighter/baked_animation/mod.rs index 1f2551a..4159352 100644 --- a/src/fighter/baked_animation/mod.rs +++ b/src/fighter/baked_animation/mod.rs @@ -4,7 +4,7 @@ mod asset_baking; mod runtime; mod visual_binding; -pub use asset_baking::BakedAnimationPlugin; +pub use asset_baking::{BakedAnimationAssetProcessingPlugin, BakedAnimationPlugin}; pub use crate::math::fixed_transform::FixedMat4; pub use runtime::{ BakedAnimationClip, BakedFighterAnimations, FighterBoneIndexMap, FighterBoneMatrices, diff --git a/src/lib.rs b/src/lib.rs index 93d25cb..9c8c19e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -155,11 +155,26 @@ impl Plugin for GamePlugin { } } +/// Registers only the custom asset processors used by CI and release builds. +/// The processing binary adds this plugin without starting the game. +pub struct AssetProcessingPlugin; + +impl Plugin for AssetProcessingPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(( + scripting::importer::FighterScriptImportPlugin, + fighter::baked_animation::BakedAnimationAssetProcessingPlugin, + )); + } +} + fn wait_for_asset_processor( processor: Option>, mut next_state: ResMut>, ) { - if processor.is_none_or(|processor| block_on(processor.get_state()) == ProcessorState::Finished) + if !cfg!(feature = "development") + || processor + .is_none_or(|processor| block_on(processor.get_state()) == ProcessorState::Finished) { next_state.set(AppState::LoadCommonAssets); } diff --git a/src/main.rs b/src/main.rs index efcec49..100c9d8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,11 @@ use bevy::asset::AssetMode::Processed; +#[cfg(feature = "debug_features")] use bevy::camera_controller::free_camera::FreeCameraPlugin; use bevy::image::{ImageAddressMode, ImagePlugin, ImageSamplerDescriptor}; use bevy::prelude::*; +#[cfg(feature = "debug_features")] use bevy_egui::EguiPlugin; +#[cfg(feature = "debug_features")] use bevy_inspector_egui::quick::WorldInspectorPlugin; fn main() -> AppExit { @@ -20,16 +23,22 @@ fn main() -> AppExit { }) .set(AssetPlugin { mode: Processed, + use_asset_processor_override: Some(cfg!(feature = "development")), ..default() }); - App::new() - .add_plugins(default_plugins) + let mut app = App::new(); + app.add_plugins(default_plugins) .add_plugins(avian3d::prelude::PhysicsPlugins::default()) .add_plugins(shinespark::GamePlugin) - .add_plugins(FreeCameraPlugin) - .add_plugins(EguiPlugin::default()) - .add_plugins(WorldInspectorPlugin::default()) // adds default options and `InspectorEguiImpl`s - .insert_resource(Time::::from_hz(60.0)) - .run() + .insert_resource(Time::::from_hz(60.0)); + + #[cfg(feature = "debug_features")] + app.add_plugins(( + FreeCameraPlugin, + EguiPlugin::default(), + WorldInspectorPlugin::default(), + )); + + app.run() }