Skip to main content

release_kit/depend/
target.rs

1//! What the target manages its development tools with.
2//!
3//! The managers and their detection live on the self-depend manager
4//! axis, so `rk depend` and `rk self-depend` read one list through one
5//! sweep. This observation adds what `rk depend` alone needs: the
6//! technology, the `.envrc` load, and where the dependency's name already
7//! appears. It judges nothing else.
8
9use camino::{Utf8Path, Utf8PathBuf};
10use serde::Serialize;
11
12use super::{Manager, canonical_dir};
13use crate::error::RkError;
14pub use crate::self_depend::manager::{MISE_FILES, ManagerFile, first_mention, manager_files};
15
16/// Where a manager file already names the dependency.
17#[derive(Debug, Clone, Serialize)]
18pub struct Already {
19    /// The manager.
20    pub manager: Manager,
21    /// The file, relative to the target.
22    pub file: String,
23    /// The first line naming it, 1-based.
24    pub line: usize,
25}
26
27/// Everything the offline pass reads from the target.
28#[derive(Debug, Clone)]
29pub struct Target {
30    /// The target, canonical.
31    pub path: Utf8PathBuf,
32    /// `rust`, `python`, `node`, or `bash`, where a manifest says.
33    pub tech: Option<&'static str>,
34    /// The managers present, in the closed order.
35    pub managers: Vec<ManagerFile>,
36    /// Whether `.envrc` carries `use flake`.
37    pub envrc_use_flake: bool,
38    /// Where the dependency is already named.
39    pub already: Vec<Already>,
40}
41
42impl Target {
43    /// The file a manager declares itself in, where present.
44    #[must_use]
45    pub fn file_of(&self, manager: Manager) -> Option<&ManagerFile> {
46        self.managers.iter().find(|m| m.manager == manager)
47    }
48
49    /// The default file a manager is seeded into when absent.
50    #[must_use]
51    pub const fn default_file(manager: Manager) -> &'static str {
52        manager.default_file()
53    }
54}
55
56/// Read the target, offline.
57///
58/// # Errors
59///
60/// Returns [`RkError::Missing`] for a path that is not a directory and
61/// [`RkError::Io`] where a present file does not read.
62pub fn observe(path: &Utf8Path, dep_name: Option<&str>) -> Result<Target, RkError> {
63    let path = canonical_dir(path, "target")?;
64    let managers = manager_files(&path, dep_name)?;
65    let envrc_use_flake = match std::fs::read_to_string(path.join(".envrc")) {
66        Ok(text) => text.lines().any(|line| {
67            let mut words = line.split_whitespace();
68            words.next() == Some("use") && words.next() == Some("flake")
69        }),
70        Err(e) if e.kind() == std::io::ErrorKind::NotFound => false,
71        Err(e) => return Err(RkError::Io(e)),
72    };
73    let already = dep_name
74        .map(|name| {
75            managers
76                .iter()
77                .filter_map(|m| {
78                    first_mention(&m.text, name).map(|line| Already {
79                        manager: m.manager,
80                        file: m.file.clone(),
81                        line,
82                    })
83                })
84                .collect()
85        })
86        .unwrap_or_default();
87    Ok(Target {
88        tech: tech_or_node(&path),
89        path,
90        managers,
91        envrc_use_flake,
92        already,
93    })
94}
95
96/// The technology, with `node` read from `package.json` after the
97/// version files the bindings define.
98#[must_use]
99pub fn tech_or_node(dir: &Utf8Path) -> Option<&'static str> {
100    crate::detect::tech_of(dir.as_std_path())
101        .or_else(|| dir.join("package.json").is_file().then_some("node"))
102}
103
104#[cfg(test)]
105mod tests {
106    use super::{Manager, Target, tech_or_node};
107
108    #[test]
109    fn the_default_file_is_the_managers_own() {
110        for manager in Manager::ALL {
111            assert_eq!(Target::default_file(manager), manager.default_file());
112        }
113    }
114
115    #[test]
116    fn node_is_read_from_package_json_after_the_version_files() {
117        let dir = tempfile::tempdir().expect("a scratch dir");
118        let root = camino::Utf8Path::from_path(dir.path()).expect("utf-8");
119        assert_eq!(tech_or_node(root), None);
120        std::fs::write(root.join("package.json"), "{}").expect("writes");
121        assert_eq!(tech_or_node(root), Some("node"));
122        std::fs::write(root.join("Cargo.toml"), "").expect("writes");
123        assert_eq!(tech_or_node(root), Some("rust"));
124    }
125}