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