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)
}