Skip to main content

lux_lib/pipeline/discover/
mod.rs

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