1use std::collections::{HashMap, HashSet, VecDeque};
2use std::sync::Arc;
3
4use bon::Builder;
5use itertools::Itertools;
6use miette::Diagnostic;
7use thiserror::Error;
8use tokio::task::JoinSet;
9
10use crate::{
11 build::BuildBehaviour,
12 config::Config,
13 lockfile::{
14 LockedPackage, LockedPackageId, LockedPackageLockType, LockedPackageSpec, OptState,
15 PinnedState, RemotePackageSourceUrl,
16 },
17 lua_rockspec::{BuildBackendSpec, RemoteLuaRockspec},
18 operations::PackageInstallSpec,
19 package::{PackageName, PackageReq},
20 package_db::PackageDB,
21 remote_package_source::RemotePackageSource,
22 rockspec::Rockspec,
23 tree::EntryType,
24};
25
26use super::{
27 discover::{self, Discover, FoundPackage},
28 materialise::PackageSource,
29 Artifacts,
30};
31
32pub(crate) fn build_dependencies_to_install<R: Rockspec>(rockspec: &R) -> Vec<PackageName> {
35 let mut names = rockspec
36 .build_dependencies()
37 .current_platform()
38 .iter()
39 .filter(|dep| {
40 !matches!(
41 dep.name().to_string().as_str(),
42 "luarocks-build-rust-mlua"
43 | "luarocks-build-rust-binary"
44 | "luarocks-build-treesitter-parser"
45 )
46 })
47 .map(|dep| dep.name().clone())
48 .collect_vec();
49
50 if let Some(backend) = luarocks_build_backend_name(rockspec) {
51 names.insert(0, backend);
52 }
53 names
54}
55
56pub(crate) fn luarocks_build_backend_name<R: Rockspec>(rockspec: &R) -> Option<PackageName> {
58 match &rockspec.build().current_platform().build_backend {
59 Some(BuildBackendSpec::LuaRock(backend)) => {
60 Some(PackageName::new(format!("luarocks-build-{backend}")))
61 }
62 _ => None,
63 }
64}
65
66pub(crate) struct ResolvedPackage {
67 pub(crate) spec: LockedPackageSpec,
68 pub(crate) rockspec: RemoteLuaRockspec,
69 pub(crate) source: RemotePackageSource,
70 pub(crate) source_url: Option<RemotePackageSourceUrl>,
71 pub(crate) entry_type: EntryType,
72 pub(crate) build_behaviour: crate::build::BuildBehaviour,
73 pub(crate) artifact: Option<PackageSource>,
74}
75
76pub(crate) type ResolvedArtifacts = Artifacts<HashMap<LockedPackageId, ResolvedPackage>>;
77
78#[derive(Error, Debug, Diagnostic)]
79#[non_exhaustive]
80pub enum Error {
81 #[error(transparent)]
82 #[diagnostic(transparent)]
83 Discover(#[from] Box<discover::Error>),
84 #[error("cyclic dependency detected:\n{0}")]
85 CyclicDependency(String),
86 #[error(transparent)]
87 Join(#[from] tokio::task::JoinError),
88}
89
90impl From<discover::Error> for Error {
91 fn from(err: discover::Error) -> Self {
92 Self::Discover(Box::new(err))
93 }
94}
95
96#[derive(Builder)]
97#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
98pub(crate) struct ResolvePackageDependencies<'a> {
99 #[builder(start_fn)]
100 pub(crate) package_db: PackageDB,
101 #[builder(start_fn)]
102 pub(crate) config: &'a Config,
103 #[builder(field)]
104 pub(crate) packages: Artifacts<Vec<PackageInstallSpec>>,
105 #[builder(default)]
106 pub(crate) installed: HashMap<LockedPackageLockType, Vec<LockedPackage>>,
107}
108
109impl<State> ResolvePackageDependenciesBuilder<'_, State>
110where
111 State: resolve_package_dependencies_builder::State,
112{
113 fn set(mut self, section: LockedPackageLockType, packages: Vec<PackageInstallSpec>) -> Self {
114 *self.packages.get_mut(section) = Some(packages);
115 self
116 }
117
118 pub(crate) fn packages(self, packages: Vec<PackageInstallSpec>) -> Self {
119 self.set(LockedPackageLockType::Regular, packages)
120 }
121
122 pub(crate) fn build_packages(self, packages: Vec<PackageInstallSpec>) -> Self {
123 self.set(LockedPackageLockType::Build, packages)
124 }
125
126 pub(crate) fn test_packages(self, packages: Vec<PackageInstallSpec>) -> Self {
127 self.set(LockedPackageLockType::Test, packages)
128 }
129}
130
131impl<State> ResolvePackageDependenciesBuilder<'_, State>
132where
133 State: resolve_package_dependencies_builder::State
134 + resolve_package_dependencies_builder::IsComplete,
135{
136 pub(crate) async fn resolve(self) -> Result<ResolvedArtifacts, Error> {
137 let args = self._build();
138 Resolver::new(args.package_db, args.config, args.installed)
139 .run(args.packages)
140 .await
141 }
142}
143
144#[derive(Clone, PartialEq, Eq, Hash)]
147struct RequestKey {
148 section: LockedPackageLockType,
149 package: PackageReq,
150 pin: PinnedState,
151 opt: OptState,
152}
153
154impl RequestKey {
155 fn from_install_spec(section: LockedPackageLockType, spec: &PackageInstallSpec) -> Self {
156 Self {
157 section,
158 package: spec.package.clone(),
159 pin: spec.pin,
160 opt: spec.opt,
161 }
162 }
163}
164
165struct Node {
167 install_spec: PackageInstallSpec,
168 id: LockedPackageId,
169 found: FoundPackage,
170 children: Vec<(bool, RequestKey)>,
171}
172
173struct Resolver {
174 package_db: PackageDB,
175 config: Arc<Config>,
176 max_concurrent: usize,
177 joinset: JoinSet<(RequestKey, PackageInstallSpec, Result<FoundPackage, Error>)>,
178 pending: VecDeque<(RequestKey, PackageInstallSpec)>,
179 seen: HashSet<RequestKey>,
180 nodes: HashMap<RequestKey, Node>,
181 resolved: ResolvedArtifacts,
182 installed: HashMap<LockedPackageLockType, Vec<LockedPackage>>,
183 installed_ids: HashMap<RequestKey, LockedPackageId>,
184}
185
186impl Resolver {
187 fn new(
188 package_db: PackageDB,
189 config: &Config,
190 installed: HashMap<LockedPackageLockType, Vec<LockedPackage>>,
191 ) -> Self {
192 Self::with_max_concurrent(
193 package_db,
194 Arc::new(config.clone()),
195 config.max_jobs(),
196 installed,
197 )
198 }
199
200 fn with_max_concurrent(
201 package_db: PackageDB,
202 config: Arc<Config>,
203 max_concurrent: usize,
204 installed: HashMap<LockedPackageLockType, Vec<LockedPackage>>,
205 ) -> Self {
206 Self {
207 package_db,
208 config,
209 max_concurrent,
210 joinset: JoinSet::new(),
211 pending: VecDeque::new(),
212 seen: HashSet::new(),
213 nodes: HashMap::new(),
214 resolved: ResolvedArtifacts::default(),
215 installed,
216 installed_ids: HashMap::new(),
217 }
218 }
219
220 async fn run(
221 mut self,
222 packages: Artifacts<Vec<PackageInstallSpec>>,
223 ) -> Result<ResolvedArtifacts, Error> {
224 for (section, specs) in packages {
225 let Some(specs) = specs else {
226 continue;
227 };
228 self.resolved
230 .get_mut(section)
231 .get_or_insert_with(HashMap::new);
232 for spec in specs {
233 let key = RequestKey::from_install_spec(section, &spec);
234 self.enqueue(key, spec);
235 }
236 }
237
238 self.spawn_ready();
239 while let Some(joined) = self.joinset.join_next().await {
240 let (key, spec, result) = joined.map_err(Error::Join)?;
241 self.on_discovered(key, spec, result?);
242 self.spawn_ready();
243 }
244
245 detect_cycles(&self.nodes)?;
246 Ok(self.assemble())
247 }
248
249 fn enqueue(&mut self, key: RequestKey, spec: PackageInstallSpec) {
251 if self.seen.insert(key.clone()) {
252 self.pending.push_back((key, spec));
253 }
254 }
255
256 fn spawn_ready(&mut self) {
258 while self.joinset.len() < self.max_concurrent {
259 let Some((key, spec)) = self.pending.pop_front() else {
260 break;
261 };
262 let package_db = self.package_db.clone();
263 let config = Arc::clone(&self.config);
264 let task_spec = spec.clone();
265 self.joinset.spawn(async move {
266 let result = Discover::new(package_db, &config)
267 .discover(&task_spec)
268 .await
269 .map_err(|err| err.into());
270 (key, spec, result)
271 });
272 }
273 }
274
275 fn on_discovered(&mut self, key: RequestKey, spec: PackageInstallSpec, found: FoundPackage) {
277 let section = key.section;
278 let is_binary = matches!(
279 found.remote.source,
280 RemotePackageSource::LuarocksBinaryRock(_)
281 );
282 let constraint = spec
283 .constraint
284 .clone()
285 .unwrap_or_else(|| spec.package.version_req().clone().into());
286 let id = LockedPackageId::new(
287 found.remote.spec.name(),
288 found.remote.spec.version(),
289 spec.pin,
290 spec.opt,
291 constraint,
292 );
293
294 let mut child_specs: Vec<(bool, RequestKey, PackageInstallSpec)> = Vec::new();
295 if !is_binary {
296 for child_spec in build_dependency_specs(&found.rockspec, &spec) {
297 push_child(&mut child_specs, true, section, child_spec);
298 }
299 }
300 for dependency in found.rockspec.dependencies().current_platform() {
301 let child_spec = PackageInstallSpec::new(
302 dependency.package_req().clone(),
303 EntryType::DependencyOnly,
304 )
305 .build_behaviour(BuildBehaviour::Ignore)
306 .pin(spec.pin)
307 .opt(spec.opt)
308 .maybe_source(dependency.source().clone())
309 .build();
310 push_child(&mut child_specs, false, section, child_spec);
311 }
312
313 let children = child_specs
314 .iter()
315 .map(|(is_build, child_key, _)| (*is_build, child_key.clone()))
316 .collect();
317 self.resolved
318 .get_mut(section)
319 .get_or_insert_with(HashMap::new);
320 self.nodes.insert(
321 key,
322 Node {
323 install_spec: spec,
324 id,
325 found,
326 children,
327 },
328 );
329
330 for (is_build, child_key, child_spec) in child_specs {
331 let child_section = if is_build {
332 LockedPackageLockType::Build
333 } else {
334 section
335 };
336 if let Some(installed_id) = self.installed_match(child_section, &child_spec) {
337 self.installed_ids.insert(child_key, installed_id);
338 } else {
339 self.enqueue(child_key, child_spec);
340 }
341 }
342 }
343
344 fn installed_match(
345 &self,
346 section: LockedPackageLockType,
347 spec: &PackageInstallSpec,
348 ) -> Option<LockedPackageId> {
349 if !matches!(spec.build_behaviour, BuildBehaviour::Ignore) || spec.source.is_some() {
350 return None;
351 }
352 self.installed
353 .get(§ion)?
354 .iter()
355 .rev()
356 .find(|package| {
357 package.name() == spec.package.name()
358 && spec.package.version_req().matches(package.version())
359 })
360 .map(|package| package.id())
361 }
362
363 fn assemble(self) -> ResolvedArtifacts {
366 let Resolver {
367 nodes,
368 mut resolved,
369 installed_ids,
370 ..
371 } = self;
372
373 let mut ids: HashMap<RequestKey, LockedPackageId> = nodes
374 .iter()
375 .map(|(key, node)| (key.clone(), node.id.clone()))
376 .collect();
377 for (key, id) in installed_ids {
378 ids.entry(key).or_insert(id);
379 }
380
381 for (key, node) in nodes {
382 let Node {
383 install_spec,
384 id,
385 found,
386 children,
387 } = node;
388 let dependencies = children
389 .iter()
390 .filter(|(is_build, _)| !is_build)
391 .map(|(_, child)| ids[child].clone())
392 .collect_vec();
393 let build_dependencies = children
394 .iter()
395 .filter(|(is_build, _)| *is_build)
396 .map(|(_, child)| ids[child].clone())
397 .collect_vec();
398 let constraint = install_spec
399 .constraint
400 .clone()
401 .unwrap_or_else(|| install_spec.package.version_req().clone().into());
402
403 let spec = LockedPackageSpec::new(
404 found.remote.spec.name(),
405 found.remote.spec.version(),
406 constraint,
407 dependencies,
408 build_dependencies,
409 &install_spec.pin,
410 &install_spec.opt,
411 );
412 let package = ResolvedPackage {
413 spec,
414 rockspec: found.rockspec,
415 source: found.remote.source,
416 source_url: found.remote.source_url,
417 entry_type: install_spec.entry_type,
418 build_behaviour: install_spec.build_behaviour,
419 artifact: found.artifact,
420 };
421
422 resolved
423 .get_mut(key.section)
424 .get_or_insert_with(HashMap::new)
425 .entry(id)
426 .or_insert(package);
427 }
428
429 resolved
430 }
431}
432
433fn push_child(
435 children: &mut Vec<(bool, RequestKey, PackageInstallSpec)>,
436 is_build: bool,
437 section: LockedPackageLockType,
438 spec: PackageInstallSpec,
439) {
440 let child_section = if is_build {
441 LockedPackageLockType::Build
442 } else {
443 section
444 };
445 let child_key = RequestKey::from_install_spec(child_section, &spec);
446 if !children
447 .iter()
448 .any(|(_, existing, _)| existing == &child_key)
449 {
450 children.push((is_build, child_key, spec));
451 }
452}
453
454fn detect_cycles(nodes: &HashMap<RequestKey, Node>) -> Result<(), Error> {
456 #[derive(Clone, Copy, PartialEq, Eq)]
457 enum Colour {
458 White,
460 Grey,
462 Black,
464 }
465
466 let mut colors: HashMap<&RequestKey, Colour> = HashMap::with_capacity(nodes.len());
467
468 for start in nodes.keys() {
469 if colors.get(start).copied().unwrap_or(Colour::White) != Colour::White {
470 continue;
471 }
472 colors.insert(start, Colour::Grey);
473 let mut stack: Vec<(&RequestKey, usize)> = vec![(start, 0)];
474 let mut path: Vec<&RequestKey> = vec![start];
475
476 while let Some(&(node, index)) = stack.last() {
477 let children = &nodes[node].children;
478 if index < children.len() {
479 if let Some(top) = stack.last_mut() {
480 top.1 += 1;
481 }
482 let child = &children[index].1;
483 if !nodes.contains_key(child) {
484 continue;
485 }
486 match colors.get(child).copied().unwrap_or(Colour::White) {
487 Colour::White => {
488 colors.insert(child, Colour::Grey);
489 path.push(child);
490 stack.push((child, 0));
491 }
492 Colour::Grey => {
493 let Some(position) = path.iter().position(|key| *key == child) else {
494 continue;
495 };
496 let chain = path[position..]
497 .iter()
498 .map(|key| key.package.name().to_string())
499 .chain(std::iter::once(child.package.name().to_string()))
500 .collect_vec()
501 .join(" -> ");
502 return Err(Error::CyclicDependency(chain));
503 }
504 Colour::Black => {}
505 }
506 } else {
507 colors.insert(node, Colour::Black);
508 stack.pop();
509 path.pop();
510 }
511 }
512 }
513
514 Ok(())
515}
516
517fn build_dependency_specs(
518 rockspec: &RemoteLuaRockspec,
519 parent: &PackageInstallSpec,
520) -> Vec<PackageInstallSpec> {
521 build_dependencies_to_install(rockspec)
522 .into_iter()
523 .filter_map(|name| {
524 rockspec
525 .build_dependencies()
526 .current_platform()
527 .iter()
528 .find(|dep| dep.name() == &name)
529 .map(|dep| {
530 PackageInstallSpec::new(dep.package_req().clone(), EntryType::Entrypoint)
531 .build_behaviour(BuildBehaviour::Ignore)
532 .pin(parent.pin)
533 .opt(parent.opt)
534 .maybe_source(dep.source().clone())
535 .build()
536 })
537 .or_else(|| {
538 Some(
539 PackageInstallSpec::new(PackageReq::from(name), EntryType::Entrypoint)
540 .build_behaviour(BuildBehaviour::Ignore)
541 .pin(parent.pin)
542 .opt(parent.opt)
543 .build(),
544 )
545 })
546 })
547 .collect()
548}
549
550#[cfg(test)]
551mod tests {
552 use std::sync::Arc;
553
554 use super::*;
555 use crate::config::ConfigBuilder;
556 use crate::lockfile::{LockConstraint, LockedPackageHashes};
557 use crate::package::{PackageSpec, RemotePackage};
558
559 fn rockspec(package: &str, dependencies: &[&str]) -> String {
560 let dependencies = if dependencies.is_empty() {
561 String::new()
562 } else {
563 let list = dependencies
564 .iter()
565 .map(|dependency| format!("'{dependency}'"))
566 .collect::<Vec<_>>()
567 .join(", ");
568 format!("\ndependencies = {{ {list} }}")
569 };
570 format!(
571 "rockspec_format = '1.0'\npackage = '{package}'\nversion = '1.0.0-1'\nsource = {{ url = 'https://example.com/{package}.zip' }}{dependencies}"
572 )
573 }
574
575 fn local_packages(rockspecs: &[String]) -> PackageDB {
576 PackageDB::from_local(
577 rockspecs
578 .iter()
579 .map(|content| {
580 let rockspec = match RemoteLuaRockspec::new(content) {
581 Ok(rockspec) => rockspec,
582 Err(err) => unreachable!("invalid test rockspec: {err}"),
583 };
584 FoundPackage {
585 remote: RemotePackage::new(
586 PackageSpec::new(
587 rockspec.package().clone(),
588 rockspec.version().clone(),
589 ),
590 RemotePackageSource::RockspecContent(content.clone()),
591 None,
592 ),
593 rockspec,
594 artifact: None,
595 }
596 })
597 .collect(),
598 )
599 }
600
601 fn install_spec(name: &str) -> PackageInstallSpec {
602 let package = match name.parse::<PackageReq>() {
603 Ok(package) => package,
604 Err(err) => unreachable!("invalid package req: {err}"),
605 };
606 PackageInstallSpec::new(package, EntryType::Entrypoint)
607 .build_behaviour(BuildBehaviour::Ignore)
608 .build()
609 }
610
611 fn installed_package(name: &str) -> LockedPackage {
612 let spec = PackageSpec::parse(name.to_string(), "1.0.0-1".to_string()).unwrap();
613 LockedPackage::from(
614 &spec,
615 LockConstraint::Unconstrained,
616 RemotePackageSource::Test,
617 None,
618 LockedPackageHashes {
619 rockspec: "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
620 .parse()
621 .unwrap(),
622 source: "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
623 .parse()
624 .unwrap(),
625 },
626 )
627 }
628
629 fn roots(spec: PackageInstallSpec) -> Artifacts<Vec<PackageInstallSpec>> {
630 Artifacts {
631 regular: Some(vec![spec]),
632 build: None,
633 test: None,
634 }
635 }
636
637 fn test_config() -> Arc<Config> {
638 let builder = match ConfigBuilder::new() {
639 Ok(builder) => builder,
640 Err(err) => unreachable!("invalid test config: {err}"),
641 };
642 let config = match builder.build() {
643 Ok(config) => config,
644 Err(err) => unreachable!("invalid test config: {err}"),
645 };
646 Arc::new(config)
647 }
648
649 #[tokio::test]
650 async fn diamond_problem() {
651 let packages = local_packages(&[
652 rockspec("a", &["b", "c"]),
653 rockspec("b", &["d"]),
654 rockspec("c", &["d"]),
655 rockspec("d", &[]),
656 ]);
657
658 let resolved =
659 match Resolver::with_max_concurrent(packages, test_config(), 8, HashMap::new())
660 .run(roots(install_spec("a")))
661 .await
662 {
663 Ok(resolved) => resolved,
664 Err(err) => panic!("resolution failed: {err}"),
665 };
666
667 assert_eq!(resolved.regular.as_ref().map(|rocks| rocks.len()), Some(4));
668 }
669
670 #[tokio::test]
671 async fn installed_dependency_is_reused() {
672 let packages = local_packages(&[rockspec("a", &["d"])]);
673 let installed =
674 HashMap::from([(LockedPackageLockType::Regular, vec![installed_package("d")])]);
675
676 let resolved = Resolver::with_max_concurrent(packages, test_config(), 8, installed)
677 .run(roots(install_spec("a")))
678 .await
679 .unwrap();
680
681 assert_eq!(resolved.regular.as_ref().map(|rocks| rocks.len()), Some(1));
682 }
683
684 #[tokio::test]
685 async fn cyclic_dependencies_are_rejected() {
686 let packages = local_packages(&[rockspec("a", &["b"]), rockspec("b", &["a"])]);
687
688 let result = Resolver::with_max_concurrent(packages, test_config(), 8, HashMap::new())
689 .run(roots(install_spec("a")))
690 .await;
691
692 assert!(matches!(result, Err(Error::CyclicDependency(_))));
693 }
694}