//! Auto-discovery of a dependency's widget manifests, for **every** kind of //! dependency — a path or workspace crate, and (the common case) a crates.io or //! git dependency Cargo has unpacked into its local cache. //! //! `include_component!` needs a dependency's `.gdw` at expansion to type-check a //! `.gdc` that names its widget. The app's `Cargo.toml` only names a version for //! a registry dependency, not a path — but Cargo has resolved every dependency //! to an on-disk manifest, and `cargo metadata` reports those paths. It is the //! one mechanism that finds a registry or git dependency's source the same way //! it finds a path one, so this asks it once per crate compile (cached) and //! reads each package's widget directory. //! //! Requirement on a published widget crate: its `.gdw` files must ship in the //! package (Cargo includes non-ignored files by default, so `widgets/*.gdw` is //! packaged unless a restrictive `include` omits it). use std::cell::RefCell; use std::collections::HashMap; use std::path::{Path, PathBuf}; use cargo_metadata::MetadataCommand; thread_local! { /// One `cargo metadata` run per crate compile, shared across every /// `include_component!` in it — keyed by the crate's manifest directory. static CACHE: RefCell>> = RefCell::new(HashMap::new()); } /// The widget-manifest directories contributed by a crate's dependencies — /// path, workspace, registry, and git alike — so naming their widgets needs no /// configuration. pub fn dependency_widget_dirs(manifest_dir: &Path) -> Vec { CACHE.with(|cache| { if let Some(dirs) = cache.borrow().get(manifest_dir) { return dirs.clone(); } let dirs = discover(manifest_dir); cache .borrow_mut() .insert(manifest_dir.to_path_buf(), dirs.clone()); dirs }) } fn discover(manifest_dir: &Path) -> Vec { // `--filter-platform` restricts the resolve to the platform being built, so // a cross-platform dependency the build never uses (and never downloaded) // does not make `--offline` fail trying to fetch it. // // `--offline`: the graph is already resolved and fetched for the build in // progress, so read it without the network. The package-cache lock the // outer Cargo holds during resolution is released before compilation, when // proc-macros run, so this does not contend with it. let mut options = vec!["--offline".to_owned()]; if let Some(host) = host_triple() { options.push("--filter-platform".to_owned()); options.push(host); } let metadata = MetadataCommand::new() .manifest_path(manifest_dir.join("Cargo.toml")) .other_options(options) .exec(); let Ok(metadata) = metadata else { return Vec::new(); }; metadata .packages .iter() .flat_map(|package| match package.manifest_path.parent() { Some(crate_dir) => widget_dirs_of(crate_dir.as_std_path(), &package.metadata), None => Vec::new(), }) .collect() } /// The existing widget directories of a crate: its own `widget-path`, or the /// default `widgets/`. fn widget_dirs_of(crate_dir: &Path, metadata: &serde_json::Value) -> Vec { let configured = metadata .get("guiduck") .and_then(|guiduck| guiduck.get("widget-path")) .and_then(serde_json::Value::as_array) .map(|array| { array .iter() .filter_map(|entry| entry.as_str().map(str::to_owned)) .collect::>() }) .unwrap_or_else(|| vec![crate::DEFAULT_WIDGET_DIR.to_owned()]); configured .into_iter() .map(|dir| crate_dir.join(dir)) .filter(|dir| dir.is_dir()) .collect() } /// The target triple being compiled for, from `rustc -vV`'s `host:` line — the /// triple `--filter-platform` needs. A proc-macro is not handed it in the /// environment, so it asks the compiler that is running. fn host_triple() -> Option { let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_owned()); let output = std::process::Command::new(rustc).arg("-vV").output().ok()?; let text = String::from_utf8(output.stdout).ok()?; text.lines() .find_map(|line| line.strip_prefix("host: ")) .map(str::to_owned) }