use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Mutex, OnceLock};
use serde::Deserialize;
use crate::meta::Project;
use crate::targets::{self, Target, TargetKind};
#[derive(Deserialize)]
struct ToolkitMeta {
target: String,
#[serde(default)]
kind: Option<String>,
#[serde(default)]
host: Option<String>,
#[serde(default)]
label: Option<String>,
#[serde(default)]
doctor: Option<String>,
}
#[derive(Debug)]
pub struct ExternalToolkit {
pub target: &'static Target,
pub crate_name: String,
pub doctor: Option<String>,
}
fn cache() -> &'static Mutex<HashMap<PathBuf, &'static [ExternalToolkit]>> {
static CACHE: OnceLock<Mutex<HashMap<PathBuf, &'static [ExternalToolkit]>>> = OnceLock::new();
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
pub fn resolve(project: &Project) -> Result<&'static [ExternalToolkit], String> {
if let Some(hit) = cache()
.lock()
.ok()
.and_then(|c| c.get(&project.root).copied())
{
return Ok(hit);
}
let meta = crate::pieces::cargo_metadata_all_features(project)?;
let mut decls: Vec<(String, ToolkitMeta)> = Vec::new();
for pkg in &meta.packages {
if let Some(m) = crate::pieces::piece_meta::<ToolkitMeta>(pkg, "toolkit") {
decls.push((pkg.name.clone(), m));
}
}
let catalog = build_catalog(decls)?;
let leaked: &'static [ExternalToolkit] = Box::leak(catalog.into_boxed_slice());
if let Ok(mut c) = cache().lock() {
c.insert(project.root.clone(), leaked);
}
Ok(leaked)
}
fn build_catalog(decls: Vec<(String, ToolkitMeta)>) -> Result<Vec<ExternalToolkit>, String> {
fn leak(s: String) -> &'static str {
Box::leak(s.into_boxed_str())
}
let mut out: Vec<ExternalToolkit> = Vec::new();
for (crate_name, m) in decls {
let name = m.target;
let Some((os, toolkit)) = name.split_once('-') else {
return Err(format!(
"{crate_name}: [package.metadata.day.toolkit] target {name:?} is not an \
<os>-<toolkit> pair"
));
};
if os.is_empty()
|| toolkit.is_empty()
|| !name
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
{
return Err(format!(
"{crate_name}: [package.metadata.day.toolkit] target {name:?} must be lowercase \
ascii <os>-<toolkit>"
));
}
match m.kind.as_deref() {
None | Some("desktop") => {}
Some(other) => {
return Err(format!(
"{crate_name}: [package.metadata.day.toolkit] kind {other:?} is not \
supported — external toolkits are \"desktop\" only today (a new pipeline \
kind needs CLI code, which cannot come from a crate)"
));
}
}
if targets::find(&name).is_some() {
return Err(format!(
"{crate_name}: [package.metadata.day.toolkit] target {name:?} collides with the \
builtin target of the same name"
));
}
if let Some(prev) = out.iter().find(|t| t.target.name == name) {
return Err(format!(
"target {name:?} is declared by both {} and {crate_name}",
prev.crate_name
));
}
let target: &'static Target = Box::leak(Box::new(Target {
name: leak(name.clone()),
toolkit: leak(toolkit.to_string()),
kind: TargetKind::Desktop,
os: leak(os.to_string()),
host: leak(m.host.unwrap_or_else(|| "any".into())),
label: leak(m.label.unwrap_or(name)),
experimental: true,
}));
out.push(ExternalToolkit {
target,
crate_name,
doctor: m.doctor,
});
}
Ok(out)
}
pub fn find_target(project: &Project, name: &str) -> Result<&'static Target, String> {
if let Some(t) = targets::find(name) {
return Ok(t);
}
let builtin = || {
targets::TARGETS
.iter()
.map(|t| t.name)
.collect::<Vec<_>>()
.join(", ")
};
match resolve(project) {
Ok(ext) => {
if let Some(t) = ext.iter().find(|t| t.target.name == name) {
return Ok(t.target);
}
let declared = if ext.is_empty() {
"none".to_string()
} else {
ext.iter()
.map(|t| format!("{} ({})", t.target.name, t.crate_name))
.collect::<Vec<_>>()
.join(", ")
};
Err(format!(
"unknown target {name:?}\n builtin: {}\n declared by this project's crates: \
{declared}",
builtin()
))
}
Err(e) => Err(format!(
"unknown target {name:?} (builtin: {}) — and external toolkit discovery failed: {e}",
builtin()
)),
}
}
pub fn known(project: &Project, name: &str) -> bool {
find_target(project, name).is_ok()
}
pub fn is_external(target: &Target) -> bool {
targets::find(target.name).is_none()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_fixture_project_declares_a_target_end_to_end() {
let root =
std::env::temp_dir().join(format!("day-external-fixture-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let tk = root.join("toolkit");
let app = root.join("app");
std::fs::create_dir_all(tk.join("src")).unwrap();
std::fs::create_dir_all(app.join("src")).unwrap();
std::fs::write(
tk.join("Cargo.toml"),
r#"[package]
name = "day-toolkit-fixture"
version = "0.0.0"
edition = "2021"
[package.metadata.day.toolkit]
target = "testos-fixturetk"
label = "Fixture TK"
doctor = "true"
"#,
)
.unwrap();
std::fs::write(tk.join("src/lib.rs"), "").unwrap();
std::fs::write(
app.join("Cargo.toml"),
r#"[package]
name = "fixture-app"
version = "0.0.0"
edition = "2021"
[features]
fixturetk = ["dep:day-toolkit-fixture"]
[dependencies]
day-toolkit-fixture = { path = "../toolkit", optional = true }
[workspace]
"#,
)
.unwrap();
std::fs::write(app.join("src/main.rs"), "fn main() {}\n").unwrap();
std::fs::write(
app.join("Day.toml"),
r#"schema = 1
[app]
id = "dev.test.fixture"
targets = ["testos-fixturetk"]
"#,
)
.unwrap();
let project = crate::meta::find_project(Some(&app)).expect("fixture project loads");
let ext = resolve(&project).expect("discovery succeeds");
assert_eq!(ext.len(), 1);
assert_eq!(ext[0].target.name, "testos-fixturetk");
assert_eq!(ext[0].crate_name, "day-toolkit-fixture");
assert_eq!(ext[0].doctor.as_deref(), Some("true"));
assert!(find_target(&project, "testos-fixturetk").is_ok());
assert!(
find_target(&project, "macos-appkit").is_ok(),
"builtins still resolve"
);
let err = find_target(&project, "bogus").expect_err("unknown stays unknown");
assert!(
err.contains("testos-fixturetk (day-toolkit-fixture)"),
"the error lists the declaration: {err}"
);
assert!(crate::external::known(&project, "testos-fixturetk"));
let _ = std::fs::remove_dir_all(&root);
}
fn meta(target: &str) -> ToolkitMeta {
ToolkitMeta {
target: target.into(),
kind: None,
host: None,
label: None,
doctor: None,
}
}
#[test]
fn a_declaration_becomes_a_desktop_target_with_defaults() {
let out = build_catalog(vec![("day-toolkit-wx".into(), meta("netbsd-wxwidgets"))])
.expect("valid declaration");
let t = out[0].target;
assert_eq!(t.name, "netbsd-wxwidgets");
assert_eq!(t.toolkit, "wxwidgets", "feature = the toolkit half");
assert_eq!(t.os, "netbsd", "os = the name prefix, by contract");
assert_eq!(t.host, "any");
assert_eq!(t.kind, TargetKind::Desktop);
assert!(t.experimental);
}
#[test]
fn a_builtin_collision_is_an_error_naming_the_crate() {
let err = build_catalog(vec![("evil".into(), meta("macos-appkit"))])
.expect_err("must not shadow a builtin");
assert!(err.contains("evil") && err.contains("collides"), "{err}");
}
#[test]
fn a_duplicate_declaration_names_both_crates() {
let err = build_catalog(vec![
("crate-a".into(), meta("netbsd-wxwidgets")),
("crate-b".into(), meta("netbsd-wxwidgets")),
])
.expect_err("duplicates are ambiguous");
assert!(err.contains("crate-a") && err.contains("crate-b"), "{err}");
}
#[test]
fn only_the_desktop_kind_is_accepted() {
let mut m = meta("solaris-motif");
m.kind = Some("mobile".into());
let err = build_catalog(vec![("x".into(), m)]).expect_err("kinds need CLI code");
assert!(err.contains("desktop"), "{err}");
}
#[test]
fn malformed_names_are_rejected() {
for bad in ["wxwidgets", "NetBSD-wx", "-wx", "netbsd-", "net bsd-wx"] {
assert!(
build_catalog(vec![("x".into(), meta(bad))]).is_err(),
"{bad:?} should be rejected"
);
}
}
}