Skip to main content

lux_lib/pipeline/discover/
mod.rs

1#[cfg(feature = "wally")]
2pub(crate) mod wally;
3
4use std::path::PathBuf;
5
6use bon::Builder;
7use miette::Diagnostic;
8use thiserror::Error;
9
10use crate::{
11    config::Config,
12    lua_rockspec::{RemoteLuaRockspec, RockSourceSpec},
13    operations,
14    operations::{Download, FetchVendored, PackageInstallSpec, RemoteRockDownload},
15    package::{PackageReq, PackageSpec, RemotePackage},
16    package_db::{PackageDB, SearchError},
17    project::Project,
18    remote_package_source::RemotePackageSource,
19    rockspec::Rockspec,
20};
21
22use super::materialise::PackageSource;
23
24#[derive(Clone, Debug)]
25pub(crate) struct FoundPackage {
26    pub(crate) remote: RemotePackage,
27    pub(crate) rockspec: RemoteLuaRockspec,
28    pub(crate) artifact: Option<PackageSource>,
29}
30
31impl FoundPackage {
32    /// Builds a discovered package from an already-parsed rockspec, so it can
33    /// be resolved as a root without contacting a remote package database.
34    pub(crate) fn from_rockspec(rockspec: RemoteLuaRockspec) -> Self {
35        let source = rockspec
36            .to_lua_remote_rockspec_string()
37            .map(RemotePackageSource::RockspecContent)
38            .unwrap_or(RemotePackageSource::Local);
39        let remote = RemotePackage::new(
40            PackageSpec::new(rockspec.package().clone(), rockspec.version().clone()),
41            source,
42            None,
43        );
44        Self {
45            remote,
46            rockspec,
47            artifact: None,
48        }
49    }
50
51    /// Builds a discovered package from a project root rockspec, using the
52    /// project's own directory as the source.
53    pub(crate) fn from_project_root(rockspec: RemoteLuaRockspec, root_dir: PathBuf) -> Self {
54        let remote = RemotePackage::new(
55            PackageSpec::new(rockspec.package().clone(), rockspec.version().clone()),
56            RemotePackageSource::Local,
57            None,
58        );
59        Self {
60            remote,
61            rockspec,
62            artifact: Some(PackageSource::SourceDir(root_dir)),
63        }
64    }
65}
66
67#[derive(Error, Debug, Diagnostic)]
68#[non_exhaustive]
69pub enum DiscoverError {
70    #[error(transparent)]
71    #[diagnostic(transparent)]
72    Search(#[from] SearchError),
73    #[error("failed to acquire metadata for '{0}'")]
74    #[diagnostic(forward(1))]
75    Download(PackageReq, Box<operations::SearchAndDownloadError>),
76    #[error("failed to fetch vendored package for '{0}'")]
77    #[diagnostic(forward(1))]
78    FetchVendored(PackageReq, Box<operations::FetchVendoredError>),
79    #[error(transparent)]
80    #[diagnostic(transparent)]
81    Project(#[from] crate::project::ProjectError),
82    #[error(transparent)]
83    #[diagnostic(transparent)]
84    LocalRockspec(#[from] crate::project::IntoLocalRockspecError),
85    #[cfg(feature = "wally")]
86    #[error(transparent)]
87    #[diagnostic(transparent)]
88    Wally(#[from] wally::WallyDiscoverError),
89    #[cfg(not(feature = "wally"))]
90    #[error("wally support is disabled in this build")]
91    #[diagnostic(help("this build of Lux was compiled without the `wally` feature"))]
92    WallyDisabled,
93}
94
95#[derive(Builder)]
96#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
97pub(crate) struct Discover<'a> {
98    #[builder(start_fn)]
99    pub(crate) package_db: PackageDB,
100    #[builder(start_fn)]
101    pub(crate) config: &'a Config,
102}
103
104impl<State> DiscoverBuilder<'_, State>
105where
106    State: discover_builder::State + discover_builder::IsComplete,
107{
108    pub(crate) async fn discover(
109        self,
110        spec: &PackageInstallSpec,
111    ) -> Result<FoundPackage, DiscoverError> {
112        let args = self._build();
113        if let Some(found) = discover_project(spec)? {
114            return Ok(found);
115        }
116        if let Some(source) = &spec.source {
117            return discover_source_spec(spec, source, args.config).await;
118        }
119        discover_package_db(spec, &args.package_db, args.config).await
120    }
121}
122
123fn discover_project(spec: &PackageInstallSpec) -> Result<Option<FoundPackage>, DiscoverError> {
124    let Some(RockSourceSpec::File(path)) = &spec.source else {
125        return Ok(None);
126    };
127    Ok(match Project::from_exact(path)? {
128        Some(project) if project.toml().package() == spec.package.name() => {
129            Some(FoundPackage::from_project_root(
130                project.local_remote_rockspec()?,
131                project.root().to_path_buf(),
132            ))
133        }
134        _ => None,
135    })
136}
137
138async fn discover_source_spec(
139    spec: &PackageInstallSpec,
140    source: &RockSourceSpec,
141    _config: &Config,
142) -> Result<FoundPackage, DiscoverError> {
143    match source {
144        RockSourceSpec::Git(_) | RockSourceSpec::File(_) | RockSourceSpec::Url(_) => {
145            discover_fetchable_source(spec, source.clone())
146        }
147        #[cfg(feature = "wally")]
148        RockSourceSpec::Wally(wally_req) => wally::discover(&spec.package, wally_req, _config)
149            .await
150            .map_err(DiscoverError::Wally),
151        #[cfg(not(feature = "wally"))]
152        RockSourceSpec::Wally(_) => Err(DiscoverError::WallyDisabled),
153    }
154}
155
156fn discover_fetchable_source(
157    spec: &PackageInstallSpec,
158    source: RockSourceSpec,
159) -> Result<FoundPackage, DiscoverError> {
160    let download =
161        RemoteRockDownload::from_package_req_and_source_spec(spec.package.clone(), source)
162            .map_err(|err| DiscoverError::Download(spec.package.clone(), Box::new(err)))?;
163    let rockspec = download.rockspec().clone();
164    let remote = RemotePackage::new(
165        spec.package.clone().try_into().map_err(|err| {
166            DiscoverError::Download(
167                spec.package.clone(),
168                Box::new(operations::SearchAndDownloadError::from(err)),
169            )
170        })?,
171        download.rockspec_download().source.clone(),
172        download.rockspec_download().source_url.clone(),
173    );
174    Ok(FoundPackage {
175        remote,
176        rockspec,
177        artifact: None,
178    })
179}
180
181async fn discover_package_db(
182    spec: &PackageInstallSpec,
183    package_db: &PackageDB,
184    config: &Config,
185) -> Result<FoundPackage, DiscoverError> {
186    if let Some(found) = package_db.resolve(spec, config).await? {
187        Ok(found)
188    } else {
189        download_remote(spec, package_db, config).await
190    }
191}
192
193async fn download_remote(
194    spec: &PackageInstallSpec,
195    package_db: &PackageDB,
196    config: &Config,
197) -> Result<FoundPackage, DiscoverError> {
198    if package_db.has_remote_sources() {
199        let mut remote = package_db.find(&spec.package, None).await?;
200        #[cfg(feature = "wally")]
201        if let RemotePackageSource::Wally(index_url) = &remote.source {
202            return wally::discover_locked(&remote, index_url, config)
203                .await
204                .map_err(DiscoverError::Wally);
205        }
206        #[cfg(not(feature = "wally"))]
207        if let RemotePackageSource::Wally(_) = &remote.source {
208            return Err(DiscoverError::WallyDisabled);
209        }
210        let download = if let Some(vendor_dir) = config.vendor_dir() {
211            FetchVendored::new()
212                .vendor_dir(vendor_dir)
213                .package(&spec.package)
214                .package_db(package_db)
215                .fetch_vendored_rock()
216                .await
217                .map_err(|err| DiscoverError::FetchVendored(spec.package.clone(), Box::new(err)))?
218        } else {
219            Download::new(&spec.package, config)
220                .package_db(package_db)
221                .download_remote_rock()
222                .await
223                .map_err(|err| DiscoverError::Download(spec.package.clone(), Box::new(err)))?
224        };
225        let rockspec = download.rockspec().clone();
226        let artifact = match &download {
227            RemoteRockDownload::SrcRock {
228                src_rock,
229                source_url,
230                ..
231            } => {
232                remote.source_url = Some(source_url.clone());
233                Some(PackageSource::Archive(src_rock.clone()))
234            }
235            RemoteRockDownload::BinaryRock { packed_rock, .. } => {
236                Some(PackageSource::Archive(packed_rock.clone()))
237            }
238            RemoteRockDownload::RockspecOnly { .. } => None,
239        };
240        Ok(FoundPackage {
241            remote,
242            rockspec,
243            artifact,
244        })
245    } else {
246        Err(DiscoverError::Search(SearchError::RockNotFound(
247            spec.package.clone(),
248        )))
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use std::path::PathBuf;
256
257    use crate::{build::BuildBehaviour, config::ConfigBuilder, tree::EntryType};
258
259    #[test]
260    fn from_rockspec_builds_local_found_package() {
261        let content = "rockspec_format = '1.0'\npackage = 'foo'\nversion = '1.0.0-1'\nsource = { url = 'https://example.com/foo.zip' }";
262        let Ok(rockspec) = RemoteLuaRockspec::new(content) else {
263            unreachable!("invalid test rockspec");
264        };
265
266        let found = FoundPackage::from_rockspec(rockspec);
267
268        assert_eq!(found.remote.spec.name().to_string(), "foo");
269        assert_eq!(found.remote.spec.version().to_string(), "1.0.0-1");
270        assert!(matches!(
271            found.remote.source,
272            RemotePackageSource::RockspecContent(_)
273        ));
274        assert!(found.artifact.is_none());
275    }
276
277    // https://github.com/lumen-oss/lux/issues/2018
278    #[tokio::test]
279    async fn resolves_path_dependency_from_project_manifest() {
280        let bar_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
281            .join("resources/test/sample-projects/multi-project-local-deps/projects/bar");
282
283        let config = ConfigBuilder::new().unwrap().build().unwrap();
284        let spec = PackageInstallSpec::new(
285            PackageReq::new("bar".into(), Some("1.0.0".into())).unwrap(),
286            EntryType::DependencyOnly,
287        )
288        .build_behaviour(BuildBehaviour::Ignore)
289        .maybe_source(Some(RockSourceSpec::File(bar_dir)))
290        .build();
291
292        let found = Discover::new(PackageDB::from_local(Vec::new()), &config)
293            .discover(&spec)
294            .await
295            .unwrap();
296
297        assert!(found
298            .rockspec
299            .dependencies()
300            .current_platform()
301            .iter()
302            .any(|dependency| dependency.name().to_string() == "foo"));
303    }
304}