1use super::discovery::{FileTargets, Found, Reach, declaration, declare, discovery_ignore, walk};
5use super::*;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct Site {
10 pub kind: ArtifactKind,
11 pub path: PathBuf,
12}
13
14impl Site {
15 pub fn folder(&self) -> &Path {
17 folder_of(self.kind, &self.path)
18 }
19
20 pub fn declaration_path(&self) -> PathBuf {
22 declaration_of(self.kind, &self.path)
23 }
24}
25
26pub(super) fn folder_of(kind: ArtifactKind, path: &Path) -> &Path {
27 match kind {
28 ArtifactKind::Folder | ArtifactKind::Graph => path,
29 ArtifactKind::File | ArtifactKind::Aspect => path.parent().unwrap_or(Path::new("")),
30 }
31}
32
33pub(super) fn declaration_of(kind: ArtifactKind, path: &Path) -> PathBuf {
34 match kind {
35 ArtifactKind::Graph => PathBuf::from(".artf"),
36 ArtifactKind::Folder => logical_join(path, std::ffi::OsStr::new(CONFIG_FILE)),
37 ArtifactKind::File | ArtifactKind::Aspect => {
38 let mut path = path.as_os_str().to_owned();
39 path.push(".artf");
40 PathBuf::from(path)
41 }
42 }
43}
44
45pub fn read_sites(repo: &Path) -> Result<BTreeMap<ArtifactName, Site>, ConfigError> {
48 let root =
49 crate::platform::canonicalize(repo).map_err(|error| ConfigError::new(repo, error))?;
50 let ignored = discovery_ignore(&root)?;
51 let mut sites = BTreeMap::new();
52 walk(
53 &root,
54 &ignored,
55 Reach::Workspace,
56 FileTargets::DeclarationsOnly,
57 &mut |found: Found<'_>| {
58 let name = declared_name(&found.source)
59 .map_err(|error| ConfigError::new(found.file, error))?;
60 let site = Site {
61 kind: found.kind,
62 path: found.path,
63 };
64 if sites.insert(name.clone(), site).is_some() {
65 return Err(ConfigError::declaration(
66 found.file,
67 &["name"],
68 format!("Duplicate Artifact name: {name}."),
69 ));
70 }
71 Ok(name)
72 },
73 )?;
74 Ok(sites)
75}
76
77fn declared_name(source: &str) -> Result<ArtifactName, String> {
79 let table = toml::de::DeTable::parse(source).map_err(|error| error.to_string())?;
80 match table.get_ref().get("name").map(|name| name.get_ref()) {
81 Some(toml::de::DeValue::String(name)) => name.parse(),
82 Some(_) => Err("name must be a string.".into()),
83 None => Err("missing field `name`".into()),
84 }
85}
86
87#[derive(Debug, Error)]
89pub enum ScopeReadError {
90 #[error("{name} is no longer declared at {}.", crate::platform::path_text(.path))]
92 Moved { name: ArtifactName, path: PathBuf },
93 #[error(transparent)]
94 Config(#[from] ConfigError),
95}
96
97pub(crate) fn read_artifact_scope(
102 repo: &Path,
103 name: &ArtifactName,
104 site: &Site,
105) -> Result<RepoConfig, ScopeReadError> {
106 let mut config = read_declared(repo, name, site)?;
107 config.evals.clear();
108 Ok(config)
109}
110
111pub(crate) fn read_eval_scope(
117 repo: &Path,
118 id: &crate::types::EvalId,
119 sites: &BTreeMap<ArtifactName, Site>,
120) -> Result<Option<RepoConfig>, ScopeReadError> {
121 let mut scoped: Option<RepoConfig> = None;
122 for (name, site) in sites {
123 let mut read = read_declared(repo, name, site)?;
124 read.evals.retain(|eval| &eval.id == id);
125 match &mut scoped {
126 None => scoped = Some(read),
127 Some(config) => {
128 for (name, artifact) in read.artifacts {
129 config.artifacts.entry(name).or_insert(artifact);
130 }
131 if config.evals.is_empty() {
132 config.evals = read.evals;
133 }
134 }
135 }
136 }
137 let Some(mut config) = scoped else {
138 return Ok(None);
139 };
140 let outside = sites
141 .keys()
142 .flat_map(|owner| named(&config.artifacts[owner]))
143 .any(|name| !config.artifacts.contains_key(name.as_str()));
144 if outside || crate::scope::resolve_evals(&mut config).is_err() {
145 return Ok(None);
146 }
147 Ok(Some(config))
148}
149
150fn named(artifact: &Artifact) -> impl Iterator<Item = &ReferenceName> {
152 let script = match &artifact.fingerprint {
153 Some(Fingerprint::Script { args, .. }) => args.as_slice(),
154 _ => &[],
155 };
156 let tools = artifact
157 .views
158 .human_tools
159 .values()
160 .flat_map(|tool| match tool {
161 HumanTool::Command(tool) => tool.args.as_slice(),
162 HumanTool::Builtin(tool) => tool.args.as_slice(),
163 });
164 script
165 .iter()
166 .chain(tools)
167 .filter_map(|argument| match argument {
168 crate::scope::Argument::Reference { name, .. } => Some(name),
169 crate::scope::Argument::Literal(_) => None,
170 })
171 .filter(|name| name.as_str() != "artifactPath")
173}
174
175fn read_declared(
177 repo: &Path,
178 name: &ArtifactName,
179 site: &Site,
180) -> Result<RepoConfig, ScopeReadError> {
181 let root =
182 crate::platform::canonicalize(repo).map_err(|error| ConfigError::new(repo, error))?;
183 let moved = || ScopeReadError::Moved {
184 name: name.clone(),
185 path: site.declaration_path(),
186 };
187 let ignored = discovery_ignore(&root)?;
188 let folder = site.folder();
189 if !crate::platform::path_kind(&root.join(site.declaration_path()))
190 .is_ok_and(|kind| kind == crate::platform::FileKind::File)
191 || !folder.as_os_str().is_empty()
192 && ignored
193 .matched_path_or_any_parents(root.join(folder), true)
194 .is_ignore()
195 {
196 return Err(moved());
197 }
198 let reach = match site.kind {
199 ArtifactKind::Folder => Reach::Folder(&site.path),
200 ArtifactKind::File | ArtifactKind::Aspect | ArtifactKind::Graph => Reach::File(
201 folder,
202 site.path
203 .file_name()
204 .and_then(|target| target.to_str())
205 .unwrap_or_default(),
206 ),
207 };
208 let mut config = RepoConfig {
209 root: root.clone(),
210 artifacts: BTreeMap::new(),
211 evals: Vec::new(),
212 relations: Vec::new(),
213 };
214 let mut schemas = super::schemas::Schemas::default();
215 walk(
216 &root,
217 &ignored,
218 reach,
219 FileTargets::Required,
220 &mut |found: Found<'_>| {
221 let own = config.artifacts.is_empty();
222 if own && (found.kind != site.kind || found.path != site.path) {
223 return Err(moved());
224 }
225 let declaration = declaration(&found, &mut schemas)?;
226 if own && &declaration.name != name {
227 return Err(moved());
228 }
229 Ok(declare(&mut config, found, declaration)?)
230 },
231 )?;
232 if !config.artifacts.contains_key(name) {
233 return Err(moved());
234 }
235 Ok(config)
236}
237
238#[cfg(test)]
239mod tests;