Repository: gfx-rs/wgpu (language: rust)

Issue:
CI/xtask for Validating State of Dependency Tree
`cargo tree` is a very useful tool for evaluating the dependency tree on a given platform, package, and feature set.

We should build a set of queries that tests for/against the presence of dependencies on certain platforms in certain situations. There's no obvious programmatic output, but a regex done in an xtask would be plenty.

Discovered as part of the absolute chaos that is #6980 and to validate #6949. Possible queries:

- `cargo tree -p wgpu --target wasm32-unknown-unknown -e normal` does not contain `wgpu-core`
- `cargo tree -p wgpu --target wasm32-unknown-unknown --features wgsl,spirv -e normal` does not contain `wgpu-core`
- `cargo tree -p wgpu --target x86_64-pc-windows-msvc -e normal` does not contain `web-sys`
etc.




Discussion / hints:
Instead of parsing `cargo tree`, how about using `cargo deny`? You would need a separate config file for each "query", but it would do the job.
It seems a bit heavy handed imo - all I'm thinking of doing for "parsing" here is substring searches or _maybe_ a light regex, plus it's nice it's included in the install of cargo. 

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9a0735ad61..c932afba13 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -780,3 +780,22 @@ 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/xtask/src/check_feature_dependencies.rs b/xtask/src/check_feature_dependencies.rs
new file mode 100644
index 0000000000..6601bc63b9
--- /dev/null
+++ b/xtask/src/check_feature_dependencies.rs
@@ -0,0 +1,114 @@
+use pico_args::Arguments;
+use xshell::Shell;
+
+#[derive(Debug)]
+enum Search<'a> {
+    #[expect(dead_code)]
+    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",
+    "replay",
+    "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")],
+        },
+    ];
+
+    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 search_term in requirement.search_terms {
+            let found = match search_term {
+                Search::Positive(search_term) => output.contains(search_term),
+                Search::Negative(search_term) => !output.contains(search_term),
+            };
+
+            if found {
+                log::info!("✅ Passed!");
+            } else {
+                log::info!("❌ Failed");
+                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 f173fe9690..8cfc12f70d 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -3,6 +3,7 @@ use std::process::ExitCode;
 use anyhow::Context;
 use pico_args::Arguments;
 
+mod check_feature_dependencies;
 mod run_wasm;
 mod test;
 mod util;
@@ -12,6 +13,9 @@ const HELP: &str = "\
 Usage: xtask <COMMAND>
 
 Commands:
+  check-feature-dependencies
+    Check certain dependency invariants are upheld.
+
   run-wasm
     Build and run web examples
 
@@ -71,6 +75,9 @@ fn main() -> anyhow::Result<ExitCode> {
     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)?,
diff --git a/xtask/src/run_wasm.rs b/xtask/src/run_wasm.rs
index d71d88722f..d8048b3ca8 100644
--- a/xtask/src/run_wasm.rs
+++ b/xtask/src/run_wasm.rs
@@ -6,28 +6,22 @@ use xshell::Shell;
 use crate::util::{check_all_programs, Program};
 
 pub(crate) fn run_wasm(shell: Shell, mut args: Arguments) -> anyhow::Result<()> {
-    let no_serve = args.contains("--no-serve");
+    let should_serve = !args.contains("--no-serve");
     let release = args.contains("--release");
 
-    let programs_needed: &[_] = if no_serve {
-        &[Program {
-            crate_name: "wasm-bindgen-cli",
-            binary_name: "wasm-bindgen",
-        }]
-    } else {
-        &[
-            Program {
-                crate_name: "wasm-bindgen-cli",
-                binary_name: "wasm-bindgen",
-            },
-            Program {
-                crate_name: "simple-http-server",
-                binary_name: "simple-http-server",
-            },
-        ]
-    };
-
-    check_all_programs(programs_needed)?;
+    let mut programs_needed = vec![Program {
+        crate_name: "wasm-bindgen-cli",
+        binary_name: "wasm-bindgen",
+    }];
+
+    if should_serve {
+        programs_needed.push(Program {
+            crate_name: "simple-http-server",
+            binary_name: "simple-http-server",
+        });
+    }
+
+    check_all_programs(&programs_needed)?;
 
     let release_flag: &[_] = if release { &["--release"] } else { &[] };
     let output_dir = if release { "release" } else { "debug" };
@@ -91,7 +85,7 @@ pub(crate) fn run_wasm(shell: Shell, mut args: Arguments) -> anyhow::Result<()>
             .with_context(|| format!("Failed to copy static file \"{}\"", file.display()))?;
     }
 
-    if !no_serve {
+    if should_serve {
         log::info!("serving on port 8000");
 
         // Explicitly specify the IP address to 127.0.0.1 since otherwise simple-http-server will


Test patch: