Skip to content

Commit

Permalink
wasm: Separate dev and release builds.
Browse files Browse the repository at this point in the history
* Pass explicit output directory to wasm-pack (which are in the
  target directory, for tidiness).
* Pass profile to wasm-pack.
* Select client files to read/embed in the server based on profile.
* Create a CI job for building the web version as release.

There is a gap in coverage here, in that xtask has no way to say
"build the release client to run the release server", but it has no
way to run the release server anyway, so that's okay, and eventually
I want to trigger it automatically from the build script.

Part of <#379>.
  • Loading branch information
kpreid committed Nov 8, 2024
1 parent 5872bc9 commit 622285e
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 61 deletions.
46 changes: 39 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -191,18 +191,50 @@ jobs:
path: |
target/cargo-timings/cargo-timing-*.html
- run: df -h .

# Build web version with release profile.
build-web:
# Only do this if the tests passed
needs: build
runs-on: ubuntu-latest

# Same condition as "deploy-web" job, because the result will only be used then.
if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/pages-alt') }}

steps:

- uses: actions/checkout@v4

- uses: Swatinem/[email protected]
with:
# Reuse the cache from the normal `build` job instead of creating an independent one,
# to reduce redundant work and save disk space — but don't *write* to that cache, so
# we don't bloat it or conflict.
#
# Since this is a release build, it won’t be able to reuse most of the build artifacts,
# but it will be able to reuse xtask and possibly some build dependencies, so it will
# still be faster than a clean build.
prefix-key: "v1-rust-ubuntu-stable-locked"
shared-key: "build"
save-if: false
workspaces: |
./
all-is-cubes-wasm/
- run: cargo build --package xtask

- run: cargo xtask build-web-release

# Save wasm build so that we can optionally deploy it without rebuilding
# (but only for the stablest matrix version)
- name: Save wasm dist artifact
if: ${{ matrix.primary }}
uses: actions/upload-pages-artifact@v3
- uses: actions/upload-pages-artifact@v3
with:
path: all-is-cubes-wasm/dist

- run: df -h .

deploy:
needs: build
# Deploy web build to GitHub Pages.
deploy-web:
needs: build-web
runs-on: ubuntu-latest
permissions:
pages: write
Expand Down
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.

18 changes: 18 additions & 0 deletions all-is-cubes-server/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#![allow(missing_docs)]

fn main() {
// Make the build profile visible so that we can
// embed/read a client built with the same profile.
// We use cfg rather than env because `include_dir!()` forces us to do that anyway.
// TODO: Replace `include_dir!()` with a build script.
// Also, replace requiring the build to already exist with having this build script do it.
println!(
"cargo::rustc-env=AIC_CLIENT_BUILD_DIR={manifest}/../all-is-cubes-wasm/target/web-app-{profile_dir_name}",
manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap(),
profile_dir_name = match &*std::env::var("PROFILE").unwrap() {
"debug" => "dev",
"release" => "release",
other => panic!("unexpected PROFILE={other}")
},
);
}
12 changes: 5 additions & 7 deletions all-is-cubes-server/src/client_static.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,9 @@ impl AicClientSource {
let static_service = match self {
#[cfg(feature = "embed")]
AicClientSource::Embedded => axum::routing::get(embedded::client),
AicClientSource::Workspace => {
axum::routing::get_service(tower_http::services::ServeDir::new(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../all-is-cubes-wasm/dist/"
)))
}
AicClientSource::Workspace => axum::routing::get_service(
tower_http::services::ServeDir::new(concat!(env!("AIC_CLIENT_BUILD_DIR"), "/")),
),
};

Router::new()
Expand All @@ -43,8 +40,9 @@ mod embedded {
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};

// TODO: need to support optionally embedding the release build rather than the dev build
static CLIENT_STATIC: include_dir::Dir<'static> =
include_dir::include_dir!("$CARGO_MANIFEST_DIR/../all-is-cubes-wasm/dist");
include_dir::include_dir!("$AIC_CLIENT_BUILD_DIR");

/// Handler for client static files
pub(crate) async fn client(path: Option<Path<String>>) -> impl IntoResponse {
Expand Down
1 change: 1 addition & 0 deletions tools/xtask/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ anyhow = { workspace = true }
# wasm-pack, so using it (as long as it still works) saves on build time and space.
cargo_metadata = "0.8.2"
clap = { workspace = true }
strum = { workspace = true }
toml_edit = { version = "0.22.13" }
walkdir = "2.3.2"
xshell = "0.1.17"
Expand Down
150 changes: 103 additions & 47 deletions tools/xtask/src/xtask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ enum XtaskCommand {
server_args: Vec<String>,
},

BuildWebRelease,

/// Update dependency versions.
Update {
#[arg(default_value = "latest")]
Expand Down Expand Up @@ -155,6 +157,28 @@ enum Scope {
OnlyFuzz,
}

#[derive(Clone, Copy, Debug, PartialEq, strum::IntoStaticStr, strum::Display)]
#[strum(serialize_all = "kebab-case")]
enum Profile {
Dev,
Release,
}
impl Profile {
/// Name of the subdirectory in `target/` where Cargo puts builds with this profile.
fn target_subdirectory_name(self) -> &'static Path {
// The dev profile is a special case:
// <https://doc.rust-lang.org/cargo/guide/build-cache.html?highlight=debug%20directory#build-cache>
#[expect(
clippy::match_wildcard_for_single_variants,
reason = "this is the correct general form"
)]
match self {
Profile::Dev => Path::new("debug"),
other => Path::new(other.into()),
}
}
}

fn main() -> Result<(), ActionError> {
let (config, command) = {
let XtaskArgs {
Expand All @@ -178,7 +202,7 @@ fn main() -> Result<(), ActionError> {
XtaskCommand::Init => {
write_development_files(&config)?;
if config.scope.includes_main_workspace() {
update_server_static(&config, &mut time_log)?; // includes installing wasm tools
build_web(&config, &mut time_log, Profile::Dev)?; // includes installing wasm tools
}
}
XtaskCommand::Test { no_run } => {
Expand Down Expand Up @@ -280,7 +304,7 @@ fn main() -> Result<(), ActionError> {
.run()?;
}
XtaskCommand::RunGameServer { server_args } => {
update_server_static(&config, &mut time_log)?;
build_web(&config, &mut time_log, Profile::Dev)?;

cargo()
.arg("run")
Expand All @@ -291,6 +315,9 @@ fn main() -> Result<(), ActionError> {
.args(server_args)
.run()?;
}
XtaskCommand::BuildWebRelease => {
build_web(&config, &mut time_log, Profile::Release)?;
}
XtaskCommand::Update { to, dry_run } => {
let mut options: Vec<&str> = Vec::new();
if dry_run {
Expand Down Expand Up @@ -517,88 +544,117 @@ fn exhaustive_test(
) -> Result<(), ActionError> {
assert!(config.scope.includes_main_workspace());

update_server_static(config, time_log)?;
build_web(config, time_log, Profile::Dev)?;

do_for_all_packages(config, op, Features::AllAndNothing, time_log)?;
Ok(())
}

fn static_web_app_out_dir(profile: Profile) -> PathBuf {
PROJECT_DIR.join(format!("all-is-cubes-wasm/target/web-app-{profile}"))
}

/// Build the WASM and other 'client' files that the web server might need.
/// Needed for build whenever `all-is-cubes-server` is being tested/run with
/// the `embed` feature; needed for run regardless.
fn update_server_static(config: &Config, time_log: &mut Vec<Timing>) -> Result<(), ActionError> {
/// the `embed` feature; needed to run the server regardless.
fn build_web(
config: &Config,
time_log: &mut Vec<Timing>,
profile: Profile,
) -> Result<(), ActionError> {
assert!(config.scope.includes_main_workspace());

let wasm_package_dir: &Path = &PROJECT_DIR.join("all-is-cubes-wasm");

ensure_wasm_tools_installed(config, time_log)?;

// Run the compilation if needed, which ensures that the wasm binary is fresh.
// Note: This must use the same profile as thfe wasm-pack command is! (Both are dev for now)
// We do this explicitly because wasm-pack release builds run `wasm-opt` unconditionally,
// and we want to do modification time checks instead.
{
let _t = CaptureTime::new(time_log, "wasm cargo build");
let _t = CaptureTime::new(time_log, format!("wasm cargo build --{profile}"));
cargo()
.arg("build")
.arg("--manifest-path=all-is-cubes-wasm/Cargo.toml")
.arg("--manifest-path")
.arg(wasm_package_dir.join("Cargo.toml"))
.arg("--profile")
.arg(<&str>::from(profile))
.arg(TARGET_WASM)
.args(config.cargo_build_args())
.run()?;
}

// Run wasm-pack if and only if we need to.
// This is because it unconditionally runs `wasm-opt` which is slow and also means
// the files will be touched unnecessarily.
// Directory we ask wasm-pack to write its output (modified wasm and generated JS) to.
let wasm_pack_out_dir = wasm_package_dir.join(format!("target/wasm-pack-{profile}"));
fs::create_dir_all(&wasm_pack_out_dir)?;
// Directory where we write the files that make up the all-static web version of All is Cubes,
// which are made from wasm_pack_out_dir and our static files.
let static_web_app_out_dir = &static_web_app_out_dir(profile);
fs::create_dir_all(static_web_app_out_dir)?;

// Run wasm-pack if and only if we need to because the `cargo build` output is newer than
// the wasm-pack generated JS.
if newer_than(
["all-is-cubes-wasm/target/wasm32-unknown-unknown/debug/all_is_cubes_wasm.wasm"],
["all-is-cubes-wasm/pkg/all_is_cubes_wasm.js"],
[PathBuf::from_iter([
wasm_package_dir,
Path::new("target/wasm32-unknown-unknown"),
profile.target_subdirectory_name(),
Path::new("all_is_cubes_wasm.wasm"),
])],
[wasm_pack_out_dir.join("all_is_cubes_wasm.js")],
) {
let _t = CaptureTime::new(time_log, "wasm-pack build");
let _pushd: Pushd = pushd("all-is-cubes-wasm")?;

// Run the compilation if needed, which ensures that the wasm binary is fresh.
// Note: This must use the same profile as the wasm-pack command is! (Both are dev for now)
cargo().arg("build").arg(TARGET_WASM).run()?;

// Run wasm-pack if and only if we need to.
// This is because it unconditionally runs `wasm-opt` which is slow and also means
// the files will be touched unnecessarily.
if newer_than(
["target/wasm32-unknown-unknown/debug/all_is_cubes_wasm.wasm"],
["pkg/all_is_cubes_wasm.js"],
) {
cmd!("wasm-pack build --dev --target web").run()?;
}
let _t = CaptureTime::new(time_log, format!("wasm-pack build --{profile}"));
cmd!("wasm-pack build --target web")
.arg("--out-dir")
.arg(
wasm_pack_out_dir
.canonicalize()
.with_context(|| wasm_pack_out_dir.display().to_string())?,
)
.arg(format!("--{profile}"))
.arg(wasm_package_dir)
.run()?;
}

// Combine the static files and build results in the same way that webpack used to
// (This will need replacement if we get subdirectories)
let pkg_path = Path::new("all-is-cubes-wasm/pkg");
let pkg_files: BTreeSet<PathBuf> = {
let wasm_pack_output_files: BTreeSet<PathBuf> = {
let mut set = BTreeSet::from([
// There are lots of other files in pkg which we do not need
PathBuf::from("all_is_cubes_wasm_bg.wasm"),
PathBuf::from("all_is_cubes_wasm.js"),
PathBuf::from("snippets"), // note this gets only the dir but not the contents
]);
let snippets = &pkg_path.join("snippets/");
set.extend(directory_tree_contents(pkg_path, snippets)?);
set.extend(directory_tree_contents(
&wasm_pack_out_dir,
&wasm_pack_out_dir.join("snippets/"),
)?);
set
};
let static_path = &Path::new("all-is-cubes-wasm/static");
let static_files = directory_tree_contents(static_path, static_path)?;
let dest_dir: &'static Path = Path::new("all-is-cubes-wasm/dist/");
let client_dest_dir = dest_dir.join("client/");
fs::create_dir_all(dest_dir)?;
// Path for our non-compiled static files that should be included.
let static_input_files_path = &wasm_package_dir.join("static");
let static_files = directory_tree_contents(static_input_files_path, static_input_files_path)?;
// Path where the wasm and js files are put.
let web_client_dir = static_web_app_out_dir.join("client/");
fs::create_dir_all(static_web_app_out_dir)?;
for src_file in &static_files {
copy_file_with_context(&static_path.join(src_file), &dest_dir.join(src_file))?;
copy_file_with_context(
&static_input_files_path.join(src_file),
&static_web_app_out_dir.join(src_file),
)?;
}
for src_file in &pkg_files {
copy_file_with_context(&pkg_path.join(src_file), &client_dest_dir.join(src_file))?;
for src_file in &wasm_pack_output_files {
copy_file_with_context(
&wasm_pack_out_dir.join(src_file),
&web_client_dir.join(src_file),
)?;
}

// Warn of unexpected files.
// (In the future with more confidence, perhaps we should delete them.)
let extra_dest_files = directory_tree_contents(dest_dir, dest_dir)?
let extra_dest_files = directory_tree_contents(static_web_app_out_dir, static_web_app_out_dir)?
.difference(
&pkg_files
&wasm_pack_output_files
.into_iter()
.map(|p| Path::new("client/").join(p))
.collect(),
Expand All @@ -610,8 +666,8 @@ fn update_server_static(config: &Config, time_log: &mut Vec<Timing>) -> Result<(
.collect::<BTreeSet<_>>();
if !extra_dest_files.is_empty() {
eprintln!(
"Warning: possibly stale files in {dest_dir}: {extra_dest_files:#?}",
dest_dir = dest_dir.display()
"Warning: possibly stale files in {static_web_app_out_dir}: {extra_dest_files:#?}",
static_web_app_out_dir = static_web_app_out_dir.display()
);
}

Expand All @@ -633,8 +689,8 @@ fn do_for_all_packages(
// Note that this is only an “exists” check not a “up-to-date” check, on the assumption
// that running server tests will not depend on the specific file contents.
// TODO: That's a fragile assumption.
if config.scope.includes_main_workspace() && !Path::new("all-is-cubes-wasm/dist/").exists() {
update_server_static(config, time_log)?;
if config.scope.includes_main_workspace() && !static_web_app_out_dir(Profile::Dev).exists() {
build_web(config, time_log, Profile::Dev)?;
}

// Test everything we can with default features and target.
Expand Down

0 comments on commit 622285e

Please sign in to comment.