Skip to main content

cargo_uv/packages/
mod.rs

1#![allow(dead_code)]
2
3mod package_name;
4pub use package_name::PackageName;
5
6mod package;
7pub use package::Package;
8
9mod error;
10pub use error::PackageError;
11
12use std::io::Write;
13use std::{
14    collections::{HashMap, HashSet},
15    path::PathBuf,
16};
17
18use cargo_metadata::Metadata;
19use semver::Version;
20use tracing::{debug, instrument};
21
22use crate::{ReadToml, Result, VersionLocation, display_path};
23
24#[derive(Debug, Clone, PartialEq)]
25pub struct Packages {
26    /// File path to the root cargo.toml
27    root_manifest_path: PathBuf,
28    /// Root package of the rust project.
29    root_package: Option<PackageName>,
30
31    /// The root package version or if not present the workplace version.
32    root_version: Option<Version>,
33
34    /// Default members
35    default_members: HashSet<PackageName>,
36
37    /// Hashmap of the packages in the rust project.
38    packages: HashMap<PackageName, Package<ReadToml>>,
39
40    /// The package of the root Cargo.toml
41    workspace_package: Option<Package<ReadToml>>,
42}
43
44impl AsRef<HashMap<PackageName, Package<ReadToml>>> for Packages {
45    fn as_ref(&self) -> &HashMap<PackageName, Package<ReadToml>> {
46        &self.packages
47    }
48}
49
50impl Packages {
51    pub fn new<P, N>(
52        path: PathBuf,
53        packages: &[P],
54        root_package: Option<&P>,
55        default_members: Vec<N>,
56    ) -> Result<Self>
57    where
58        P: Into<Package<ReadToml>> + Clone,
59        N: ToString + Clone,
60    {
61        let workspace_package = Package::workspace_package(&path).ok();
62        let mut ret = Self {
63            root_manifest_path: path,
64            root_package: None,
65            root_version: None,
66            packages: HashMap::from_iter(packages.iter().map(|package| {
67                let package: Package<ReadToml> = package.clone().into();
68                (package.name().clone(), package)
69            })),
70            default_members: HashSet::from_iter(
71                default_members.iter().map(|n| PackageName(n.to_string())),
72            ),
73            workspace_package,
74        };
75        if let Some(root_package) = root_package {
76            let root_package: Package<ReadToml> = root_package.clone().into();
77            ret.root_package = ret.set_root_package_name(root_package.name()).cloned();
78            ret.root_version = Some(root_package.version().clone());
79        }
80        Ok(ret)
81    }
82
83    #[instrument()]
84    pub fn cargo_file_path(&self) -> &PathBuf {
85        debug!("Fetching cargo path");
86        &self.root_manifest_path
87    }
88    pub fn drop_root_package_name(&mut self) {
89        self.root_package = None
90    }
91
92    pub fn workspace_default_members(&self) -> HashSet<&PackageName> {
93        self.packages()
94            .keys()
95            .filter(|&p| self.default_members.contains(p))
96            .collect()
97    }
98
99    pub fn workspace_members(&self) -> HashSet<&PackageName> {
100        self.packages.keys().collect()
101    }
102
103    /// Trys to set root package name, returns [None] if package doesn't exist or an invalid name is inputed.
104    ///
105    /// Use [Packages::try_set_root_package_name] if you want to see the error.
106    pub fn set_root_package_name<'name>(
107        &mut self,
108        package_name: &'name PackageName,
109    ) -> Option<&'name PackageName> {
110        self.try_set_root_package_name(package_name).ok()
111    }
112
113    pub fn try_set_root_package_name<'name>(
114        &mut self,
115        package_name: &'name PackageName,
116    ) -> miette::Result<&'name PackageName> {
117        if package_name.is_empty() {
118            Err(PackageError::PackageNameNotProvided)?
119        }
120        match self.packages.contains_key(package_name) {
121            true => {
122                self.root_package = Some(package_name.clone());
123                Ok(package_name)
124            }
125            false => Err(PackageError::PackageNameNotFound(package_name.clone()))?,
126        }
127    }
128
129    /// Only needs a `&self` as it returns what is set.
130    pub fn root_package_name_unchecked(&self) -> Option<&PackageName> {
131        self.root_package.as_ref()
132    }
133
134    /// Validated the set root name, uses [Packages::drop_root_package_name] if root_name is invalid.
135    pub fn root_package_name(&mut self) -> Option<&PackageName> {
136        let root_name = self.root_package.clone()?;
137        if self.packages.contains_key(&root_name) {
138            self.root_package.as_ref()
139        } else {
140            self.drop_root_package_name();
141            None
142        }
143    }
144
145    /// Checks whether there is a root name and returns a ref to the [root_package][Package].
146    pub fn get_root_package(&self) -> Option<&Package<ReadToml>> {
147        let root_name = self.root_package_name_unchecked()?;
148        self.packages.get(root_name)
149    }
150
151    /// Checks whether there is a root name and returns a mut ref to the [root_package][Package].
152    pub fn get_root_package_mut(&mut self) -> Option<&mut Package<ReadToml>> {
153        let root_name = self.root_package_name_unchecked()?.clone();
154        self.packages.get_mut(&root_name)
155    }
156
157    /// Checks whether there is a root name and returns an owned clone of the [root_package][Package].
158    pub fn get_root_package_owned(&self) -> Option<Package<ReadToml>> {
159        let root_name = self.root_package_name_unchecked()?;
160        self.packages.get(root_name).cloned()
161    }
162
163    /// Uses the [HashMap::get] and returns a ref to the [Package] if it exists.
164    pub fn get_package(&self, package_name: &PackageName) -> Option<&Package<ReadToml>> {
165        self.packages.get(package_name)
166    }
167
168    ///  Uses the [HashMap::get_mut] and returns a mut ref to the [Package] if it exists.
169    pub fn get_package_mut(
170        &mut self,
171        package_name: &PackageName,
172    ) -> Option<&mut Package<ReadToml>> {
173        self.packages.get_mut(package_name)
174    }
175
176    ///  Uses the [HashMap::get] followed by [Option::cloned] and returns [Package] if it exists.
177    pub fn get_package_owned(&self, package_name: &PackageName) -> Option<Package<ReadToml>> {
178        self.packages.get(package_name).cloned()
179    }
180
181    pub fn packages(&self) -> &HashMap<PackageName, Package<ReadToml>> {
182        &self.packages
183    }
184
185    pub fn package_set(&self) -> HashSet<&Package<ReadToml>> {
186        self.packages.values().collect::<HashSet<_>>()
187    }
188
189    pub fn set_cargo_file_path(&mut self, cargo_file: PathBuf) {
190        self.root_manifest_path = cargo_file;
191    }
192
193    pub(crate) fn get_root_package_version(&self) -> Option<Version> {
194        self.get_root_package().map(|rp| rp.version().clone())
195    }
196
197    /// Determine what the root version is for the packages.
198    ///
199    /// Order of checks:
200    /// - root version is set
201    /// - root package version
202    /// - workplace.package.version
203    /// - TODO: If all versions are the same use that version
204    ///
205    pub fn root_version(&self) -> Result<Version, PackageError> {
206        let error_no_root_package = PackageError::NoRootVersion;
207        if self.root_version.is_some() {
208            return Ok(self
209                .root_version
210                .as_ref()
211                .expect("There is a root version")
212                .clone());
213        };
214
215        // Checking the root package
216        if let Some(root_package_name) = &self.root_package {
217            let root_package = self.packages.get(root_package_name);
218            if let Some(root_package) = root_package {
219                return Ok(root_package.version().clone());
220            };
221        };
222
223        if let Some(workspace_package) = &self.workspace_package {
224            // Checking the workspace package
225            match VersionLocation::WorkspacePackage.get_version(workspace_package.cargo_file()) {
226                Ok(v) => return Ok(v),
227                Err(e) => match e.kind() {
228                    crate::VersionLocationErrorKind::SetByWorkspace => (),
229                    crate::VersionLocationErrorKind::NotFound(_) => (),
230                    crate::VersionLocationErrorKind::PackageNotFound => unreachable!(),
231                    crate::VersionLocationErrorKind::WorkspaceNotFound => {
232                        todo!("Workspace Not Found")
233                    }
234                    crate::VersionLocationErrorKind::ItemInvalid(_) => (),
235                    crate::VersionLocationErrorKind::SemverError(_) => (),
236                },
237            };
238        }
239
240        Err(error_no_root_package)
241    }
242
243    pub fn workspace_package(&mut self) -> Option<&mut Package<ReadToml>> {
244        self.workspace_package.as_mut()
245    }
246}
247
248impl Packages {
249    pub fn display_tree(&self) -> String {
250        let mut ret_string = Vec::new();
251        let root_package = self.root_package.as_ref();
252        let path_base = self.cargo_file_path().parent().unwrap();
253        let make_relative = |package: &Package<ReadToml>| {
254            PathBuf::new()
255                .join(".")
256                .join(
257                    package
258                        .manifest_path()
259                        .parent()
260                        .unwrap()
261                        .strip_prefix(path_base)
262                        .unwrap(),
263                )
264                .as_os_str()
265                .to_string_lossy()
266                .into_owned()
267        };
268        let _ = writeln!(
269            ret_string,
270            "Workspace root: {}",
271            path_base.as_os_str().to_str().unwrap()
272        );
273        if let Some(root) = root_package {
274            let package = self.get_package(root).unwrap();
275            let _ = writeln!(ret_string, "Root package: {root} {}", package.version(),);
276        }
277
278        if !self.default_members.is_empty() {
279            let _ = writeln!(
280                ret_string,
281                "Default members: {:?}",
282                self.default_members
283                    .iter()
284                    .map(|n| n.to_string())
285                    .collect::<Vec<_>>()
286            );
287        }
288
289        let _ = writeln!(ret_string);
290
291        if let Some(root) = root_package {
292            let _ = writeln!(ret_string, "{root}");
293        } else {
294            let _ = writeln!(ret_string, "Members:");
295        }
296
297        let mut items = self.packages.iter().collect::<Vec<_>>();
298        items.sort_by_key(|(n, _)| n.0.as_str());
299        let last = items.last().cloned();
300
301        for (name, package) in items {
302            if Some(name) == root_package {
303                continue;
304            }
305            if Some((name, package)) == last {
306                let _ = writeln!(
307                    ret_string,
308                    "└─ {name} {}: {}",
309                    package.version(),
310                    make_relative(package)
311                );
312            } else {
313                let _ = writeln!(
314                    ret_string,
315                    "├─ {name} {}: {}",
316                    package.version(),
317                    make_relative(package)
318                );
319            }
320        }
321        String::from_utf8(ret_string).expect("Chars is valid utf-8")
322    }
323}
324impl From<&Metadata> for Packages {
325    #[track_caller]
326    #[instrument(skip_all)]
327    fn from(metadata: &Metadata) -> Self {
328        let root_path = metadata.workspace_root.join("Cargo.toml");
329        tracing::debug!(
330            "Constructing Packages from Metadata: {}",
331            display_path!(root_path)
332        );
333
334        let default_members = metadata
335            .workspace_default_packages()
336            .iter()
337            .map(|p| p.name.clone())
338            .collect::<Vec<_>>();
339        tracing::trace!("Default members {:?}", default_members);
340        Self::new(
341            root_path.into(),
342            &metadata.packages,
343            metadata.root_package(),
344            default_members,
345        )
346        .expect("From cargo metadata")
347    }
348}