1use std::{collections::HashMap, sync::Arc};
2
3use crate::{
4 config::{Config, ConfigError},
5 lockfile::{LockfileIntegrityError, PackageLock},
6 lua_rockspec::{RemoteLuaRockspec, RockSourceSpec},
7 manifest::{Manifest, ManifestError},
8 manifest_cache::{Closure, ClosureSpec, ManifestCache},
9 operations::{Download, FetchVendored, PackageInstallSpec, RemoteRockDownload},
10 package::{
11 PackageName, PackageReq, PackageSpec, PackageVersion, RemotePackage, RemotePackageType,
12 RemotePackageTypeFilterSpec,
13 },
14 pipeline::{
15 discover::{DiscoverError, FoundPackage},
16 materialise::PackageSource,
17 },
18 project::Project,
19 remote_package_source::RemotePackageSource,
20 workspace::{Workspace, WorkspaceError},
21};
22use itertools::Itertools;
23use tokio::sync::{Mutex, OnceCell};
24use url::Url;
25
26use miette::Diagnostic;
27use thiserror::Error;
28
29#[derive(Clone, Debug)]
30pub struct PackageDB {
31 providers: Vec<PackageProvider>,
32}
33
34#[derive(Error, Debug, Diagnostic)]
35pub enum PackageDBError {
36 #[error(transparent)]
37 #[diagnostic(transparent)]
38 ManifestError(#[from] ManifestError),
39 #[error(transparent)]
40 #[diagnostic(transparent)]
41 ConfigError(#[from] ConfigError),
42 #[error(transparent)]
43 #[diagnostic(transparent)]
44 Workspace(Box<WorkspaceError>),
45}
46
47impl From<WorkspaceError> for PackageDBError {
48 fn from(err: WorkspaceError) -> Self {
49 Self::Workspace(Box::new(err))
50 }
51}
52
53#[derive(Error, Debug, Diagnostic)]
54#[non_exhaustive]
55pub enum SearchError {
56 #[error("no rock that matches '{0}' found in the lockfile or in the manifests")]
57 RockNotFound(PackageReq),
58 #[error("error when pulling manifest")]
59 #[diagnostic(forward(0))]
60 Manifest(#[from] ManifestError),
61}
62
63#[derive(Error, Debug, Diagnostic)]
64#[non_exhaustive]
65pub enum RemotePackageDbIntegrityError {
66 #[error(transparent)]
67 #[diagnostic(transparent)]
68 Lockfile(#[from] LockfileIntegrityError),
69}
70
71#[derive(Clone, Debug)]
73pub(crate) enum PackageProvider {
74 Local(Vec<FoundPackage>),
76 Lockfile(Vec<PackageLock>),
78 Manifest(ManifestProvider),
80 Closure(ClosureProvider),
82}
83
84#[derive(Clone, Debug)]
86pub(crate) struct ManifestProvider {
87 server_url: Url,
88 config: Box<Config>,
89 manifest: Arc<OnceCell<Manifest>>,
90}
91
92impl ManifestProvider {
93 fn new(server_url: Url, config: &Config) -> Self {
94 Self {
95 server_url,
96 config: Box::new(config.clone()),
97 manifest: Arc::new(OnceCell::new()),
98 }
99 }
100
101 #[cfg(test)]
102 fn from_manifest(manifest: Manifest, config: &Config) -> Self {
103 Self {
104 server_url: manifest.server_url().clone(),
105 config: Box::new(config.clone()),
106 manifest: Arc::new(OnceCell::from(manifest)),
107 }
108 }
109
110 async fn get(&self) -> Result<&Manifest, ManifestError> {
111 let manifest = self
112 .manifest
113 .get_or_try_init(|| Manifest::from_config(self.server_url.clone(), &self.config));
114 Box::pin(manifest).await
115 }
116}
117
118impl PackageProvider {
119 async fn find(
120 &self,
121 package_req: &PackageReq,
122 filter: Option<RemotePackageTypeFilterSpec>,
123 ) -> Result<Option<RemotePackage>, SearchError> {
124 match self {
125 Self::Local(packages) => Ok(packages
126 .iter()
127 .find(|found| found.remote.spec.name() == package_req.name())
128 .map(|found| found.remote.clone())),
129 Self::Lockfile(locks) => Ok(locks
130 .iter()
131 .filter_map(|lock| lock.has_rock(package_req, filter.clone()))
132 .map(|locked_package| {
133 RemotePackage::new(
134 locked_package.to_package(),
135 locked_package.source().clone(),
136 locked_package.source_url().cloned(),
137 )
138 })
139 .next()),
140 Self::Manifest(manifest) => Ok(manifest.get().await?.find(package_req, filter)),
141 Self::Closure(closure) => Ok(closure.find(package_req, filter).await),
142 }
143 }
144
145 async fn resolve(
146 &self,
147 spec: &PackageInstallSpec,
148 config: &Config,
149 ) -> Result<Option<FoundPackage>, DiscoverError> {
150 match self {
151 Self::Local(packages) => Ok(packages
152 .iter()
153 .find(|package| package.remote.spec.name() == spec.package.name())
154 .cloned()),
155 Self::Closure(closure) => closure.resolve(spec, config).await,
156 Self::Lockfile(_) | Self::Manifest(_) => Ok(None),
157 }
158 }
159
160 async fn search(
161 &self,
162 package_req: &PackageReq,
163 ) -> Result<Vec<(PackageName, Vec<PackageVersion>)>, SearchError> {
164 match self {
165 Self::Local(packages) => Ok(packages
166 .iter()
167 .filter_map(|package| {
168 let name = package.remote.spec.name();
169 if name.to_string().contains(&package_req.name().to_string())
170 && package_req
171 .version_req()
172 .matches(package.remote.spec.version())
173 {
174 Some((name.clone(), vec![package.remote.spec.version().clone()]))
175 } else {
176 None
177 }
178 })
179 .collect()),
180 Self::Lockfile(locks) => Ok(locks
183 .iter()
184 .flat_map(|lock| lock.rocks().values())
185 .filter_map(|package| {
186 let name = package.name();
187 if name.to_string().contains(&package_req.name().to_string()) {
188 Some((name.clone(), vec![package.version().clone()]))
189 } else {
190 None
191 }
192 })
193 .collect()),
194 Self::Manifest(manifest) => Ok(manifest
195 .get()
196 .await?
197 .metadata()
198 .repository
199 .iter()
200 .filter_map(|(name, elements)| {
201 if name.to_string().contains(&package_req.name().to_string()) {
202 Some((
203 name.clone(),
204 elements
205 .keys()
206 .filter(|version| package_req.version_req().matches(version))
207 .sorted_by(|a, b| Ord::cmp(b, a))
208 .cloned()
209 .collect_vec(),
210 ))
211 } else {
212 None
213 }
214 })
215 .collect()),
216 Self::Closure(_) => Ok(Vec::new()),
218 }
219 }
220}
221
222#[derive(Clone, Debug)]
223pub(crate) struct ClosureProvider {
224 cache: ManifestCache,
225 rock_server: Url,
226 loaded: Arc<Mutex<HashMap<PackageName, Arc<Closure>>>>,
227}
228
229impl ClosureProvider {
230 fn new(cache: ManifestCache, config: &Config) -> Self {
231 Self {
232 cache,
233 rock_server: config.server().clone(),
234 loaded: Arc::new(Mutex::new(HashMap::new())),
235 }
236 }
237
238 async fn load_closure_for(&self, req: &PackageReq) -> Option<Arc<Closure>> {
239 if let Some(closure) = self.loaded.lock().await.get(req.name()) {
240 return Some(closure.clone());
241 }
242
243 let versions = self.cache.versions(req.name()).await.ok()?;
244 let chosen = versions
245 .iter()
246 .find(|version| req.version_req().matches(version))?;
247
248 let is_latest = versions.first().is_some_and(|latest| latest == chosen);
249 let closure = if is_latest {
250 self.cache.latest_closure(req.name()).await.ok()?
251 } else {
252 self.cache.closure(req.name(), chosen).await.ok()?
253 };
254 let closure = Arc::new(closure);
255
256 self.loaded
257 .lock()
258 .await
259 .insert(req.name().clone(), closure.clone());
260 Some(closure)
261 }
262
263 fn remote_package(&self, spec: &ClosureSpec, package_type: RemotePackageType) -> RemotePackage {
264 let source = match package_type {
265 RemotePackageType::Rockspec => {
266 RemotePackageSource::LuarocksRockspec(self.rock_server.clone())
267 }
268 RemotePackageType::Src => {
269 RemotePackageSource::LuarocksSrcRock(self.rock_server.clone())
270 }
271 RemotePackageType::Binary => {
272 RemotePackageSource::LuarocksBinaryRock(self.rock_server.clone())
273 }
274 };
275 RemotePackage::new(
276 PackageSpec::new(spec.package().clone(), spec.version().clone()),
277 source,
278 None,
279 )
280 }
281
282 async fn find(
283 &self,
284 req: &PackageReq,
285 filter: Option<RemotePackageTypeFilterSpec>,
286 ) -> Option<RemotePackage> {
287 let closure = self.load_closure_for(req).await?;
288 let (spec, package_type) = closure.spec(req, filter.as_ref())?;
289 Some(self.remote_package(spec, package_type))
290 }
291
292 async fn resolve(
293 &self,
294 spec: &PackageInstallSpec,
295 _config: &Config,
296 ) -> Result<Option<FoundPackage>, DiscoverError> {
297 let Some(closure) = self.load_closure_for(&spec.package).await else {
298 return Ok(None);
299 };
300 let Some((closure_spec, package_type)) = closure.spec(&spec.package, None) else {
301 return Ok(None);
302 };
303 if package_type != RemotePackageType::Rockspec {
307 return Ok(None);
308 }
309 let rockspec = RemoteLuaRockspec::new(closure_spec.rockspec_raw()).map_err(|err| {
310 DiscoverError::Download(
311 spec.package.clone(),
312 Box::new(crate::operations::SearchAndDownloadError::Rockspec(
313 Box::new(err),
314 )),
315 )
316 })?;
317 let remote = self.remote_package(closure_spec, package_type);
318 Ok(Some(FoundPackage {
319 remote,
320 rockspec,
321 artifact: None,
322 }))
323 }
324}
325
326impl PackageDB {
327 #[tracing::instrument(level = "trace", skip_all)]
328 pub async fn from_config(config: &Config) -> Result<Self, PackageDBError> {
329 let mut providers = Vec::new();
330 if let Some(cache) = ManifestCache::from_config(config) {
331 providers.push(PackageProvider::Closure(ClosureProvider::new(
332 cache, config,
333 )));
334 }
335 for server in config.enabled_dev_servers()? {
336 providers.push(PackageProvider::Manifest(ManifestProvider::new(
337 server, config,
338 )));
339 }
340 for server in config.extra_servers() {
341 providers.push(PackageProvider::Manifest(ManifestProvider::new(
342 server.clone(),
343 config,
344 )));
345 }
346 providers.push(PackageProvider::Manifest(ManifestProvider::new(
347 config.server().clone(),
348 config,
349 )));
350 Ok(Self { providers })
351 }
352
353 #[cfg(test)]
355 pub(crate) fn from_manifest(manifest: Manifest, config: &Config) -> Self {
356 Self {
357 providers: vec![PackageProvider::Manifest(ManifestProvider::from_manifest(
358 manifest, config,
359 ))],
360 }
361 }
362
363 pub(crate) async fn open(
366 config: &Config,
367 workspace: &Workspace,
368 ) -> Result<Self, PackageDBError> {
369 Ok(Self::from_config(config)
370 .await?
371 .with_locks(workspace.lockfile()?.local_pkg_locks()))
372 }
373
374 #[cfg(test)]
375 pub(crate) fn from_local(packages: Vec<FoundPackage>) -> Self {
376 Self {
377 providers: vec![PackageProvider::Local(packages)],
378 }
379 }
380
381 pub(crate) fn with_local(mut self, packages: Vec<FoundPackage>) -> Self {
383 self.providers
384 .retain(|provider| !matches!(provider, PackageProvider::Local(_)));
385 self.providers.insert(0, PackageProvider::Local(packages));
386 self
387 }
388
389 pub(crate) fn with_locks(mut self, locks: Vec<PackageLock>) -> Self {
391 self.providers
392 .retain(|provider| !matches!(provider, PackageProvider::Lockfile(_)));
393 let index = self
394 .providers
395 .iter()
396 .position(|provider| {
397 matches!(
398 provider,
399 PackageProvider::Closure(_) | PackageProvider::Manifest(_)
400 )
401 })
402 .unwrap_or(self.providers.len());
403 self.providers
404 .insert(index, PackageProvider::Lockfile(locks));
405 self
406 }
407
408 fn has_remote_sources(&self) -> bool {
409 self.providers.iter().any(|provider| {
410 matches!(
411 provider,
412 PackageProvider::Lockfile(_)
413 | PackageProvider::Manifest(_)
414 | PackageProvider::Closure(_)
415 )
416 })
417 }
418
419 #[tracing::instrument(
421 name = "Searching",
422 level = "info",
423 skip_all,
424 fields(package = package_req.to_string()),
425 )]
426 pub(crate) async fn find(
427 &self,
428 package_req: &PackageReq,
429 filter: Option<RemotePackageTypeFilterSpec>,
430 ) -> Result<RemotePackage, SearchError> {
431 for provider in &self.providers {
432 if let Some(package) = provider.find(package_req, filter.clone()).await? {
433 return Ok(package);
434 }
435 }
436
437 Err(SearchError::RockNotFound(package_req.clone()))
438 }
439
440 pub(crate) async fn resolve(
442 &self,
443 spec: &PackageInstallSpec,
444 config: &Config,
445 ) -> Result<FoundPackage, DiscoverError> {
446 if let Some(found) = resolve_path_project(spec)? {
447 return Ok(found);
448 }
449
450 if let Some(source) = &spec.source {
451 let download = RemoteRockDownload::from_package_req_and_source_spec(
452 spec.package.clone(),
453 source.clone(),
454 )
455 .map_err(|err| DiscoverError::Download(spec.package.clone(), Box::new(err)))?;
456 let rockspec = download.rockspec().clone();
457 let package = RemotePackage::new(
458 spec.package.clone().try_into().map_err(|err| {
459 DiscoverError::Download(
460 spec.package.clone(),
461 Box::new(crate::operations::SearchAndDownloadError::from(err)),
462 )
463 })?,
464 download.rockspec_download().source.clone(),
465 download.rockspec_download().source_url.clone(),
466 );
467 return Ok(FoundPackage {
468 remote: package,
469 rockspec,
470 artifact: None,
471 });
472 }
473
474 for provider in &self.providers {
475 if let Some(found) = provider.resolve(spec, config).await? {
478 return Ok(found);
479 }
480
481 if provider.find(&spec.package, None).await?.is_some() {
484 break;
485 }
486 }
487
488 if self.has_remote_sources() {
489 let mut package = self.find(&spec.package, None).await?;
490 let download = if let Some(vendor_dir) = config.vendor_dir() {
491 FetchVendored::new()
492 .vendor_dir(vendor_dir)
493 .package(&spec.package)
494 .package_db(self)
495 .fetch_vendored_rock()
496 .await
497 .map_err(|err| {
498 DiscoverError::FetchVendored(spec.package.clone(), Box::new(err))
499 })?
500 } else {
501 Download::new(&spec.package, config)
502 .package_db(self)
503 .download_remote_rock()
504 .await
505 .map_err(|err| DiscoverError::Download(spec.package.clone(), Box::new(err)))?
506 };
507 let rockspec = download.rockspec().clone();
508 let artifact = match &download {
509 RemoteRockDownload::SrcRock {
510 src_rock,
511 source_url,
512 ..
513 } => {
514 package.source_url = Some(source_url.clone());
515 Some(PackageSource::Archive(src_rock.clone()))
516 }
517 RemoteRockDownload::BinaryRock { packed_rock, .. } => {
518 Some(PackageSource::Archive(packed_rock.clone()))
519 }
520 RemoteRockDownload::RockspecOnly { .. } => None,
521 };
522 return Ok(FoundPackage {
523 remote: package,
524 rockspec,
525 artifact,
526 });
527 }
528
529 Err(DiscoverError::Search(SearchError::RockNotFound(
530 spec.package.clone(),
531 )))
532 }
533
534 pub async fn search(
536 &self,
537 package_req: &PackageReq,
538 ) -> Result<Vec<(PackageName, Vec<PackageVersion>)>, SearchError> {
539 let mut results = Vec::new();
540 for provider in &self.providers {
541 results.extend(provider.search(package_req).await?);
542 }
543 Ok(results)
544 }
545
546 pub(crate) async fn latest_version(&self, rock_name: &PackageName) -> Option<PackageVersion> {
548 self.latest_match(&rock_name.clone().into(), None)
549 .await
550 .map(|result| result.version().clone())
551 }
552
553 pub async fn latest_match(
555 &self,
556 package_req: &PackageReq,
557 filter: Option<RemotePackageTypeFilterSpec>,
558 ) -> Option<PackageSpec> {
559 match self.find(package_req, filter).await {
560 Ok(result) => Some(result.spec),
561 Err(_) => None,
562 }
563 }
564}
565
566impl From<Vec<PackageLock>> for PackageDB {
567 fn from(locks: Vec<PackageLock>) -> Self {
568 Self {
569 providers: vec![PackageProvider::Lockfile(locks)],
570 }
571 }
572}
573
574fn resolve_path_project(spec: &PackageInstallSpec) -> Result<Option<FoundPackage>, DiscoverError> {
577 if let Some(RockSourceSpec::File(path)) = &spec.source {
578 Ok(match Project::from_exact(path)? {
579 Some(project) if project.toml().package() == spec.package.name() => {
580 Some(FoundPackage::from_project_root(
581 project.local_remote_rockspec()?,
582 project.root().to_path_buf(),
583 ))
584 }
585 _ => None,
586 })
587 } else {
588 Ok(None)
589 }
590}
591
592#[cfg(test)]
593mod tests {
594 use super::*;
595 use crate::{
596 build::BuildBehaviour, config::ConfigBuilder, rockspec::Rockspec, tree::EntryType,
597 };
598 use std::path::PathBuf;
599
600 #[tokio::test]
602 async fn resolves_path_dependency_from_project_manifest() {
603 let bar_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
604 .join("resources/test/sample-projects/multi-project-local-deps/projects/bar");
605
606 let config = ConfigBuilder::new().unwrap().build().unwrap();
607 let spec = PackageInstallSpec::new(
608 PackageReq::new("bar".into(), Some("1.0.0".into())).unwrap(),
609 EntryType::DependencyOnly,
610 )
611 .build_behaviour(BuildBehaviour::Ignore)
612 .maybe_source(Some(RockSourceSpec::File(bar_dir)))
613 .build();
614
615 let found = PackageDB::from_local(Vec::new())
616 .resolve(&spec, &config)
617 .await
618 .unwrap();
619
620 assert!(found
621 .rockspec
622 .dependencies()
623 .current_platform()
624 .iter()
625 .any(|dependency| dependency.name().to_string() == "foo"));
626 }
627}