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 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 #[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}