diff --git a/.github/workflows/gen.yml b/.github/workflows/gen.yml index 49fab2cecb..9acad5fd35 100644 --- a/.github/workflows/gen.yml +++ b/.github/workflows/gen.yml @@ -32,7 +32,7 @@ jobs: strategy: fail-fast: false matrix: - tool: [bindings, package, reactor, webview, yml, license, workspace] + tool: [bindings, package, reactor, webview, yml, license, workspace, features] steps: - name: Checkout uses: actions/checkout@v7 diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml new file mode 100644 index 0000000000..e26b13ce88 --- /dev/null +++ b/.github/workflows/web.yml @@ -0,0 +1,38 @@ +name: web + +on: + push: + branches: [master] + paths: + - "web/**" + - ".github/workflows/web.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Get sources + uses: actions/checkout@v7 + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v5 + with: + path: ./web + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/crates/tools/features/Cargo.toml b/crates/tools/features/Cargo.toml new file mode 100644 index 0000000000..8383068575 --- /dev/null +++ b/crates/tools/features/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "tool_features" +version = "0.0.0" +edition = "2024" +publish = false + +[dependencies] +windows-metadata = { workspace = true } + +[lints] +workspace = true diff --git a/crates/tools/features/src/main.rs b/crates/tools/features/src/main.rs new file mode 100644 index 0000000000..d81b728772 --- /dev/null +++ b/crates/tools/features/src/main.rs @@ -0,0 +1,290 @@ +use std::collections::HashMap; +use std::fmt::Write; +use windows_metadata::reader::{File, Index, Item, TypeCategory}; +use windows_metadata::{Signature, Type}; + +/// The metadata that backs the published `windows` and `windows-sys` crates. +/// Both crates share the same namespace-to-feature taxonomy, so a single index +/// answers "which feature do I enable?" for either crate. +const WINMD: [&str; 3] = [ + "crates/libs/bindgen/default/Windows.winmd", + "crates/libs/bindgen/default/Windows.Win32.winmd", + "crates/libs/bindgen/default/Windows.Wdk.winmd", +]; + +/// The folder published to GitHub Pages by `web.yml`; regenerated and checked +/// in by `gen.yml` like every other tool's output. Nested under `features` so +/// the page is served at `microsoft.github.io/windows-rs/features`. +const OUTPUT: &str = "web/features"; + +/// A record in the search index: an API's namespace, simple name, and any +/// additional namespaces (beyond its own) whose features it also requires +/// because they appear in its parameter or return types. +struct Entry { + namespace: usize, + name: String, + extras: Vec, +} + +fn main() { + let time = std::time::Instant::now(); + generate_page(OUTPUT); + println!("Finished in {:.2}s", time.elapsed().as_secs_f32()); +} + +/// Loads the bundled metadata and projects every type, function, constant, and +/// interface method into a flat list of [`Entry`] records plus the namespace +/// table they index. Methods and functions also record the extra namespaces +/// their signatures pull in, so the page can report every feature a call needs. +/// The output is canonical (namespaces sorted, entries sorted and de-duplicated) +/// so the generated page is byte-for-byte deterministic. +fn load() -> (Vec, Vec) { + let files: Vec = WINMD + .iter() + .map(|path| File::read(path).unwrap_or_else(|| panic!("cannot read {path}"))) + .collect(); + + let index = Index::new(files); + + let mut raw: Vec<(String, String, Vec)> = Vec::new(); + + for (namespace, name, item) in index.iter_items() { + match item { + // An interface also contributes its methods as `Interface::Method` + // entries; a method requires its interface's feature plus those of + // any other namespace appearing in its signature. + Item::Type(ty) => { + if ty.category() == TypeCategory::Interface { + // Generic interfaces (`IVector`, ...) reference type + // variables that need a generics slice to resolve, so their + // signatures are skipped; the method names are still indexed. + let generic = ty.generic_params().next().is_some(); + for method in ty.methods() { + let extras = if generic { + Vec::new() + } else { + signature_features(&method.signature(&[]), namespace, &index) + }; + raw.push(( + namespace.to_string(), + format!("{name}::{}", method.name()), + extras, + )); + } + } + raw.push((namespace.to_string(), name.to_string(), Vec::new())); + } + Item::Fn(method) => { + let extras = signature_features(&method.signature(&[]), namespace, &index); + raw.push((namespace.to_string(), name.to_string(), extras)); + } + Item::Const(_) => raw.push((namespace.to_string(), name.to_string(), Vec::new())), + } + } + + let mut namespaces: Vec = raw.iter().map(|(ns, _, _)| ns.clone()).collect(); + namespaces.sort(); + namespaces.dedup(); + + let lookup: HashMap<&str, usize> = namespaces + .iter() + .enumerate() + .map(|(i, ns)| (ns.as_str(), i)) + .collect(); + + let mut entries: Vec = raw + .into_iter() + .map(|(ns, name, extras)| Entry { + namespace: lookup[ns.as_str()], + name, + extras: extras.iter().map(|e| lookup[e.as_str()]).collect(), + }) + .collect(); + + entries + .sort_by(|a, b| (a.namespace, &a.name, &a.extras).cmp(&(b.namespace, &b.name, &b.extras))); + entries.dedup_by(|a, b| a.namespace == b.namespace && a.name == b.name && a.extras == b.extras); + + (namespaces, entries) +} + +/// Collects the additional feature-bearing namespaces referenced by a method or +/// function signature: every namespace named by a parameter or the return type, +/// minus the API's own namespace and the always-compiled `Foundation` ones. +fn signature_features(signature: &Signature, own: &str, index: &Index) -> Vec { + let mut referenced = Vec::new(); + for ty in signature.types.iter().chain([&signature.return_type]) { + collect_namespaces(ty, &mut referenced); + } + + let mut extras: Vec = referenced + .into_iter() + .filter(|ns| ns != own && !always_on(ns) && index.contains_namespace(ns)) + .collect(); + extras.sort(); + extras.dedup(); + extras +} + +/// Pushes the namespace of every named type reachable through pointers, arrays, +/// and by-ref wrappers in `ty`. +fn collect_namespaces(ty: &Type, out: &mut Vec) { + match ty { + Type::ClassName(name) | Type::ValueName(name) => out.push(name.namespace.clone()), + Type::Array(inner) + | Type::RefMut(inner) + | Type::RefConst(inner) + | Type::PtrMut(inner, _) + | Type::PtrConst(inner, _) + | Type::ArrayFixed(inner, _) => collect_namespaces(inner, out), + _ => {} + } +} + +/// The two namespaces that are always compiled and so never need a feature. +fn always_on(namespace: &str) -> bool { + namespace == "Windows.Foundation" || namespace == "Windows.Win32.Foundation" +} + +/// Emits a single self-contained, dependency-free `index.html` that searches an +/// inlined index in the browser. The Cargo feature for each API is derived in +/// the page from its namespace (drop the leading `Windows`, join with `_`; the +/// two `Foundation` namespaces are always compiled and need no feature), and a +/// method or function also lists the extra features its signature pulls in. +/// Inlining keeps it to one file that works both when hosted and when opened +/// directly from disk (a `file://` page cannot `fetch` a sibling file). +fn generate_page(dir: &str) { + let (namespaces, entries) = load(); + + std::fs::create_dir_all(dir).unwrap(); + + let mut json = String::from("{\"namespaces\":["); + for (i, namespace) in namespaces.iter().enumerate() { + if i > 0 { + json.push(','); + } + write!(json, "\"{}\"", escape(namespace)).unwrap(); + } + json.push_str("],\"items\":["); + for (i, entry) in entries.iter().enumerate() { + if i > 0 { + json.push(','); + } + write!(json, "[\"{}\",{}", escape(&entry.name), entry.namespace).unwrap(); + if !entry.extras.is_empty() { + json.push_str(",["); + for (j, extra) in entry.extras.iter().enumerate() { + if j > 0 { + json.push(','); + } + write!(json, "{extra}").unwrap(); + } + json.push(']'); + } + json.push(']'); + } + json.push_str("]}"); + + let html_path = format!("{dir}/index.html"); + std::fs::write(&html_path, PAGE.replace("__FEATURES_JSON__", &json)).unwrap(); + + println!( + "Wrote {} items across {} namespaces:\n {html_path} (self-contained, {} KB)", + entries.len(), + namespaces.len(), + json.len() / 1024 + ); +} + +/// Minimal JSON string escaping for metadata identifiers. +fn escape(value: &str) -> String { + let mut out = String::with_capacity(value.len()); + for ch in value.chars() { + match ch { + '"' => out.push_str("\\\""), + '\\' => out.push_str("\\\\"), + c if (c as u32) < 0x20 => write!(out, "\\u{:04x}", c as u32).unwrap(), + c => out.push(c), + } + } + out +} + +const PAGE: &str = r#" + + + + +windows-rs feature search + + + +

windows-rs feature search

+

Type an API name to find the Cargo feature(s) to enable in the windows or windows-sys crate. A method or function may need more than one when its parameters span namespaces. Matching is a case-insensitive regular expression over Namespace::Name.

+ +
Loading index…
+
APIFeature
+ + + + +"#; diff --git a/web/features/index.html b/web/features/index.html new file mode 100644 index 0000000000..5aafee13d6 --- /dev/null +++ b/web/features/index.html @@ -0,0 +1,77 @@ + + + + + +windows-rs feature search + + + +

windows-rs feature search

+

Type an API name to find the Cargo feature(s) to enable in the windows or windows-sys crate. A method or function may need more than one when its parameters span namespaces. Matching is a case-insensitive regular expression over Namespace::Name.

+ +
Loading index…
+
APIFeature
+ + + +