Skip to main content

cargo/workspace/
package_id.rs

1use cargo_util::collections::HashSet;
2use std::fmt::{self, Formatter};
3use std::hash;
4use std::hash::Hash;
5use std::path::Path;
6use std::ptr;
7use std::sync::Mutex;
8use std::sync::OnceLock;
9
10use serde::de;
11use serde::ser;
12
13use crate::util::CargoResult;
14use crate::util::interning::InternedString;
15use crate::workspace::PackageIdSpec;
16use crate::workspace::SourceId;
17
18static PACKAGE_ID_CACHE: OnceLock<Mutex<HashSet<&'static PackageIdInner>>> = OnceLock::new();
19
20/// Identifier for a specific version of a package in a specific source.
21#[derive(Clone, Copy, Eq, PartialOrd, Ord)]
22pub struct PackageId {
23    inner: &'static PackageIdInner,
24}
25
26#[derive(PartialOrd, Eq, Ord)]
27struct PackageIdInner {
28    name: InternedString,
29    version: semver::Version,
30    source_id: SourceId,
31}
32
33// Custom equality that uses full equality of SourceId, rather than its custom equality.
34//
35// The `build` part of the version is usually ignored (like a "comment").
36// However, there are some cases where it is important. The download path from
37// a registry includes the build metadata, and Cargo uses PackageIds for
38// creating download paths. Including it here prevents the PackageId interner
39// from getting poisoned with PackageIds where that build metadata is missing.
40impl PartialEq for PackageIdInner {
41    fn eq(&self, other: &Self) -> bool {
42        self.name == other.name
43            && self.version == other.version
44            && self.source_id.full_eq(other.source_id)
45    }
46}
47
48// Custom hash that is coherent with the custom equality above.
49impl Hash for PackageIdInner {
50    fn hash<S: hash::Hasher>(&self, into: &mut S) {
51        self.name.hash(into);
52        self.version.hash(into);
53        self.source_id.full_hash(into);
54    }
55}
56
57impl ser::Serialize for PackageId {
58    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
59    where
60        S: ser::Serializer,
61    {
62        s.collect_str(&format_args!(
63            "{} {} ({})",
64            self.inner.name,
65            self.inner.version,
66            self.inner.source_id.as_url()
67        ))
68    }
69}
70
71impl<'de> de::Deserialize<'de> for PackageId {
72    fn deserialize<D>(d: D) -> Result<PackageId, D::Error>
73    where
74        D: de::Deserializer<'de>,
75    {
76        let string = String::deserialize(d)?;
77
78        let (field, rest) = string
79            .split_once(' ')
80            .ok_or_else(|| de::Error::custom("invalid serialized PackageId"))?;
81        let name = field.into();
82
83        let (field, rest) = rest
84            .split_once(' ')
85            .ok_or_else(|| de::Error::custom("invalid serialized PackageId"))?;
86        let version = field.parse().map_err(de::Error::custom)?;
87
88        let url = rest
89            .strip_circumfix('(', ')')
90            .ok_or_else(|| de::Error::custom("invalid serialized PackageId"))?;
91        let source_id = SourceId::from_url(url).map_err(de::Error::custom)?;
92
93        Ok(PackageId::new(name, version, source_id))
94    }
95}
96
97impl PartialEq for PackageId {
98    fn eq(&self, other: &PackageId) -> bool {
99        if ptr::eq(self.inner, other.inner) {
100            return true;
101        }
102        // This is here so that PackageId uses SourceId's and Version's idea
103        // of equality. PackageIdInner uses a more exact notion of equality.
104        self.inner.name == other.inner.name
105            && self.inner.version == other.inner.version
106            && self.inner.source_id == other.inner.source_id
107    }
108}
109
110impl Hash for PackageId {
111    fn hash<S: hash::Hasher>(&self, state: &mut S) {
112        // This is here (instead of derived) so that PackageId uses SourceId's
113        // and Version's idea of equality. PackageIdInner uses a more exact
114        // notion of hashing.
115        self.inner.name.hash(state);
116        self.inner.version.hash(state);
117        self.inner.source_id.hash(state);
118    }
119}
120
121impl PackageId {
122    pub fn try_new(
123        name: impl Into<InternedString>,
124        version: &str,
125        sid: SourceId,
126    ) -> CargoResult<PackageId> {
127        let v = version.parse()?;
128        Ok(PackageId::new(name.into(), v, sid))
129    }
130
131    pub fn new(name: InternedString, version: semver::Version, source_id: SourceId) -> PackageId {
132        let inner = PackageIdInner {
133            name,
134            version,
135            source_id,
136        };
137        let mut cache = PACKAGE_ID_CACHE
138            .get_or_init(|| Default::default())
139            .lock()
140            .unwrap();
141        let inner = cache.get(&inner).cloned().unwrap_or_else(|| {
142            let inner = Box::leak(Box::new(inner));
143            cache.insert(inner);
144            inner
145        });
146        PackageId { inner }
147    }
148
149    pub fn name(self) -> InternedString {
150        self.inner.name
151    }
152    pub fn version(self) -> &'static semver::Version {
153        &self.inner.version
154    }
155    pub fn source_id(self) -> SourceId {
156        self.inner.source_id
157    }
158
159    pub fn with_source_id(self, source: SourceId) -> PackageId {
160        PackageId::new(self.inner.name, self.inner.version.clone(), source)
161    }
162
163    pub fn map_source(self, to_replace: SourceId, replace_with: SourceId) -> Self {
164        if self.source_id() == to_replace {
165            self.with_source_id(replace_with)
166        } else {
167            self
168        }
169    }
170
171    /// Returns a value that implements a "stable" hashable value.
172    ///
173    /// Stable hashing removes the path prefix of the workspace from path
174    /// packages. This helps with reproducible builds, since this hash is part
175    /// of the symbol metadata, and we don't want the absolute path where the
176    /// build is performed to affect the binary output.
177    pub fn stable_hash(self, workspace: &Path, trim_paths: bool) -> PackageIdStableHash<'_> {
178        PackageIdStableHash {
179            id: self,
180            workspace,
181            trim_paths,
182        }
183    }
184
185    /// Filename of the `.crate` tarball, e.g., `once_cell-1.18.0.crate`.
186    pub fn tarball_name(&self) -> String {
187        format!("{}-{}.crate", self.name(), self.version())
188    }
189
190    /// Convert a `PackageId` to a `PackageIdSpec`, which will have both the `PartialVersion` and `Url`
191    /// fields filled in.
192    pub fn to_spec(&self) -> PackageIdSpec {
193        PackageIdSpec::new(String::from(self.name().as_str()))
194            .with_version(self.version().clone().into())
195            .with_url(self.source_id().url().clone())
196            .with_kind(self.source_id().kind().clone())
197    }
198}
199
200pub struct PackageIdStableHash<'a> {
201    id: PackageId,
202    /// Workspace root path.
203    workspace: &'a Path,
204    /// Whether trim-paths profile is enabled
205    /// i.e., paths are trimmed in rustc output artifacts.
206    trim_paths: bool,
207}
208
209impl<'a> Hash for PackageIdStableHash<'a> {
210    fn hash<S: hash::Hasher>(&self, state: &mut S) {
211        self.id.inner.name.hash(state);
212        self.id.inner.version.hash(state);
213        self.id
214            .inner
215            .source_id
216            .stable_hash(self.workspace, self.trim_paths, state);
217    }
218}
219
220impl fmt::Display for PackageId {
221    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
222        write!(f, "{} v{}", self.inner.name, self.inner.version)?;
223
224        if !self.inner.source_id.is_crates_io() {
225            write!(f, " ({})", self.inner.source_id)?;
226        }
227
228        Ok(())
229    }
230}
231
232impl fmt::Debug for PackageId {
233    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
234        f.debug_struct("PackageId")
235            .field("name", &self.inner.name)
236            .field("version", &self.inner.version.to_string())
237            .field("source", &self.inner.source_id.to_string())
238            .finish()
239    }
240}
241
242impl fmt::Debug for PackageIdInner {
243    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
244        f.debug_struct("PackageIdInner")
245            .field("name", &self.name)
246            .field("version", &self.version.to_string())
247            .field("source", &self.source_id.to_string())
248            .finish()
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::PackageId;
255    use crate::sources::CRATES_IO_INDEX;
256    use crate::util::IntoUrl;
257    use crate::workspace::SourceId;
258
259    #[test]
260    fn invalid_version_handled_nicely() {
261        let loc = CRATES_IO_INDEX.into_url().unwrap();
262        let repo = SourceId::for_registry(&loc).unwrap();
263
264        assert!(PackageId::try_new("foo", "1.0", repo).is_err());
265        assert!(PackageId::try_new("foo", "1", repo).is_err());
266        assert!(PackageId::try_new("foo", "bar", repo).is_err());
267        assert!(PackageId::try_new("foo", "", repo).is_err());
268    }
269
270    #[test]
271    fn display() {
272        let loc = CRATES_IO_INDEX.into_url().unwrap();
273        let pkg_id =
274            PackageId::try_new("foo", "1.0.0", SourceId::for_registry(&loc).unwrap()).unwrap();
275        assert_eq!("foo v1.0.0", pkg_id.to_string());
276    }
277
278    #[test]
279    fn unequal_build_metadata() {
280        let loc = CRATES_IO_INDEX.into_url().unwrap();
281        let repo = SourceId::for_registry(&loc).unwrap();
282        let first = PackageId::try_new("foo", "0.0.1+first", repo).unwrap();
283        let second = PackageId::try_new("foo", "0.0.1+second", repo).unwrap();
284        assert_ne!(first, second);
285        assert_ne!(first.inner, second.inner);
286    }
287}