diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2becb7cf0..ed8997f8d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -809,22 +809,3 @@ jobs: command: check bans licenses sources arguments: --all-features --workspace rust-version: ${{ env.REPO_MSRV }} - - check-feature-dependencies: - # runtime is normally 1 minute - timeout-minutes: 5 - - name: "Feature Dependencies" - runs-on: ubuntu-latest - steps: - - name: Checkout repo - uses: actions/checkout@v4 - - - name: Install repo MSRV toolchain - run: | - rustup toolchain install ${{ env.REPO_MSRV }} --no-self-update --profile=minimal - rustup override set ${{ env.REPO_MSRV }} - cargo -V - - - name: Run `cargo feature-dependencies` - run: cargo xtask check-feature-dependencies diff --git a/Cargo.lock b/Cargo.lock index e440ef336..d2236332c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -546,6 +546,15 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + [[package]] name = "capacity_builder" version = "0.1.3" @@ -575,6 +584,29 @@ dependencies = [ "syn", ] +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8769706aad5d996120af43197bf46ef6ad0fda35216b4505f926a365a232d924" +dependencies = [ + "camino", + "cargo-platform", + "semver 1.0.25", + "serde", + "serde_json", + "thiserror 2.0.11", +] + [[package]] name = "cast" version = "0.3.0" @@ -3329,7 +3361,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" dependencies = [ - "semver", + "semver 0.9.0", ] [[package]] @@ -3413,6 +3445,15 @@ dependencies = [ "semver-parser", ] +[[package]] +name = "semver" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" +dependencies = [ + "serde", +] + [[package]] name = "semver-parser" version = "0.7.0" @@ -4738,6 +4779,7 @@ dependencies = [ "arrayvec", "bitflags 2.8.0", "bytemuck", + "cargo_metadata", "cfg-if", "console_log", "ctor", diff --git a/Cargo.toml b/Cargo.toml index 760b9c85b..c7bf43939 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,6 +93,7 @@ bytemuck = { version = "1.21", features = [ "extern_crate_alloc", "min_const_generics", ] } +cargo_metadata = "0.19" cfg_aliases = "0.2.1" cfg-if = "1" criterion = "0.5" diff --git a/docs/testing.md b/docs/testing.md index e381b4334..1ddf86a56 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -37,6 +37,7 @@ This is a table of contents, in the form of the repository's directory structure - [tests](#player-tests) - tests - [compile-tests](#wgpu-compile-tests) + - [dependency-tests](#wgpu-dependency-tests) - [gpu-tests](#wgpu-gpu-tests) - [validation-tests](#wgpu-validation-tests) @@ -169,6 +170,18 @@ the `wgpu` crate is expected to fail to compile. This mainly revolves around ensuring lifetimes are properly handled when dropping passes, etc. +## `wgpu` Dependency Tests + +- Located in: `tests/dependency-tests` +- Run with `cargo nextest run --test wgpu-dependency-test` +- Tests against `cargo tree`. + +These tests ensure that the `wgpu` crate has the correct dependency +tree on all platforms. It's super easy to subtly mess up the dependencies +which can cause issues or extra dependencies to be pulled in. + +This provides a way to ensure that our `toml` files are correct. + ## `wgpu` GPU Tests - Located in: `tests/gpu-tests` diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 0c8a99b06..abdab2b85 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -17,6 +17,11 @@ name = "wgpu-compile-test" path = "compile-tests/root.rs" harness = true +[[test]] +name = "wgpu-dependency-test" +path = "dependency-tests/root.rs" +harness = true + [[test]] name = "wgpu-gpu-test" path = "gpu-tests/root.rs" @@ -39,6 +44,7 @@ arrayvec.workspace = true approx.workspace = true bitflags.workspace = true bytemuck.workspace = true +cargo_metadata.workspace = true cfg-if.workspace = true ctor.workspace = true futures-lite.workspace = true diff --git a/tests/dependency-tests/root.rs b/tests/dependency-tests/root.rs new file mode 100644 index 000000000..3b06ca5ed --- /dev/null +++ b/tests/dependency-tests/root.rs @@ -0,0 +1,290 @@ +use std::process::Command; + +#[derive(Debug)] +enum Search<'a> { + Positive(&'a str), + Negative(&'a str), +} + +#[derive(Debug)] +struct Requirement<'a> { + human_readable_name: &'a str, + target: &'a str, + packages: &'a [&'a str], + features: &'a [&'a str], + default_features: bool, + search_terms: &'a [Search<'a>], +} + +fn check_feature_dependency(requirement: Requirement) { + println!("Checking: {}", requirement.human_readable_name); + + let mut args = Vec::new(); + args.extend(["tree", "--target", requirement.target]); + + for package in requirement.packages { + args.push("--package"); + args.push(package); + } + + if !requirement.default_features { + args.push("--no-default-features"); + } + + let features = requirement.features.join(","); + if !requirement.features.is_empty() { + args.push("--features"); + args.push(&features); + } + + println!("$ cargo {}", args.join(" ")); + + let output = Command::new("cargo") + .args(&args) + .output() + .expect("Failed to run cargo tree") + .stdout; + let output = String::from_utf8(output).expect("Output is not valid UTF-8"); + + let mut any_failed = false; + println!("{output}"); + + for (i, search_term) in requirement.search_terms.iter().enumerate() { + // Add a space and after to make sure we're getting a full match + let found = match search_term { + Search::Positive(search_term) => output.contains(&format!(" {search_term} ")), + Search::Negative(search_term) => !output.contains(&format!(" {search_term} ")), + }; + + if found { + println!( + "✅ Passed! ({} of {})", + i + 1, + requirement.search_terms.len() + ); + } else { + println!( + "❌ Failed! ({} of {})", + i + 1, + requirement.search_terms.len() + ); + any_failed = true; + } + } + + assert!(!any_failed); +} + +fn get_all_wgpu_features() -> Vec { + let metadata = cargo_metadata::MetadataCommand::new() + .no_deps() + .exec() + .unwrap(); + + metadata + .packages + .iter() + .find(|p| p.name == "wgpu") + .unwrap() + .features + .keys() + .cloned() + .collect() +} + +#[test] +fn wasm32_without_webgl_or_noop_does_not_depend_on_wgpu_core() { + let all_features = get_all_wgpu_features(); + + let removed_features = ["webgl", "noop", "wgpu-core"]; + + let features_no_webgl: Vec<&str> = all_features + .iter() + .map(String::as_str) + .filter(|&feature| !removed_features.contains(&feature)) + .collect(); + + check_feature_dependency(Requirement { + human_readable_name: + "wasm32 without `webgl` or `noop` feature does not depend on `wgpu-core`", + target: "wasm32-unknown-unknown", + packages: &["wgpu"], + features: &features_no_webgl, + default_features: false, + search_terms: &[Search::Negative("wgpu-core")], + }); +} + +#[test] +fn wasm32_with_webgpu_and_wgsl_does_not_depend_on_naga() { + check_feature_dependency(Requirement { + human_readable_name: "wasm32 with `webgpu` and `wgsl` feature does not depend on `naga`", + target: "wasm32-unknown-unknown", + packages: &["wgpu"], + features: &["webgpu", "wgsl"], + default_features: false, + search_terms: &[Search::Negative("naga")], + }); +} + +#[test] +fn wasm32_with_webgl_depends_on_glow() { + check_feature_dependency(Requirement { + human_readable_name: "wasm32 with `webgl` feature depends on `glow`", + target: "wasm32-unknown-unknown", + packages: &["wgpu"], + features: &["webgl"], + default_features: false, + search_terms: &[Search::Positive("glow")], + }); +} + +#[test] +fn windows_with_webgl_does_not_depend_on_glow() { + check_feature_dependency(Requirement { + human_readable_name: "windows with `webgl` does not depend on `glow`", + target: "x86_64-pc-windows-msvc", + packages: &["wgpu"], + features: &["webgl"], + default_features: false, + search_terms: &[Search::Negative("glow")], + }); +} + +#[test] +fn apple_with_vulkan_does_not_depend_on_ash() { + check_feature_dependency(Requirement { + human_readable_name: "apple with `vulkan` feature does not depend on `ash`", + target: "aarch64-apple-darwin", + packages: &["wgpu"], + features: &["vulkan"], + default_features: false, + search_terms: &[Search::Negative("ash")], + }); +} + +#[test] +fn apple_with_vulkan_portability_depends_on_ash_and_renderdoc_sys() { + check_feature_dependency(Requirement { + human_readable_name: + "apple with `vulkan-portability` feature depends on `ash` and `renderdoc-sys`", + target: "aarch64-apple-darwin", + packages: &["wgpu"], + features: &["vulkan-portability"], + default_features: false, + search_terms: &[Search::Positive("ash"), Search::Positive("renderdoc-sys")], + }); +} + +#[test] +fn apple_with_gles_does_not_depend_on_glow() { + check_feature_dependency(Requirement { + human_readable_name: "apple with 'gles' feature does not depend on 'glow'", + target: "aarch64-apple-darwin", + packages: &["wgpu"], + features: &["gles"], + default_features: false, + search_terms: &[Search::Negative("glow")], + }); +} + +#[test] +fn apple_with_angle_depends_on_glow_and_renderdoc_sys() { + check_feature_dependency(Requirement { + human_readable_name: "apple with 'angle' feature depends on 'glow' and `renderdoc-sys`", + target: "aarch64-apple-darwin", + packages: &["wgpu"], + features: &["angle"], + default_features: false, + search_terms: &[Search::Positive("glow"), Search::Positive("renderdoc-sys")], + }); +} + +#[test] +fn apple_with_no_features_does_not_depend_on_renderdoc_sys() { + check_feature_dependency(Requirement { + human_readable_name: "apple with no features does not depend on 'renderdoc-sys'", + target: "aarch64-apple-darwin", + packages: &["wgpu"], + features: &[], + default_features: false, + search_terms: &[Search::Negative("renderdoc-sys")], + }); +} + +#[test] +fn windows_with_no_features_does_not_depend_on_glow_windows_or_ash() { + check_feature_dependency(Requirement { + human_readable_name: + "windows with no features does not depend on 'glow', `windows`, or `ash`", + target: "x86_64-pc-windows-msvc", + packages: &["wgpu"], + features: &[], + default_features: false, + search_terms: &[ + Search::Negative("glow"), + Search::Negative("windows"), + Search::Negative("ash"), + ], + }); +} + +#[test] +fn windows_with_no_features_depends_on_renderdoc_sys() { + check_feature_dependency(Requirement { + human_readable_name: "windows with no features depends on renderdoc-sys", + target: "x86_64-pc-windows-msvc", + packages: &["wgpu"], + features: &[], + default_features: false, + search_terms: &[Search::Positive("renderdoc-sys")], + }); +} + +#[test] +fn emscripten_with_webgl_does_not_depend_on_glow() { + check_feature_dependency(Requirement { + human_readable_name: "emscripten with webgl feature does not depend on glow", + target: "wasm32-unknown-emscripten", + packages: &["wgpu"], + features: &["webgl"], + default_features: false, + search_terms: &[Search::Negative("glow")], + }); +} + +#[test] +fn emscripten_with_gles_depends_on_glow() { + check_feature_dependency(Requirement { + human_readable_name: "emscripten with gles feature depends on glow", + target: "wasm32-unknown-emscripten", + packages: &["wgpu"], + features: &["gles"], + default_features: false, + search_terms: &[Search::Positive("glow")], + }); +} + +#[test] +fn x86_64_does_not_depend_on_portable_atomic() { + check_feature_dependency(Requirement { + human_readable_name: "x86-64 does not depend on portable-atomic", + target: "x86_64-unknown-linux-gnu", + packages: &["wgpu"], + features: &[], + default_features: false, + search_terms: &[Search::Negative("portable-atomic")], + }); +} + +#[test] +fn ppc32_does_depend_on_portable_atomic() { + check_feature_dependency(Requirement { + human_readable_name: "ppc32 does depend on portable-atomic", + target: "powerpc-unknown-linux-gnu", + packages: &["wgpu"], + features: &[], + default_features: false, + search_terms: &[Search::Positive("portable-atomic")], + }); +} diff --git a/xtask/src/check_feature_dependencies.rs b/xtask/src/check_feature_dependencies.rs deleted file mode 100644 index 70451df02..000000000 --- a/xtask/src/check_feature_dependencies.rs +++ /dev/null @@ -1,231 +0,0 @@ -use pico_args::Arguments; -use xshell::Shell; - -#[derive(Debug)] -enum Search<'a> { - Positive(&'a str), - Negative(&'a str), -} - -#[derive(Debug)] -struct Requirement<'a> { - human_readable_name: &'a str, - target: &'a str, - packages: &'a [&'a str], - features: &'a [&'a str], - default_features: bool, - search_terms: &'a [Search<'a>], -} - -const ALL_WGPU_FEATURES: &[&str] = &[ - "dx12", - "metal", - "webgpu", - "angle", - "vulkan-portability", - "webgl", - "spirv", - "glsl", - "wgsl", - "naga-ir", - "serde", - "counters", - "fragile-send-sync-non-atomic-wasm", - "static-dxc", -]; - -pub fn check_feature_dependencies(shell: Shell, arguments: Arguments) -> anyhow::Result<()> { - let mut _args = arguments.finish(); - - let features_no_webgl: Vec<&str> = ALL_WGPU_FEATURES - .iter() - .copied() - .filter(|feature| *feature != "webgl") - .collect(); - - let requirements = [ - Requirement { - human_readable_name: "wasm32 without `webgl` feature does not depend on `wgpu-core`", - target: "wasm32-unknown-unknown", - packages: &["wgpu"], - features: &features_no_webgl, - default_features: false, - search_terms: &[Search::Negative("wgpu-core")], - }, - Requirement { - human_readable_name: - "wasm32 with `webgpu` and `wgsl` feature does not depend on `naga`", - target: "wasm32-unknown-unknown", - packages: &["wgpu"], - features: &["webgpu", "wgsl"], - default_features: false, - search_terms: &[Search::Negative("naga")], - }, - Requirement { - human_readable_name: "wasm32 with `webgl` feature depends on `glow`", - target: "wasm32-unknown-unknown", - packages: &["wgpu"], - features: &["webgl"], - default_features: false, - search_terms: &[Search::Positive("glow")], - }, - Requirement { - human_readable_name: "windows with `webgl` does not depend on `glow`", - target: "x86_64-pc-windows-msvc", - packages: &["wgpu"], - features: &["webgl"], - default_features: false, - search_terms: &[Search::Negative("glow")], - }, - Requirement { - human_readable_name: "apple with `vulkan` feature does not depend on `ash`", - target: "aarch64-apple-darwin", - packages: &["wgpu"], - features: &["vulkan"], - default_features: false, - search_terms: &[Search::Negative("ash")], - }, - Requirement { - human_readable_name: - "apple with `vulkan-portability` feature depends on `ash` and `renderdoc-sys`", - target: "aarch64-apple-darwin", - packages: &["wgpu"], - features: &["vulkan-portability"], - default_features: false, - search_terms: &[Search::Positive("ash"), Search::Positive("renderdoc-sys")], - }, - Requirement { - human_readable_name: "apple with 'gles' feature does not depend on 'glow'", - target: "aarch64-apple-darwin", - packages: &["wgpu"], - features: &["gles"], - default_features: false, - search_terms: &[Search::Negative("glow")], - }, - Requirement { - human_readable_name: "apple with 'angle' feature depends on 'glow' and `renderdoc-sys`", - target: "aarch64-apple-darwin", - packages: &["wgpu"], - features: &["angle"], - default_features: false, - search_terms: &[Search::Positive("glow"), Search::Positive("renderdoc-sys")], - }, - Requirement { - human_readable_name: "apple with no features does not depend on 'renderdoc-sys'", - target: "aarch64-apple-darwin", - packages: &["wgpu"], - features: &[], - default_features: false, - search_terms: &[Search::Negative("renderdoc-sys")], - }, - Requirement { - human_readable_name: - "windows with no features does not depend on 'glow', `windows`, or `ash`", - target: "x86_64-pc-windows-msvc", - packages: &["wgpu"], - features: &[], - default_features: false, - search_terms: &[ - Search::Negative("glow"), - Search::Negative("windows"), - Search::Negative("ash"), - ], - }, - Requirement { - human_readable_name: "windows with no features depends on renderdoc-sys", - target: "x86_64-pc-windows-msvc", - packages: &["wgpu"], - features: &[], - default_features: false, - search_terms: &[Search::Positive("renderdoc-sys")], - }, - Requirement { - human_readable_name: "emscripten with webgl feature does not depend on glow", - target: "wasm32-unknown-emscripten", - packages: &["wgpu"], - features: &["webgl"], - default_features: false, - search_terms: &[Search::Negative("glow")], - }, - Requirement { - human_readable_name: "emscripten with gles feature depends on glow", - target: "wasm32-unknown-emscripten", - packages: &["wgpu"], - features: &["gles"], - default_features: false, - search_terms: &[Search::Positive("glow")], - }, - Requirement { - human_readable_name: "x86-64 does not depend on portable-atomic", - target: "x86_64-unknown-linux-gnu", - packages: &["wgpu"], - features: &[], - default_features: false, - search_terms: &[Search::Negative("portable-atomic")], - }, - Requirement { - human_readable_name: "ppc32 does depend on portable-atomic", - target: "powerpc-unknown-linux-gnu", - packages: &["wgpu"], - features: &[], - default_features: false, - search_terms: &[Search::Positive("portable-atomic")], - }, - ]; - - let mut any_failures = false; - for requirement in requirements { - let mut cmd = shell - .cmd("cargo") - .args(["tree", "--target", requirement.target]); - - for package in requirement.packages { - cmd = cmd.arg("--package").arg(package); - } - - if !requirement.default_features { - cmd = cmd.arg("--no-default-features"); - } - - if !requirement.features.is_empty() { - cmd = cmd.arg("--features").arg(requirement.features.join(",")); - } - - log::info!("Checking Requirement: {}", requirement.human_readable_name); - log::debug!("{:#?}", requirement); - log::debug!("$ {cmd}"); - - let output = cmd.read()?; - - log::debug!("{output}"); - - for (i, search_term) in requirement.search_terms.into_iter().enumerate() { - // Add a space and after to make sure we're getting a full match - let found = match search_term { - Search::Positive(search_term) => output.contains(&format!(" {search_term} ")), - Search::Negative(search_term) => !output.contains(&format!(" {search_term} ")), - }; - - if found { - log::info!( - "✅ Passed! ({} of {})", - i + 1, - requirement.search_terms.len() - ); - } else { - log::info!( - "❌ Failed! ({} of {})", - i + 1, - requirement.search_terms.len() - ); - any_failures = true; - } - } - } - - if any_failures { - anyhow::bail!("Some feature dependencies are not met"); - } - - Ok(()) -} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 9fb5e08c1..a87c68ebd 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -3,7 +3,6 @@ use std::process::ExitCode; use anyhow::Context; use pico_args::Arguments; -mod check_feature_dependencies; mod run_wasm; mod test; mod util; @@ -13,9 +12,6 @@ const HELP: &str = "\ Usage: xtask Commands: - check-feature-dependencies - Check certain dependency invariants are upheld. - run-wasm Build and run web examples @@ -77,9 +73,6 @@ fn main() -> anyhow::Result { shell.change_dir(String::from(env!("CARGO_MANIFEST_DIR")) + "/.."); match subcommand.as_deref() { - Some("check-feature-dependencies") => { - check_feature_dependencies::check_feature_dependencies(shell, args)? - } Some("run-wasm") => run_wasm::run_wasm(shell, args)?, Some("test") => test::run_tests(shell, args)?, Some("vendor-web-sys") => vendor_web_sys::run_vendor_web_sys(shell, args)?,