Skip to main content

lux_lib/package_db/
mod.rs

1use crate::package;
2use std::{collections::HashMap, sync::Arc};
3
4use crate::{
5    config::{self, Config},
6    lockfile::{IntegrityError, PackageLock},
7    lua_rockspec::RemoteLuaRockspec,
8    manifest::{self, Manifest},
9    manifest_cache::{Closure, ClosureSpec, ManifestCache},
10    operations::PackageInstallSpec,
11    package::{RemotePackage, RemotePackageType, RemotePackageTypeFilterSpec},
12    pipeline::discover::{self, FoundPackage},
13    remote_package_source::RemotePackageSource,
14    workspace::{self, Workspace},
15};
16use itertools::Itertools;
17use tokio::sync::{Mutex, OnceCell};
18use url::Url;
19
20use miette::Diagnostic;
21use thiserror::Error;
22
23#[derive(Clone, Debug)]
24pub struct PackageDB {
25    providers: Vec<PackageProvider>,
26}
27
28#[derive(Error, Debug, Diagnostic)]
29pub enum Error {
30    #[error(transparent)]
31    #[diagnostic(transparent)]
32    Manifest(#[from] manifest::Error),
33    #[error(transparent)]
34    #[diagnostic(transparent)]
35    Config(#[from] config::Error),
36    #[error(transparent)]
37    #[diagnostic(transparent)]
38    Workspace(Box<workspace::Error>),
39}
40
41impl From<workspace::Error> for Error {
42    fn from(err: workspace::Error) -> Self {
43        Self::Workspace(Box::new(err))
44    }
45}
46
47#[derive(Error, Debug, Diagnostic)]
48#[non_exhaustive]
49pub enum SearchError {
50    #[error("no rock that matches '{0}' found in the lockfile or in the manifests")]
51    RockNotFound(package::Req),
52    #[error("error when pulling manifest")]
53    #[diagnostic(forward(0))]
54    Manifest(#[from] manifest::Error),
55}
56
57#[derive(Error, Debug, Diagnostic)]
58#[non_exhaustive]
59pub enum RemotePackageDbIntegrityError {
60    #[error(transparent)]
61    #[diagnostic(transparent)]
62    Lockfile(#[from] IntegrityError),
63}
64
65/// A source of package metadata consulted by a [`PackageDB`].
66#[derive(Clone, Debug)]
67pub(crate) enum PackageProvider {
68    /// Locally available packages, e.g. workspace members.
69    Local(Vec<FoundPackage>),
70    /// Packages pinned in the lockfile.
71    Lockfile(Vec<PackageLock>),
72    /// A luarocks manifest.
73    Manifest(ManifestProvider),
74    /// The manifest cache.
75    Closure(ClosureProvider),
76}
77
78/// A luarocks manifest that is only fetched when it is first needed.
79#[derive(Clone, Debug)]
80pub(crate) struct ManifestProvider {
81    server_url: Url,
82    config: Box<Config>,
83    manifest: Arc<OnceCell<Manifest>>,
84}
85
86impl ManifestProvider {
87    fn new(server_url: Url, config: &Config) -> Self {
88        Self {
89            server_url,
90            config: Box::new(config.clone()),
91            manifest: Arc::new(OnceCell::new()),
92        }
93    }
94
95    #[cfg(test)]
96    fn from_manifest(manifest: Manifest, config: &Config) -> Self {
97        Self {
98            server_url: manifest.server_url().clone(),
99            config: Box::new(config.clone()),
100            manifest: Arc::new(OnceCell::from(manifest)),
101        }
102    }
103
104    async fn get(&self) -> Result<&Manifest, manifest::Error> {
105        let manifest = self
106            .manifest
107            .get_or_try_init(|| Manifest::from_config(self.server_url.clone(), &self.config));
108        Box::pin(manifest).await
109    }
110}
111
112impl PackageProvider {
113    async fn find(
114        &self,
115        package_req: &package::Req,
116        filter: Option<RemotePackageTypeFilterSpec>,
117    ) -> Result<Option<RemotePackage>, SearchError> {
118        match self {
119            Self::Local(packages) => Ok(packages
120                .iter()
121                .find(|found| found.remote.spec.name() == package_req.name())
122                .map(|found| found.remote.clone())),
123            Self::Lockfile(locks) => Ok(locks
124                .iter()
125                .filter_map(|lock| lock.has_rock(package_req, filter.clone()))
126                .map(|locked_package| {
127                    RemotePackage::new(
128                        locked_package.to_package(),
129                        locked_package.source().clone(),
130                        locked_package.source_url().cloned(),
131                    )
132                })
133                .next()),
134            Self::Manifest(manifest) => Ok(manifest.get().await?.find(package_req, filter)),
135            Self::Closure(closure) => Ok(closure.find(package_req, filter).await),
136        }
137    }
138
139    async fn resolve(
140        &self,
141        spec: &PackageInstallSpec,
142        config: &Config,
143    ) -> Result<Option<FoundPackage>, discover::Error> {
144        match self {
145            Self::Local(packages) => Ok(packages
146                .iter()
147                .find(|package| package.remote.spec.name() == spec.package.name())
148                .cloned()),
149            Self::Closure(closure) => closure.resolve(spec, config).await,
150            Self::Lockfile(_) | Self::Manifest(_) => Ok(None),
151        }
152    }
153
154    async fn search(
155        &self,
156        package_req: &package::Req,
157    ) -> Result<Vec<(package::Name, Vec<package::Version>)>, SearchError> {
158        match self {
159            Self::Local(packages) => Ok(packages
160                .iter()
161                .filter_map(|package| {
162                    let name = package.remote.spec.name();
163                    if name.to_string().contains(&package_req.name().to_string())
164                        && package_req
165                            .version_req()
166                            .matches(package.remote.spec.version())
167                    {
168                        Some((name.clone(), vec![package.remote.spec.version().clone()]))
169                    } else {
170                        None
171                    }
172                })
173                .collect()),
174            // NOTE: This doesn't group packages by name, but we don't care for now,
175            // as we shouldn't need to use this function with a lockfile.
176            Self::Lockfile(locks) => Ok(locks
177                .iter()
178                .flat_map(|lock| lock.rocks().values())
179                .filter_map(|package| {
180                    let name = package.name();
181                    if name.to_string().contains(&package_req.name().to_string()) {
182                        Some((name.clone(), vec![package.version().clone()]))
183                    } else {
184                        None
185                    }
186                })
187                .collect()),
188            Self::Manifest(manifest) => Ok(manifest
189                .get()
190                .await?
191                .metadata()
192                .repository
193                .iter()
194                .filter_map(|(name, elements)| {
195                    if name.to_string().contains(&package_req.name().to_string()) {
196                        Some((
197                            name.clone(),
198                            elements
199                                .keys()
200                                .filter(|version| package_req.version_req().matches(version))
201                                .sorted_by(|a, b| Ord::cmp(b, a))
202                                .cloned()
203                                .collect_vec(),
204                        ))
205                    } else {
206                        None
207                    }
208                })
209                .collect()),
210            // The cache only supports lookups by exact name, not substring search.
211            Self::Closure(_) => Ok(Vec::new()),
212        }
213    }
214}
215
216#[derive(Clone, Debug)]
217pub(crate) struct ClosureProvider {
218    cache: ManifestCache,
219    rock_server: Url,
220    loaded: Arc<Mutex<HashMap<package::Name, Arc<Closure>>>>,
221}
222
223impl ClosureProvider {
224    fn new(cache: ManifestCache, config: &Config) -> Self {
225        Self {
226            cache,
227            rock_server: config.server().clone(),
228            loaded: Arc::new(Mutex::new(HashMap::new())),
229        }
230    }
231
232    async fn load_closure_for(&self, req: &package::Req) -> Option<Arc<Closure>> {
233        if let Some(closure) = self.loaded.lock().await.get(req.name()) {
234            return Some(closure.clone());
235        }
236
237        let versions = self.cache.versions(req.name()).await.ok()?;
238        let chosen = versions
239            .iter()
240            .find(|version| req.version_req().matches(version))?;
241
242        let is_latest = versions.first().is_some_and(|latest| latest == chosen);
243        let closure = if is_latest {
244            self.cache.latest_closure(req.name()).await.ok()?
245        } else {
246            self.cache.closure(req.name(), chosen).await.ok()?
247        };
248        let closure = Arc::new(closure);
249
250        self.loaded
251            .lock()
252            .await
253            .insert(req.name().clone(), closure.clone());
254        Some(closure)
255    }
256
257    fn remote_package(&self, spec: &ClosureSpec, package_type: RemotePackageType) -> RemotePackage {
258        let source = match package_type {
259            RemotePackageType::Rockspec => {
260                RemotePackageSource::LuarocksRockspec(self.rock_server.clone())
261            }
262            RemotePackageType::Src => {
263                RemotePackageSource::LuarocksSrcRock(self.rock_server.clone())
264            }
265            RemotePackageType::Binary => {
266                RemotePackageSource::LuarocksBinaryRock(self.rock_server.clone())
267            }
268        };
269        RemotePackage::new(
270            package::Spec::new(spec.package().clone(), spec.version().clone()),
271            source,
272            None,
273        )
274    }
275
276    async fn find(
277        &self,
278        req: &package::Req,
279        filter: Option<RemotePackageTypeFilterSpec>,
280    ) -> Option<RemotePackage> {
281        let closure = self.load_closure_for(req).await?;
282        let (spec, package_type) = closure.spec(req, filter.as_ref())?;
283        Some(self.remote_package(spec, package_type))
284    }
285
286    async fn resolve(
287        &self,
288        spec: &PackageInstallSpec,
289        _config: &Config,
290    ) -> Result<Option<FoundPackage>, discover::Error> {
291        let Some(closure) = self.load_closure_for(&spec.package).await else {
292            return Ok(None);
293        };
294        let Some((closure_spec, package_type)) = closure.spec(&spec.package, None) else {
295            return Ok(None);
296        };
297        // Only rockspec-type packages can be resolved fully offline, as they have no source archive
298        // to download and hash. For src/binary rocks we fall through to the generic download step,
299        // which fetches the archive.
300        if package_type != RemotePackageType::Rockspec {
301            return Ok(None);
302        }
303        let rockspec = RemoteLuaRockspec::new(closure_spec.rockspec_raw()).map_err(|err| {
304            discover::Error::Download(
305                spec.package.clone(),
306                Box::new(crate::operations::SearchAndDownloadError::Rockspec(
307                    Box::new(err),
308                )),
309            )
310        })?;
311        let remote = self.remote_package(closure_spec, package_type);
312        Ok(Some(FoundPackage {
313            remote,
314            rockspec,
315            artifact: None,
316        }))
317    }
318}
319
320impl PackageDB {
321    #[tracing::instrument(level = "trace", skip_all)]
322    pub async fn from_config(config: &Config) -> Result<Self, Error> {
323        let mut providers = Vec::new();
324        if let Some(cache) = ManifestCache::from_config(config) {
325            providers.push(PackageProvider::Closure(ClosureProvider::new(
326                cache, config,
327            )));
328        }
329        for server in config.enabled_dev_servers()? {
330            providers.push(PackageProvider::Manifest(ManifestProvider::new(
331                server, config,
332            )));
333        }
334        for server in config.extra_servers() {
335            providers.push(PackageProvider::Manifest(ManifestProvider::new(
336                server.clone(),
337                config,
338            )));
339        }
340        providers.push(PackageProvider::Manifest(ManifestProvider::new(
341            config.server().clone(),
342            config,
343        )));
344        Ok(Self { providers })
345    }
346
347    /// Builds a package database from a single, already loaded manifest.
348    #[cfg(test)]
349    pub(crate) fn from_manifest(manifest: Manifest, config: &Config) -> Self {
350        Self {
351            providers: vec![PackageProvider::Manifest(ManifestProvider::from_manifest(
352                manifest, config,
353            ))],
354        }
355    }
356
357    /// Builds a package database that prefers the workspace lockfile, falling
358    /// back to the manifests for requirements the lockfile does not satisfy.
359    pub(crate) async fn open(config: &Config, workspace: &Workspace) -> Result<Self, Error> {
360        Ok(Self::from_config(config)
361            .await?
362            .with_locks(workspace.lockfile()?.local_pkg_locks()))
363    }
364
365    #[cfg(test)]
366    pub(crate) fn from_local(packages: Vec<FoundPackage>) -> Self {
367        Self {
368            providers: vec![PackageProvider::Local(packages)],
369        }
370    }
371
372    /// Sets the local packages to look up first.
373    pub(crate) fn with_local(mut self, packages: Vec<FoundPackage>) -> Self {
374        self.providers
375            .retain(|provider| !matches!(provider, PackageProvider::Local(_)));
376        self.providers.insert(0, PackageProvider::Local(packages));
377        self
378    }
379
380    /// Sets the locked packages to look up after local packages, ahead of manifests.
381    pub(crate) fn with_locks(mut self, locks: Vec<PackageLock>) -> Self {
382        self.providers
383            .retain(|provider| !matches!(provider, PackageProvider::Lockfile(_)));
384        let index = self
385            .providers
386            .iter()
387            .position(|provider| {
388                matches!(
389                    provider,
390                    PackageProvider::Closure(_) | PackageProvider::Manifest(_)
391                )
392            })
393            .unwrap_or(self.providers.len());
394        self.providers
395            .insert(index, PackageProvider::Lockfile(locks));
396        self
397    }
398
399    pub(crate) fn has_remote_sources(&self) -> bool {
400        self.providers.iter().any(|provider| {
401            matches!(
402                provider,
403                PackageProvider::Lockfile(_)
404                    | PackageProvider::Manifest(_)
405                    | PackageProvider::Closure(_)
406            )
407        })
408    }
409
410    /// Find a remote package that matches the requirement, returning the latest match.
411    #[tracing::instrument(
412          name = "Searching",
413          level = "info",
414          skip_all,
415          fields(package = package_req.to_string()),
416    )]
417    pub(crate) async fn find(
418        &self,
419        package_req: &package::Req,
420        filter: Option<RemotePackageTypeFilterSpec>,
421    ) -> Result<RemotePackage, SearchError> {
422        for provider in &self.providers {
423            if let Some(package) = provider.find(package_req, filter.clone()).await? {
424                return Ok(package);
425            }
426        }
427
428        Err(SearchError::RockNotFound(package_req.clone()))
429    }
430
431    /// Resolves a package install spec to a package that is already available.
432    pub(crate) async fn resolve(
433        &self,
434        spec: &PackageInstallSpec,
435        config: &Config,
436    ) -> Result<Option<FoundPackage>, discover::Error> {
437        for provider in &self.providers {
438            // Providers that can serve a package without downloading it (local packages and the
439            // manifest cache) resolve early.
440            if let Some(found) = provider.resolve(spec, config).await? {
441                return Ok(Some(found));
442            }
443
444            // Otherwise break on the first provider that has the package. The `break` prevents a
445            // further invocation of `provider.resolve` on a provider we don't want.
446            if provider.find(&spec.package, None).await?.is_some() {
447                break;
448            }
449        }
450        Ok(None)
451    }
452
453    /// Search for all packages that match the requirement.
454    pub async fn search(
455        &self,
456        package_req: &package::Req,
457    ) -> Result<Vec<(package::Name, Vec<package::Version>)>, SearchError> {
458        let mut results = Vec::new();
459        for provider in &self.providers {
460            results.extend(provider.search(package_req).await?);
461        }
462        Ok(results)
463    }
464
465    /// Find the latest version for a package by name.
466    pub(crate) async fn latest_version(
467        &self,
468        rock_name: &package::Name,
469    ) -> Option<package::Version> {
470        self.latest_match(&rock_name.clone().into(), None)
471            .await
472            .map(|result| result.version().clone())
473    }
474
475    /// Find the latest package that matches the requirement.
476    pub async fn latest_match(
477        &self,
478        package_req: &package::Req,
479        filter: Option<RemotePackageTypeFilterSpec>,
480    ) -> Option<package::Spec> {
481        match self.find(package_req, filter).await {
482            Ok(result) => Some(result.spec),
483            Err(_) => None,
484        }
485    }
486}
487
488impl From<Vec<PackageLock>> for PackageDB {
489    fn from(locks: Vec<PackageLock>) -> Self {
490        Self {
491            providers: vec![PackageProvider::Lockfile(locks)],
492        }
493    }
494}