veryl_metadata/
component.rs1use crate::ComponentManifest;
2use crate::component_manifest::{COMMITTED_MANIFEST_FILE, parse_library_manifest};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6use std::time::SystemTime;
7
8#[derive(Clone, Debug, Serialize, Deserialize)]
13#[serde(deny_unknown_fields)]
14pub struct Component {
15 pub path: PathBuf,
18 #[serde(default)]
20 pub wasm: Option<PathBuf>,
21}
22
23impl Component {
24 pub fn collect_manifests(
32 &self,
33 root: &Path,
34 target_dir: &Path,
35 ) -> Vec<(String, ComponentManifest)> {
36 let crate_dir = root.join(&self.path);
37 let sidecar =
38 component_crate_name(&crate_dir).map(|name| sidecar_manifest_path(target_dir, &name));
39 let committed = crate_dir.join(COMMITTED_MANIFEST_FILE);
40 let found =
41 read_newest_manifest_file(&[sidecar.as_deref(), Some(&committed)]).or_else(|| {
42 let wasm = std::fs::read(root.join(self.wasm.as_ref()?)).ok()?;
43 ComponentManifest::parse_all_from_wasm(&wasm)
44 });
45 let mut ret: Vec<_> = found
46 .unwrap_or_default()
47 .into_iter()
48 .filter(|(name, _)| {
49 let valid = veryl_component_sys::is_valid_component_name(name);
50 if !valid {
51 log::warn!(
52 "component export `{name}` in {} is not an identifier and cannot be referenced as $comp::<name>; ignored",
53 self.path.display()
54 );
55 }
56 valid
57 })
58 .collect();
59 ret.sort_by(|a, b| a.0.cmp(&b.0));
60 ret
61 }
62}
63
64pub fn component_crate_name(crate_dir: &Path) -> Option<String> {
66 let text = std::fs::read_to_string(crate_dir.join("Cargo.toml")).ok()?;
67 let value: toml::Value = toml::from_str(&text).ok()?;
68 Some(value.get("package")?.get("name")?.as_str()?.to_string())
69}
70
71pub fn sidecar_manifest_path(target_dir: &Path, crate_name: &str) -> PathBuf {
76 let snake = crate_name.replace('-', "_");
77 target_dir
78 .join("release")
79 .join(format!("{snake}.manifest.json"))
80}
81
82pub fn read_committed_manifests(crate_dir: &Path) -> Option<HashMap<String, ComponentManifest>> {
85 read_manifest_file(&crate_dir.join(COMMITTED_MANIFEST_FILE))
86}
87
88fn read_manifest_file(path: &Path) -> Option<HashMap<String, ComponentManifest>> {
91 let json = std::fs::read_to_string(path).ok()?;
92 let manifests = parse_library_manifest(&json);
93 (!manifests.is_empty()).then_some(manifests)
94}
95
96fn read_newest_manifest_file(
101 candidates: &[Option<&Path>],
102) -> Option<HashMap<String, ComponentManifest>> {
103 let mut found: Vec<(SystemTime, &Path)> = candidates
104 .iter()
105 .flatten()
106 .filter_map(|path| {
107 let mtime = std::fs::metadata(path).and_then(|m| m.modified()).ok()?;
108 Some((mtime, *path))
109 })
110 .collect();
111 found.sort_by_key(|(mtime, _)| std::cmp::Reverse(*mtime));
112 found
113 .into_iter()
114 .find_map(|(_, path)| read_manifest_file(path))
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 fn manifest_json(ty: &str) -> String {
122 format!(r#"{{"types":{{"{ty}":{{"kind":"clocked"}}}}}}"#)
123 }
124
125 fn setup(dir: &Path) -> Component {
126 std::fs::create_dir_all(dir.join("crate")).unwrap();
127 std::fs::create_dir_all(dir.join("target/release")).unwrap();
128 std::fs::write(
129 dir.join("crate/Cargo.toml"),
130 "[package]\nname = \"demo-comp\"\n",
131 )
132 .unwrap();
133 Component {
134 path: "crate".into(),
135 wasm: None,
136 }
137 }
138
139 fn names(entry: &Component, root: &Path) -> Vec<String> {
140 entry
141 .collect_manifests(root, &root.join("target"))
142 .into_iter()
143 .map(|(n, _)| n)
144 .collect()
145 }
146
147 #[test]
148 fn newest_manifest_source_wins() {
149 let dir = std::env::temp_dir().join(format!("veryl_newest_{}", std::process::id()));
150 let _ = std::fs::remove_dir_all(&dir);
151 let entry = setup(&dir);
152 let committed = dir.join("crate").join(COMMITTED_MANIFEST_FILE);
153 let sidecar = sidecar_manifest_path(&dir.join("target"), "demo-comp");
154
155 std::fs::write(&committed, manifest_json("from_committed")).unwrap();
156 assert_eq!(names(&entry, &dir), ["from_committed"]);
157
158 std::thread::sleep(std::time::Duration::from_millis(20));
160 std::fs::write(&sidecar, manifest_json("from_sidecar")).unwrap();
161 assert_eq!(names(&entry, &dir), ["from_sidecar"]);
162
163 std::thread::sleep(std::time::Duration::from_millis(20));
165 std::fs::write(&committed, manifest_json("from_committed")).unwrap();
166 assert_eq!(names(&entry, &dir), ["from_committed"]);
167
168 let _ = std::fs::remove_dir_all(&dir);
169 }
170
171 #[test]
172 fn non_identifier_export_names_are_dropped() {
173 let dir = std::env::temp_dir().join(format!("veryl_names_{}", std::process::id()));
174 let _ = std::fs::remove_dir_all(&dir);
175 let entry = setup(&dir);
176 std::fs::write(
177 dir.join("crate").join(COMMITTED_MANIFEST_FILE),
178 r#"{"types":{"ok_name":{"kind":"clocked"},"bus::monitor":{"kind":"clocked"},"1bad":{}}}"#,
179 )
180 .unwrap();
181 assert_eq!(names(&entry, &dir), ["ok_name"]);
182 let _ = std::fs::remove_dir_all(&dir);
183 }
184}