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 Error {
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::Error),
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(self, spec: &PackageInstallSpec) -> Result<FoundPackage, Error> {
109        let args = self._build();
110        if let Some(found) = discover_project(spec)? {
111            return Ok(found);
112        }
113        if let Some(source) = &spec.source {
114            return discover_source_spec(spec, source, args.config).await;
115        }
116        discover_package_db(spec, &args.package_db, args.config).await
117    }
118}
119
120fn discover_project(spec: &PackageInstallSpec) -> Result<Option<FoundPackage>, Error> {
121    let Some(RockSourceSpec::File(path)) = &spec.source else {
122        return Ok(None);
123    };
124    Ok(match Project::from_exact(path)? {
125        Some(project) if project.toml().package() == spec.package.name() => {
126            Some(FoundPackage::from_project_root(
127                project.local_remote_rockspec()?,
128                project.root().to_path_buf(),
129            ))
130        }
131        _ => None,
132    })
133}
134
135async fn discover_source_spec(
136    spec: &PackageInstallSpec,
137    source: &RockSourceSpec,
138    _config: &Config,
139) -> Result<FoundPackage, Error> {
140    match source {
141        RockSourceSpec::Git(_) | RockSourceSpec::File(_) | RockSourceSpec::Url(_) => {
142            discover_fetchable_source(spec, source.clone())
143        }
144        #[cfg(feature = "wally")]
145        RockSourceSpec::Wally(wally_req) => wally::discover(&spec.package, wally_req, _config)
146            .await
147            .map_err(Error::Wally),
148        #[cfg(not(feature = "wally"))]
149        RockSourceSpec::Wally(_) => Err(Error::WallyDisabled),
150    }
151}
152
153fn discover_fetchable_source(
154    spec: &PackageInstallSpec,
155    source: RockSourceSpec,
156) -> Result<FoundPackage, Error> {
157    let download =
158        RemoteRockDownload::from_package_req_and_source_spec(spec.package.clone(), source)
159            .map_err(|err| Error::Download(spec.package.clone(), Box::new(err)))?;
160    let rockspec = download.rockspec().clone();
161    let remote = RemotePackage::new(
162        spec.package.clone().try_into().map_err(|err| {
163            Error::Download(
164                spec.package.clone(),
165                Box::new(operations::SearchAndDownloadError::from(err)),
166            )
167        })?,
168        download.rockspec_download().source.clone(),
169        download.rockspec_download().source_url.clone(),
170    );
171    Ok(FoundPackage {
172        remote,
173        rockspec,
174        artifact: None,
175    })
176}
177
178async fn discover_package_db(
179    spec: &PackageInstallSpec,
180    package_db: &PackageDB,
181    config: &Config,
182) -> Result<FoundPackage, Error> {
183    if let Some(found) = package_db.resolve(spec, config).await? {
184        Ok(found)
185    } else {
186        download_remote(spec, package_db, config).await
187    }
188}
189
190async fn download_remote(
191    spec: &PackageInstallSpec,
192    package_db: &PackageDB,
193    config: &Config,
194) -> Result<FoundPackage, Error> {
195    if package_db.has_remote_sources() {
196        let mut remote = package_db.find(&spec.package, None).await?;
197        #[cfg(feature = "wally")]
198        if let RemotePackageSource::Wally(index_url) = &remote.source {
199            return wally::discover_locked(&remote, index_url, config)
200                .await
201                .map_err(Error::Wally);
202        }
203        #[cfg(not(feature = "wally"))]
204        if let RemotePackageSource::Wally(_) = &remote.source {
205            return Err(Error::WallyDisabled);
206        }
207        let download = if let Some(vendor_dir) = config.vendor_dir() {
208            FetchVendored::new()
209                .vendor_dir(vendor_dir)
210                .package(&spec.package)
211                .package_db(package_db)
212                .fetch_vendored_rock()
213                .await
214                .map_err(|err| Error::FetchVendored(spec.package.clone(), Box::new(err)))?
215        } else {
216            Download::new(&spec.package, config)
217                .package_db(package_db)
218                .download_remote_rock()
219                .await
220                .map_err(|err| Error::Download(spec.package.clone(), Box::new(err)))?
221        };
222        let rockspec = download.rockspec().clone();
223        let artifact = match &download {
224            RemoteRockDownload::SrcRock {
225                src_rock,
226                source_url,
227                ..
228            } => {
229                remote.source_url = Some(source_url.clone());
230                Some(PackageSource::Archive(src_rock.clone()))
231            }
232            RemoteRockDownload::BinaryRock { packed_rock, .. } => {
233                Some(PackageSource::Archive(packed_rock.clone()))
234            }
235            RemoteRockDownload::RockspecOnly { .. } => None,
236        };
237        Ok(FoundPackage {
238            remote,
239            rockspec,
240            artifact,
241        })
242    } else {
243        Err(Error::Search(SearchError::RockNotFound(
244            spec.package.clone(),
245        )))
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use std::path::PathBuf;
253
254    use crate::{build::BuildBehaviour, config::ConfigBuilder, tree::EntryType};
255
256    #[test]
257    fn from_rockspec_builds_local_found_package() {
258        let content = "rockspec_format = '1.0'\npackage = 'foo'\nversion = '1.0.0-1'\nsource = { url = 'https://example.com/foo.zip' }";
259        let Ok(rockspec) = RemoteLuaRockspec::new(content) else {
260            unreachable!("invalid test rockspec");
261        };
262
263        let found = FoundPackage::from_rockspec(rockspec);
264
265        assert_eq!(found.remote.spec.name().to_string(), "foo");
266        assert_eq!(found.remote.spec.version().to_string(), "1.0.0-1");
267        assert!(matches!(
268            found.remote.source,
269            RemotePackageSource::RockspecContent(_)
270        ));
271        assert!(found.artifact.is_none());
272    }
273
274    // https://github.com/lumen-oss/lux/issues/2018
275    #[tokio::test]
276    async fn resolves_path_dependency_from_project_manifest() {
277        let bar_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
278            .join("resources/test/sample-projects/multi-project-local-deps/projects/bar");
279
280        let config = ConfigBuilder::new().unwrap().build().unwrap();
281        let spec = PackageInstallSpec::new(
282            PackageReq::new("bar".into(), Some("1.0.0".into())).unwrap(),
283            EntryType::DependencyOnly,
284        )
285        .build_behaviour(BuildBehaviour::Ignore)
286        .maybe_source(Some(RockSourceSpec::File(bar_dir)))
287        .build();
288
289        let found = Discover::new(PackageDB::from_local(Vec::new()), &config)
290            .discover(&spec)
291            .await
292            .unwrap();
293
294        assert!(found
295            .rockspec
296            .dependencies()
297            .current_platform()
298            .iter()
299            .any(|dependency| dependency.name().to_string() == "foo"));
300    }
301}