#![allow(dead_code)]
use std::collections::{BTreeMap, BTreeSet};
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use serde::Deserialize;
fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let plugins_dir = manifest_dir.join("plugins");
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let out_file = out_dir.join("embedded_catalog.toml");
println!("cargo:rerun-if-changed=plugins");
let index_path = plugins_dir.join("_index.toml");
let index_text = fs::read_to_string(&index_path)
.unwrap_or_else(|e| panic!("could not read {}: {e}", index_path.display()));
let index: IndexFile = toml::from_str(&index_text).unwrap_or_else(|e| {
panic!(
"plugins/_index.toml failed shape check: {e}\n\
expected `catalog_version = <int>` and `plugins = [\"name\", ...]`"
)
});
let mut plugins: Vec<(String, PluginFile, String)> = Vec::with_capacity(index.plugins.len());
for name in &index.plugins {
let path = plugins_dir.join(format!("{name}.toml"));
let body = fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("could not read {}: {e}", path.display()));
let parsed: PluginFile = toml::from_str(&body).unwrap_or_else(|e| {
panic!(
"plugins/{name}.toml failed shape check: {e}\n\
(build.rs validates plugin TOML against the same shape src/config.rs uses)"
)
});
plugins.push((name.clone(), parsed, body));
}
if let Err(violations) = check_referential_integrity(&plugins) {
panic!(
"plugin catalog failed referential integrity ({} violations):\n - {}\n\n\
every relation must point at a source defined by some plugin file",
violations.len(),
violations.join("\n - ")
);
}
let mut buf = String::new();
buf.push_str("# Generated by build.rs from plugins/*.toml. Edit those.\n");
buf.push_str(&format!("catalog_version = {}\n\n", index.catalog_version));
for (name, _parsed, body) in &plugins {
buf.push_str(&format!("# ---- plugin: {name} ----\n\n"));
buf.push_str(body);
if !body.ends_with('\n') {
buf.push('\n');
}
buf.push('\n');
}
write_if_changed(&out_file, &buf)
.unwrap_or_else(|e| panic!("could not write {}: {e}", out_file.display()));
}
fn write_if_changed(path: &Path, contents: &str) -> std::io::Result<()> {
if let Ok(existing) = fs::read_to_string(path) {
if existing == contents {
return Ok(());
}
}
fs::write(path, contents)
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct IndexFile {
catalog_version: u32,
plugins: Vec<String>,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct PluginFile {
#[serde(default, rename = "source")]
sources: BTreeMap<String, PluginSourceDef>,
#[serde(default, rename = "relation")]
relations: Vec<PluginRelation>,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct PluginSourceDef {
#[serde(default)]
description: Option<String>,
#[serde(default)]
windows: Option<String>,
#[serde(default)]
macos: Option<String>,
#[serde(default)]
linux: Option<String>,
#[serde(default)]
termux: Option<String>,
#[serde(default)]
unix: Option<String>,
#[serde(default)]
uninstall_command: Option<String>,
}
#[derive(Deserialize, Debug)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
enum PluginRelation {
AliasOf {
parent: String,
children: Vec<String>,
},
ConflictsWhenBothInPath {
sources: Vec<String>,
diagnostic: String,
},
ServedByVia {
host: String,
guest_pattern: String,
guest_provider: String,
#[serde(default)]
installer_token: Option<String>,
},
DependsOn {
source: String,
target: String,
},
PreferOrderOver {
earlier: String,
later: String,
},
}
fn check_referential_integrity(
plugins: &[(String, PluginFile, String)],
) -> Result<(), Vec<String>> {
let mut all_sources: BTreeSet<&str> = BTreeSet::new();
for (_name, plugin, _body) in plugins {
for src_name in plugin.sources.keys() {
all_sources.insert(src_name.as_str());
}
}
let mut violations: Vec<String> = Vec::new();
for (plugin_name, plugin, _body) in plugins {
for rel in &plugin.relations {
let referenced = relation_source_refs(rel);
for r in referenced {
if !all_sources.contains(r) {
violations.push(format!(
"{plugin_name}.toml: relation references undefined source `{r}`"
));
}
}
}
}
if violations.is_empty() {
Ok(())
} else {
Err(violations)
}
}
fn relation_source_refs(rel: &PluginRelation) -> Vec<&str> {
match rel {
PluginRelation::AliasOf { parent, children } => {
let mut out = vec![parent.as_str()];
out.extend(children.iter().map(|c| c.as_str()));
out
}
PluginRelation::ConflictsWhenBothInPath { sources, .. } => {
sources.iter().map(|s| s.as_str()).collect()
}
PluginRelation::ServedByVia {
host,
guest_provider,
..
} => vec![host.as_str(), guest_provider.as_str()],
PluginRelation::DependsOn { source, target } => vec![source.as_str(), target.as_str()],
PluginRelation::PreferOrderOver { earlier, later } => {
vec![earlier.as_str(), later.as_str()]
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_plugin(text: &str) -> Result<PluginFile, toml::de::Error> {
toml::from_str(text)
}
#[test]
fn empty_plugin_parses_to_empty_collections() {
let p = parse_plugin("").unwrap();
assert!(p.sources.is_empty());
assert!(p.relations.is_empty());
}
#[test]
fn shape_check_rejects_unknown_top_level_field() {
let err = parse_plugin("unknown_top = 1").unwrap_err();
assert!(
err.to_string().contains("unknown"),
"expected unknown-field error, got: {err}"
);
}
#[test]
fn shape_check_rejects_unknown_source_field() {
let err = parse_plugin(
r#"
[source.x]
unix = "/foo/bin"
typo_field = "oops"
"#,
)
.unwrap_err();
assert!(
err.to_string().contains("typo_field"),
"expected unknown-field error, got: {err}"
);
}
#[test]
fn shape_check_rejects_unknown_relation_kind() {
let err = parse_plugin(
r#"
[[relation]]
kind = "made_up_kind"
foo = "bar"
"#,
)
.unwrap_err();
assert!(
err.to_string().contains("made_up_kind"),
"expected unknown-variant error, got: {err}"
);
}
#[test]
fn referential_integrity_passes_when_every_relation_target_exists() {
let plugin = parse_plugin(
r#"
[source.host]
unix = "/h"
[source.guest]
unix = "/g"
[[relation]]
kind = "served_by_via"
host = "host"
guest_pattern = "x-*"
guest_provider = "guest"
"#,
)
.unwrap();
let plugins = vec![("p".to_string(), plugin, String::new())];
assert!(check_referential_integrity(&plugins).is_ok());
}
#[test]
fn referential_integrity_rejects_dangling_relation_target() {
let plugin = parse_plugin(
r#"
[source.host]
unix = "/h"
[[relation]]
kind = "served_by_via"
host = "host"
guest_pattern = "x-*"
guest_provider = "missing_guest"
"#,
)
.unwrap();
let plugins = vec![("p".to_string(), plugin, String::new())];
let violations = check_referential_integrity(&plugins).unwrap_err();
assert!(
violations.iter().any(|v| v.contains("missing_guest")),
"expected the dangling source name in the violations: {violations:?}"
);
}
#[test]
fn referential_integrity_lists_every_violation() {
let plugin = parse_plugin(
r#"
[source.host]
unix = "/h"
[[relation]]
kind = "served_by_via"
host = "host"
guest_pattern = "x-*"
guest_provider = "missing_one"
[[relation]]
kind = "depends_on"
source = "host"
target = "missing_two"
"#,
)
.unwrap();
let plugins = vec![("p".to_string(), plugin, String::new())];
let violations = check_referential_integrity(&plugins).unwrap_err();
assert_eq!(violations.len(), 2);
assert!(violations.iter().any(|v| v.contains("missing_one")));
assert!(violations.iter().any(|v| v.contains("missing_two")));
}
#[test]
fn referential_integrity_sees_sources_across_plugins() {
let plugin_a = parse_plugin(
r#"
[source.host]
unix = "/h"
"#,
)
.unwrap();
let plugin_b = parse_plugin(
r#"
[source.guest]
unix = "/g"
[[relation]]
kind = "served_by_via"
host = "host"
guest_pattern = "x-*"
guest_provider = "guest"
"#,
)
.unwrap();
let plugins = vec![
("a".to_string(), plugin_a, String::new()),
("b".to_string(), plugin_b, String::new()),
];
assert!(check_referential_integrity(&plugins).is_ok());
}
}