Skip to main content

lux_lib/package_db/
mod.rs

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/// A source of package metadata consulted by a [`PackageDB`].
72#[derive(Clone, Debug)]
73pub(crate) enum PackageProvider {
74    /// Locally available packages, e.g. workspace members.
75    Local(Vec<FoundPackage>),
76    /// Packages pinned in the lockfile.
77    Lockfile(Vec<PackageLock>),
78    /// A luarocks manifest.
79    Manifest(ManifestProvider),
80    /// The manifest cache.
81    Closure(ClosureProvider),
82}
83
84/// A luarocks manifest that is only fetched when it is first needed.
85#[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            // NOTE: This doesn't group packages by name, but we don't care for now,
181            // as we shouldn't need to use this function with a lockfile.
182            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            // The cache only supports lookups by exact name, not substring search.
217            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        // Only rockspec-type packages can be resolved fully offline, as they have no source archive
304        // to download and hash. For src/binary rocks we fall through to the generic download step,
305        // which fetches the archive.
306        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    /// Builds a package database from a single, already loaded manifest.
354    #[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    /// Builds a package database that prefers the workspace lockfile, falling
364    /// back to the manifests for requirements the lockfile does not satisfy.
365    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    /// Sets the local packages to look up first.
382    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    /// Sets the locked packages to look up after local packages, ahead of manifests.
390    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    /// Find a remote package that matches the requirement, returning the latest match.
420    #[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    /// Resolves a package install spec into a fully discovered package.
441    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            // Providers that can serve a package without downloading it (local packages and the
476            // manifest cache) resolve early.
477            if let Some(found) = provider.resolve(spec, config).await? {
478                return Ok(found);
479            }
480
481            // Otherwise break on the first provider that has the package. The `break` prevents a
482            // further invocation of `provider.resolve` on a provider we don't want.
483            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    /// Search for all packages that match the requirement.
535    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    /// Find the latest version for a package by name.
547    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    /// Find the latest package that matches the requirement.
554    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
574/// Resolves a `path` dependency from the project TOML in the given directory.
575/// Returns `None` for non-project paths or if the project's name does not match the spec.
576fn 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    // https://github.com/lumen-oss/lux/issues/2018
601    #[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}