1use std::collections::BTreeMap;
11use std::path::{Path, PathBuf};
12use std::sync::Mutex;
13
14use dre_protocol::{PluginId, executable_name, parse_executable_name, parse_package_executable_name};
15use semver::{Version, VersionReq};
16
17use crate::codes::Code;
18use serde::{Deserialize, Serialize};
19
20pub const MANIFEST: &str = "plugin.json";
22
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25pub struct Manifest {
26 pub executable: String,
28 pub provides: Vec<PluginId>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct InstalledPackage {
33 pub name: String,
34 pub version: Option<Version>,
36 pub path: PathBuf,
37 pub provides: Vec<PluginId>,
39}
40
41impl InstalledPackage {
42 pub fn provides(&self, id: &PluginId) -> bool {
43 self.provides.contains(id)
44 }
45}
46
47pub const DEPS_DIR: &str = "dre_deps";
49
50pub fn override_dir() -> Option<PathBuf> {
52 crate::settings::env(crate::settings::PLUGINS_DIR).map(PathBuf::from)
53}
54
55pub fn cache_dir() -> PathBuf {
58 override_dir().unwrap_or_else(|| crate::dre_home().join("plugins"))
59}
60
61pub fn plugins_dir(project: Option<&Path>) -> PathBuf {
63 match (override_dir(), project) {
64 (Some(d), _) => d,
65 (None, Some(root)) => root.join(DEPS_DIR).join("plugins"),
66 (None, None) => cache_dir(),
67 }
68}
69
70pub fn link_or_copy(src: &Path, dst: &Path) -> Result<(), String> {
72 let parent = dst.parent().unwrap();
73 std::fs::create_dir_all(parent).map_err(|e| format!("can't create {}: {e}", parent.display()))?;
74 let _ = std::fs::remove_file(dst);
75 if std::fs::hard_link(src, dst).is_ok() {
76 return Ok(());
77 }
78 std::fs::copy(src, dst).map_err(|e| format!("can't copy {} to {}: {e}", src.display(), dst.display()))?;
79 #[cfg(unix)]
80 {
81 use std::os::unix::fs::PermissionsExt;
82 std::fs::set_permissions(dst, std::fs::Permissions::from_mode(0o755)).map_err(|e| e.to_string())?;
83 }
84 Ok(())
85}
86
87pub fn link_version(from: &Path, to: &Path) -> Result<(), String> {
90 let m = read_manifest(from).ok_or_else(|| format!("{} has no {MANIFEST}", from.display()))?;
91 link_or_copy(&from.join(&m.executable), &to.join(&m.executable))?;
92 link_or_copy(&from.join(MANIFEST), &to.join(MANIFEST))
93}
94
95pub fn read_manifest(version_dir: &Path) -> Option<Manifest> {
96 let text = std::fs::read_to_string(version_dir.join(MANIFEST)).ok()?;
97 serde_json::from_str(&text).ok()
98}
99
100pub fn write_manifest(version_dir: &Path, m: &Manifest) -> Result<(), String> {
101 let p = version_dir.join(MANIFEST);
102 crate::fs::write_atomic(&p, serde_json::to_string_pretty(m).unwrap().as_bytes())
103 .map_err(|e| format!("can't write {}: {e}", p.display()))
104}
105
106fn is_executable(p: &Path) -> bool {
107 let Ok(meta) = std::fs::metadata(p) else {
108 return false;
109 };
110 if !meta.is_file() {
111 return false;
112 }
113 #[cfg(unix)]
114 {
115 use std::os::unix::fs::PermissionsExt;
116 meta.permissions().mode() & 0o111 != 0
117 }
118 #[cfg(not(unix))]
119 true
120}
121
122type Asked = (Option<std::time::SystemTime>, Vec<PluginId>);
124
125pub fn probe(path: &Path) -> Result<Vec<PluginId>, String> {
128 static ASKED: Mutex<BTreeMap<PathBuf, Asked>> = Mutex::new(BTreeMap::new());
129 let file = path.file_name().unwrap_or_default().to_string_lossy();
130 if let Some((kind, name)) = parse_executable_name(&file) {
131 return Ok(vec![PluginId::new(kind, name)]);
132 }
133 let modified = std::fs::metadata(path).and_then(|m| m.modified()).ok();
134 if let Some((m, ids)) = ASKED.lock().unwrap().get(path)
135 && *m == modified
136 {
137 return Ok(ids.clone());
138 }
139 let quiet: dre_protocol::host::LogSink = std::sync::Arc::new(|_, _| {});
140 let p = dre_protocol::host::PluginProcess::start(path, quiet)
141 .map_err(|e| format!("can't ask {} what it provides: {e}", path.display()))?;
142 let ids = p.info().provides.clone();
143 let _ = p.close();
144 ASKED
145 .lock()
146 .unwrap()
147 .insert(path.to_path_buf(), (modified, ids.clone()));
148 Ok(ids)
149}
150
151pub fn discover(dir: &Path) -> Vec<InstalledPackage> {
153 scan(dir, None)
154}
155
156fn scan(dir: &Path, only: Option<&str>) -> Vec<InstalledPackage> {
158 let mut out = Vec::new();
159 let Ok(entries) = std::fs::read_dir(dir) else {
160 return out;
161 };
162 let mut entries: Vec<_> = entries.filter_map(Result::ok).map(|e| e.path()).collect();
163 entries.sort();
164 for p in &entries {
165 let file = p.file_name().unwrap_or_default().to_string_lossy();
166 let name =
167 parse_package_executable_name(&file).or_else(|| parse_executable_name(&file).map(|(_, n)| n));
168 if let Some(name) = name
169 && only.is_none_or(|o| o == name)
170 && is_executable(p)
171 {
172 out.push(InstalledPackage {
173 name,
174 version: None,
175 path: p.clone(),
176 provides: probe(p).unwrap_or_default(),
177 });
178 }
179 }
180 for pdir in entries.iter().filter(|p| p.is_dir()) {
181 let name = pdir.file_name().unwrap_or_default().to_string_lossy().to_string();
182 if !dre_protocol::valid_name(&name) || only.is_some_and(|o| o != name) {
183 continue;
184 }
185 let Ok(versions) = std::fs::read_dir(pdir) else {
186 continue;
187 };
188 let mut found: Vec<(Version, InstalledPackage)> = versions
189 .filter_map(Result::ok)
190 .filter_map(|v| {
191 let ver = Version::parse(&v.file_name().to_string_lossy()).ok()?;
192 let m = read_manifest(&v.path())?;
193 let exe = v.path().join(&m.executable);
194 is_executable(&exe).then(|| {
195 (
196 ver.clone(),
197 InstalledPackage {
198 name: name.clone(),
199 version: Some(ver),
200 path: exe,
201 provides: m.provides,
202 },
203 )
204 })
205 })
206 .collect();
207 found.sort_by(|a, b| crate::manager::version_order(&a.0, &b.0));
208 out.extend(found.into_iter().map(|(_, p)| p));
209 }
210 out
211}
212
213pub fn find(
217 dir: &Path,
218 name: &str,
219 req: Option<&VersionReq>,
220 pin: Option<&Version>,
221) -> Vec<InstalledPackage> {
222 let all = scan(dir, Some(name));
223 let flat = || {
224 all.iter()
225 .filter(|p| p.version.is_none())
226 .cloned()
227 .collect::<Vec<_>>()
228 };
229 if let Some(pin) = pin {
230 if let Some(p) = all.iter().find(|p| p.version.as_ref() == Some(pin)) {
231 return vec![p.clone()];
232 }
233 return flat();
234 }
235 let versioned: Vec<(&InstalledPackage, &Version)> = all
236 .iter()
237 .filter_map(|p| p.version.as_ref().map(|v| (p, v)))
238 .collect();
239 match crate::manager::prefer_stable(versioned, req.unwrap_or(&VersionReq::STAR), |(_, v)| v) {
240 Some((p, _)) => vec![p.clone()],
241 None => flat(),
242 }
243}
244
245pub fn version_dir(dir: &Path, package: &str, version: &Version) -> PathBuf {
247 dir.join(package).join(version.to_string())
248}
249
250pub fn legacy_executable(provides: &[PluginId]) -> Option<String> {
253 match provides {
254 [one] => Some(executable_name(one.kind, &one.name)),
255 _ => None,
256 }
257}
258
259#[derive(Debug, Clone, PartialEq, Eq)]
261pub struct Located {
262 pub path: PathBuf,
263 pub plugin: PluginId,
264 pub package: String,
266}
267
268impl Located {
269 pub fn start(
271 &self,
272 log: dre_protocol::host::LogSink,
273 cwd: Option<&Path>,
274 ) -> dre_protocol::host::Result<dre_protocol::host::PluginProcess> {
275 dre_protocol::host::PluginProcess::start_for(&self.path, Some(&self.plugin), log, cwd)
276 }
277}
278
279fn installed(
282 root: &Path,
283 lock: &crate::lock::Lock,
284 req: &crate::project::PluginRequirement,
285) -> Result<Vec<InstalledPackage>, String> {
286 use crate::project::PluginSource;
287 if let PluginSource::Local(p) = &req.source {
288 let path = root.join(p);
289 if !path.is_file() {
290 return Err(format!(
291 "the plugin package `{}` is declared `local: {p}`, but there's no file at {}",
292 req.name,
293 path.display()
294 ));
295 }
296 return Ok(vec![InstalledPackage {
297 name: req.name.clone(),
298 version: None,
299 provides: probe(&path)?,
300 path,
301 }]);
302 }
303 let pin = lock
304 .get(&req.name)
305 .filter(|l| l.from == req.source.lock_key())
306 .map(|l| l.version.clone());
307 let dir = plugins_dir(Some(root));
308 Ok(find(&dir, &req.name, Some(&req.req()), pin.as_ref()))
309}
310
311#[derive(Debug, Clone, PartialEq, Eq)]
313pub enum LocateError {
314 NotProvided(String),
316 Unavailable(String),
318}
319
320impl std::fmt::Display for LocateError {
321 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
322 match self {
323 LocateError::NotProvided(m) | LocateError::Unavailable(m) => f.write_str(m),
324 }
325 }
326}
327
328impl From<LocateError> for String {
329 fn from(e: LocateError) -> String {
330 e.to_string()
331 }
332}
333
334pub fn locate(project: &crate::project::Project, id: &PluginId) -> Result<Located, LocateError> {
336 locate_in(&project.root, &project.plugins, id)
337}
338
339pub fn locate_in(
341 root: &Path,
342 plugins: &[crate::project::PluginRequirement],
343 id: &PluginId,
344) -> Result<Located, LocateError> {
345 let lock = crate::lock::Lock::load(root).unwrap_or_default();
346 let mut missing = Vec::new();
347 for req in plugins {
348 let found = installed(root, &lock, req).map_err(LocateError::Unavailable)?;
349 if found.is_empty() {
350 missing.push(format!("`{}`", req.name));
351 }
352 if let Some(p) = found.into_iter().find(|p| p.provides(id)) {
353 return Ok(Located {
354 path: p.path,
355 plugin: id.clone(),
356 package: req.name.clone(),
357 });
358 }
359 }
360 let what = format!("the {} plugin `{}`", id.kind, id.name);
361 Err(if missing.is_empty() {
362 LocateError::NotProvided(format!("no plugin package the project declares provides {what}"))
363 } else {
364 LocateError::Unavailable(format!(
365 "{what} isn't installed: the plugin package{} {} {} (looked in {}); run `dre deps` to install the project's plugins",
366 if missing.len() == 1 { "" } else { "s" },
367 missing.join(", "),
368 if missing.len() == 1 {
369 "isn't installed"
370 } else {
371 "aren't installed"
372 },
373 plugins_dir(Some(root)).display()
374 ))
375 })
376}
377
378pub fn check_uses(project: &crate::project::Project, diags: &mut crate::Diagnostics) {
382 let lock = crate::lock::Lock::load(&project.root).unwrap_or_default();
383 let mut known: Vec<PluginId> = Vec::new();
384 let mut unknown = project.plugins_incomplete;
386 for req in &project.plugins {
387 let found = installed(&project.root, &lock, req).unwrap_or_default();
388 let from_lock = lock
389 .get(&req.name)
390 .filter(|l| l.from == req.source.lock_key())
391 .map(|l| l.provides.clone())
392 .unwrap_or_default();
393 if found.is_empty() && from_lock.is_empty() {
394 unknown = true;
395 }
396 known.extend(found.into_iter().flat_map(|p| p.provides));
397 known.extend(from_lock);
398 }
399 for u in &project.plugin_uses {
400 if known.contains(&u.plugin) || unknown {
401 continue;
402 }
403 let PluginId { kind, name } = &u.plugin;
404 diags.push(crate::Diagnostic {
405 severity: crate::Severity::Error,
406 code: Code::UndeclaredPlugin,
407 message: format!(
408 "{}, but no plugin package the project declares provides it — add the package with the `{name}` {kind} under `plugins:` in dependencies.yml, then run `dre deps`",
409 u.what
410 ),
411 file: u.file.clone(),
412 line: u.line,
413 plugin: Some((*kind, name.clone())),
414 });
415 }
416}