cargo/sources/registry/local.rs
1//! Access to a registry on the local filesystem. See [`LocalRegistry`] for more.
2
3use crate::core::PackageId;
4use crate::sources::registry::{LoadResponse, MaybeLock, RegistryConfig, RegistryData};
5use crate::util::errors::CargoResult;
6use crate::util::{Filesystem, GlobalContext};
7use cargo_util::{Sha256, paths};
8use std::cell::Cell;
9use std::fs::File;
10use std::io::SeekFrom;
11use std::io::{self, prelude::*};
12use std::path::Path;
13
14/// A local registry is a registry that lives on the filesystem as a set of
15/// `.crate` files with an `index` directory in the [same format] as a remote
16/// registry.
17///
18/// This type is primarily accessed through the [`RegistryData`] trait.
19///
20/// When a local registry is requested for a package, it simply looks into what
21/// its index has under the `index` directory. When [`LocalRegistry::download`]
22/// is called, a local registry verifies the checksum of the requested `.crate`
23/// tarball and then unpacks it to `$CARGO_HOME/.registry/src`.
24///
25/// > Note that there is a third-party subcommand [`cargo-local-registry`],
26/// > which happened to be developed by a former Cargo team member when local
27/// > registry was introduced. The tool is to ease the burden of maintaining
28/// > local registries. However, in general the Cargo team avoids recommending
29/// > any specific third-party crate. Just FYI.
30///
31/// [same format]: super#the-format-of-the-index
32/// [`cargo-local-registry`]: https://crates.io/crates/cargo-local-registry
33///
34/// # Filesystem hierarchy
35///
36/// Here is an example layout of a local registry on a local filesystem:
37///
38/// ```text
39/// [registry root]/
40/// ├── index/ # registry index
41/// │ ├── an/
42/// │ │ └── yh/
43/// │ │ └── anyhow
44/// │ ├── ru/
45/// │ │ └── st/
46/// │ │ ├── rustls
47/// │ │ └── rustls-ffi
48/// │ └── se/
49/// │ └── mv/
50/// │ └── semver
51/// ├── anyhow-1.0.71.crate # pre-downloaded crate tarballs
52/// ├── rustls-0.20.8.crate
53/// ├── rustls-ffi-0.8.2.crate
54/// └── semver-1.0.17.crate
55/// ```
56///
57/// For general concepts of registries, see the [module-level documentation](crate::sources::registry).
58pub struct LocalRegistry<'gctx> {
59 /// Path to the registry index.
60 index_path: Filesystem,
61 /// Root path of this local registry.
62 root: Filesystem,
63 /// Path where this local registry extract `.crate` tarballs to.
64 src_path: Filesystem,
65 gctx: &'gctx GlobalContext,
66 /// Whether this source has updated all package information it may contain.
67 updated: Cell<bool>,
68 /// Disables status messages.
69 quiet: bool,
70}
71
72impl<'gctx> LocalRegistry<'gctx> {
73 /// Creates a local registry at `root`.
74 ///
75 /// * `name` --- Name of a path segment where `.crate` tarballs are stored.
76 /// Expect to be unique.
77 pub fn new(root: &Path, gctx: &'gctx GlobalContext, name: &str) -> LocalRegistry<'gctx> {
78 LocalRegistry {
79 src_path: gctx.registry_source_path().join(name),
80 index_path: Filesystem::new(root.join("index")),
81 root: Filesystem::new(root.to_path_buf()),
82 gctx,
83 updated: Cell::new(false),
84 quiet: false,
85 }
86 }
87
88 fn update(&self) -> CargoResult<()> {
89 if self.updated.get() {
90 return Ok(());
91 }
92 // Nothing to update, we just use what's on disk. Verify it actually
93 // exists though. We don't use any locks as we're just checking whether
94 // these directories exist.
95 let root = self.root.clone().into_path_unlocked();
96 if !root.is_dir() {
97 anyhow::bail!("local registry path is not a directory: {}", root.display());
98 }
99 let index_path = self.index_path.clone().into_path_unlocked();
100 if !index_path.is_dir() {
101 anyhow::bail!(
102 "local registry index path is not a directory: {}",
103 index_path.display()
104 );
105 }
106 self.updated.set(true);
107 Ok(())
108 }
109}
110
111#[async_trait::async_trait(?Send)]
112impl<'gctx> RegistryData for LocalRegistry<'gctx> {
113 fn prepare(&self) -> CargoResult<()> {
114 Ok(())
115 }
116
117 fn index_path(&self) -> &Filesystem {
118 &self.index_path
119 }
120
121 fn cache_path(&self) -> &Filesystem {
122 &self.root
123 }
124
125 fn assert_index_locked<'a>(&self, path: &'a Filesystem) -> &'a Path {
126 // Note that the `*_unlocked` variant is used here since we're not
127 // modifying the index and it's required to be externally synchronized.
128 path.as_path_unlocked()
129 }
130
131 async fn load(
132 &self,
133 root: &Path,
134 path: &Path,
135 _index_version: Option<&str>,
136 ) -> CargoResult<LoadResponse> {
137 if !self.updated.get() {
138 self.update()?;
139 }
140 let raw_data = match paths::read_bytes(&root.join(path)) {
141 Err(e)
142 if e.downcast_ref::<io::Error>()
143 .map_or(false, |ioe| ioe.kind() == io::ErrorKind::NotFound) =>
144 {
145 return Ok(LoadResponse::NotFound);
146 }
147 r => r,
148 }?;
149 Ok(LoadResponse::Data {
150 raw_data,
151 index_version: None,
152 })
153 }
154
155 async fn config(&self) -> CargoResult<Option<RegistryConfig>> {
156 // Local registries don't have configuration for remote APIs or anything
157 // like that
158 Ok(None)
159 }
160
161 fn invalidate_cache(&self) {
162 // Local registry has no cache - just reads from disk.
163 }
164
165 fn set_quiet(&mut self, _quiet: bool) {
166 self.quiet = true;
167 }
168
169 fn is_updated(&self) -> bool {
170 // There is nothing to update.
171 true
172 }
173
174 fn download(&self, pkg: PackageId, checksum: &str) -> CargoResult<MaybeLock> {
175 // Note that the usage of `into_path_unlocked` here is because the local
176 // crate files here never change in that we're not the one writing them,
177 // so it's not our responsibility to synchronize access to them.
178 let path = self.root.join(&pkg.tarball_name()).into_path_unlocked();
179 let mut crate_file = paths::open(&path)?;
180
181 // If we've already got an unpacked version of this crate, then skip the
182 // checksum below as it is in theory already verified.
183 let dst = path.file_stem().unwrap();
184 if self.src_path.join(dst).into_path_unlocked().exists() {
185 return Ok(MaybeLock::Ready(crate_file));
186 }
187
188 if !self.quiet {
189 self.gctx.shell().status("Unpacking", pkg)?;
190 }
191
192 // We don't actually need to download anything per-se, we just need to
193 // verify the checksum matches the .crate file itself.
194 let actual = Sha256::new().update_file(&crate_file)?.finish_hex();
195 if actual != checksum {
196 anyhow::bail!("failed to verify the checksum of `{}`", pkg)
197 }
198
199 crate_file.seek(SeekFrom::Start(0))?;
200
201 Ok(MaybeLock::Ready(crate_file))
202 }
203
204 fn finish_download(&self, _pkg: PackageId, _checksum: &str, _data: &[u8]) -> CargoResult<File> {
205 panic!("this source doesn't download")
206 }
207}