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