lux_lib/pipeline/discover/
mod.rs1#[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 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 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 #[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}