discover.rs
raw
//! 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<HashMap<PathBuf, Vec<PathBuf>>> = 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<PathBuf> {
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<PathBuf> {
// `--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<PathBuf> {
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::<Vec<_>>()
})
.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<String> {
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)
}