1use std::{
2 collections::{HashMap, HashSet},
3 io,
4 sync::Arc,
5};
6
7use crate::{
8 build::{Build, BuildBehaviour, BuildError, RemotePackageSourceSpec, SrcRockSource},
9 config::Config,
10 lockfile::{
11 FlushLockfileError, LocalPackage, LocalPackageId, LockConstraint, Lockfile, OptState,
12 PinnedState, ReadOnly, ReadWrite,
13 },
14 lua_installation::{LuaInstallation, LuaInstallationError},
15 lua_rockspec::BuildBackendSpec,
16 lua_version::LuaVersionUnset,
17 luarocks::{
18 install_binary_rock::{BinaryRockInstall, InstallBinaryRockError},
19 luarocks_installation::{LuaRocksError, LuaRocksInstallError, LuaRocksInstallation},
20 },
21 operations::resolve::{
22 build_dependencies_to_install, PackageInstallData, Resolve, ResolveDependenciesError,
23 },
24 package::{PackageName, PackageNameList, PackageReq},
25 remote_package_db::{RemotePackageDB, RemotePackageDBError, RemotePackageDbIntegrityError},
26 rockspec::Rockspec,
27 tree::{self, InstallTree, Tree, TreeError},
28 workspace::{Workspace, WorkspaceTreeError},
29};
30
31pub use crate::operations::install::spec::PackageInstallSpec;
32
33use super::{DownloadedRockspec, RemoteRockDownload};
34use bon::Builder;
35use bytes::Bytes;
36use futures::stream::FuturesUnordered;
37use futures::StreamExt;
38use itertools::Itertools;
39use miette::Diagnostic;
40use thiserror::Error;
41use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
42use tokio::task::{JoinError, JoinHandle};
43
44use tracing::Instrument;
45pub mod spec;
46
47#[derive(Builder)]
51#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
52pub struct Install<'a, T>
53where
54 T: InstallTree + Clone + Send + Sync,
55{
56 #[builder(start_fn)]
57 config: &'a Config,
58 #[builder(field)]
59 packages: Vec<PackageInstallSpec>,
60 #[builder(setters(name = "_tree", vis = ""))]
61 tree: T,
62 package_db: Option<RemotePackageDB>,
63}
64
65impl<'a, State> InstallBuilder<'a, Tree, State>
66where
67 State: install_builder::State,
68{
69 pub fn workspace(
70 self,
71 workspace: &'a Workspace,
72 ) -> Result<InstallBuilder<'a, Tree, install_builder::SetTree<State>>, WorkspaceTreeError>
73 where
74 State::Tree: install_builder::IsUnset,
75 {
76 let config = self.config;
77 Ok(self._tree(workspace.tree(config)?))
78 }
79}
80
81impl<'a, T, State> InstallBuilder<'a, T, State>
82where
83 State: install_builder::State,
84 T: InstallTree + Clone + Send + Sync,
85{
86 pub fn tree(self, tree: T) -> InstallBuilder<'a, T, install_builder::SetTree<State>>
87 where
88 State::Tree: install_builder::IsUnset,
89 {
90 self._tree(tree)
91 }
92
93 pub fn packages(self, packages: Vec<PackageInstallSpec>) -> Self {
94 Self { packages, ..self }
95 }
96
97 pub fn package(self, package: PackageInstallSpec) -> Self {
98 Self {
99 packages: self
100 .packages
101 .into_iter()
102 .chain(std::iter::once(package))
103 .collect(),
104 ..self
105 }
106 }
107}
108
109impl<State, T> InstallBuilder<'_, T, State>
110where
111 State: install_builder::State + install_builder::IsComplete,
112 T: InstallTree + Clone + Send + Sync + 'static,
113{
114 pub async fn install(self) -> Result<Vec<LocalPackage>, InstallError> {
116 let install_built = self._build();
117 if install_built.packages.is_empty() {
118 return Ok(Vec::default());
119 }
120 let count = install_built.packages.len();
121 let span = if count > 1 {
122 tracing::info_span!("Installing", count)
123 } else {
124 let install_spec = &install_built.packages[0];
125 tracing::info_span!("Installing", package = install_spec.package.to_string())
126 };
127 install_impl(install_built).instrument(span).await
128 }
129}
130
131type InstallWorkerOutput = Result<(LocalPackageId, (LocalPackage, tree::EntryType)), InstallError>;
132
133#[derive(Error, Debug, Diagnostic)]
134pub enum InstallError {
135 #[error("unable to resolve dependencies:\n{0}")]
136 #[diagnostic(forward(0))]
137 ResolveDependencies(#[from] ResolveDependenciesError),
138 #[error(transparent)]
139 #[diagnostic(transparent)]
140 LuaVersionUnset(#[from] LuaVersionUnset),
141 #[error(transparent)]
142 #[diagnostic(transparent)]
143 LuaInstallation(#[from] LuaInstallationError),
144 #[error(transparent)]
145 #[diagnostic(transparent)]
146 FlushLockfile(#[from] FlushLockfileError),
147 #[error(transparent)]
148 #[diagnostic(transparent)]
149 Tree(#[from] TreeError),
150 #[error(transparent)]
151 #[diagnostic(transparent)]
152 WorkspaceTree(#[from] WorkspaceTreeError),
153 #[error("error instantiating LuaRocks compatibility layer:\n{0}")]
154 #[diagnostic(forward(0))]
155 LuaRocks(#[from] LuaRocksError),
156 #[error("error installing LuaRocks compatibility layer:\n{0}")]
157 #[diagnostic(forward(0))]
158 LuaRocksInstall(#[from] LuaRocksInstallError),
159 #[error("failed to build {0}: {1}")]
160 Build(PackageName, BuildError),
161 #[error("failed to install build depencency {0}:\n{1}")]
162 BuildDependency(PackageName, BuildError),
163 #[error("error initialising remote package DB:\n{0}")]
164 #[diagnostic(forward(0))]
165 RemotePackageDB(#[from] RemotePackageDBError),
166 #[error("failed to install pre-built rock {0}:\n{1}")]
167 InstallBinaryRock(PackageName, InstallBinaryRockError),
168 #[error("integrity error for package '{package}'")]
169 Integrity {
170 package: PackageName,
171 #[diagnostic_source]
172 err: RemotePackageDbIntegrityError,
173 },
174 #[error("cannot install duplicate entrypoints:\n{0}")]
175 DuplicateEntrypoints(PackageNameList),
176 #[error("install worker panicked")]
177 #[diagnostic(help(
178 r#"this is a bug in Lux, please report it, ideally with `RUST_BACKTRACE=1`.
179retrying with fewer parallel jobs (`--max-jobs`) may avoid the panic in the meantime"#
180 ))]
181 Join(#[from] tokio::task::JoinError),
182}
183
184async fn install_impl<T>(install: Install<'_, T>) -> Result<Vec<LocalPackage>, InstallError>
185where
186 T: InstallTree + Clone + Send + Sync + 'static,
187{
188 let package_db = match install.package_db {
189 Some(db) => db,
190 None => RemotePackageDB::from_config(install.config).await?,
191 };
192
193 let duplicate_entrypoints = install
194 .packages
195 .iter()
196 .filter(|pkg| pkg.entry_type == tree::EntryType::Entrypoint)
197 .map(|pkg| pkg.package.name())
198 .duplicates()
199 .cloned()
200 .collect_vec();
201
202 if !duplicate_entrypoints.is_empty() {
203 return Err(InstallError::DuplicateEntrypoints(PackageNameList::new(
204 duplicate_entrypoints,
205 )));
206 }
207
208 let packages = install.packages;
209 let package_db = Arc::new(package_db);
210 let config = install.config;
211 let tree = &install.tree;
212
213 let (dep_tx, mut dep_rx) = tokio::sync::mpsc::unbounded_channel();
214 let (build_dep_tx, build_dep_rx) = tokio::sync::mpsc::unbounded_channel();
215 let (build_dep_install_done_tx, mut build_dep_install_done_rx) =
216 tokio::sync::mpsc::unbounded_channel::<PackageName>();
217
218 let lockfile = tree.lockfile()?;
219 let build_lockfile = tree.build_tree(config)?.lockfile()?;
220
221 let lua = Arc::new(LuaInstallation::new_from_config(config).await?);
222
223 let mut resolve_worker = spawn_resolve_worker(
224 config,
225 packages,
226 package_db,
227 lockfile.clone(),
228 build_lockfile.clone(),
229 dep_tx,
230 build_dep_tx,
231 );
232 let mut build_deps_worker = spawn_build_deps_worker(
233 config,
234 tree,
235 lua.clone(),
236 build_dep_rx,
237 build_dep_install_done_tx,
238 );
239
240 let mut all_packages: HashMap<LocalPackageId, PackageInstallData> = HashMap::new();
241 let mut scheduled_packages: HashSet<LocalPackageId> = HashSet::new();
242 let mut installed_packages: HashMap<LocalPackageId, (LocalPackage, tree::EntryType)> =
243 HashMap::new();
244 let mut installed_build_deps: HashSet<PackageName> = HashSet::new();
245 let mut ongoing_installs: FuturesUnordered<
246 tracing::instrument::Instrumented<tokio::task::JoinHandle<InstallWorkerOutput>>,
247 > = FuturesUnordered::new();
248 let mut resolve_done = false;
249 let mut build_deps_done = false;
250 let mut dep_rx_drained = false;
251 let mut build_dep_rx_drained = false;
252 let mut install_loop_result: Result<(), InstallError> = Ok(());
253 let max_jobs = config.max_jobs();
254
255 'install: loop {
256 if resolve_done && build_deps_done && dep_rx_drained && build_dep_rx_drained {
257 break;
258 }
259 tokio::select! {
260 resolve_result = &mut resolve_worker, if !resolve_done => {
261 if let Err(err) = worker_result(resolve_result) {
262 install_loop_result = Err(*err);
263 break 'install;
264 }
265 resolve_done = true;
266 }
267 build_deps_result = &mut build_deps_worker, if !build_deps_done => {
268 if let Err(err) = worker_result(build_deps_result) {
269 install_loop_result = Err(*err);
270 break 'install;
271 }
272 build_deps_done = true;
273 }
274 name = build_dep_install_done_rx.recv(), if !build_dep_rx_drained => {
275 if let Some(name) = name {
276 installed_build_deps.insert(name);
277 } else {
278 build_dep_rx_drained = true;
279 }
280 }
281 dep = dep_rx.recv(), if !dep_rx_drained => {
282 if let Some(dep) = dep {
283 all_packages.insert(dep.spec.id(), dep);
284 } else {
285 dep_rx_drained = true;
286 }
287 }
288 }
289
290 for (package_id, package_install_data) in ready_to_install(
291 &all_packages,
292 &scheduled_packages,
293 &build_lockfile,
294 &installed_build_deps,
295 ) {
296 if max_jobs > 0 && ongoing_installs.len() >= max_jobs {
297 if let Err(err) =
298 wait_for_next_install(&mut ongoing_installs, &mut installed_packages).await
299 {
300 install_loop_result = Err(err);
301 break 'install;
302 }
303 }
304 scheduled_packages.insert(package_id);
305 ongoing_installs.push(spawn_install_worker(
306 package_install_data,
307 &lua,
308 tree,
309 config,
310 ));
311 }
312 }
313
314 match install_loop_result {
315 Ok(_) => {
316 while wait_for_next_install(&mut ongoing_installs, &mut installed_packages).await? {}
317
318 lockfile.map_then_flush(|lockfile| {
319 for (package_id, (package, is_entrypoint)) in installed_packages.iter().unique() {
320 lockfile.add_dependencies(
321 package_id,
322 package,
323 *is_entrypoint,
324 &all_packages,
325 &installed_packages,
326 )?;
327 }
328 Ok::<_, io::Error>(())
329 })?;
330
331 Ok(installed_packages
332 .into_values()
333 .map(|(pkg, _)| pkg)
334 .collect_vec())
335 }
336 Err(err) => {
337 resolve_worker.into_inner().abort();
338 build_deps_worker.into_inner().abort();
339 for install in ongoing_installs {
340 install.into_inner().abort();
341 }
342 Err(err)
343 }
344 }
345}
346
347fn spawn_resolve_worker(
348 config: &Config,
349 packages: Vec<PackageInstallSpec>,
350 package_db: Arc<RemotePackageDB>,
351 lockfile: Lockfile<ReadOnly>,
352 build_lockfile: Lockfile<ReadOnly>,
353 dep_tx: UnboundedSender<PackageInstallData>,
354 build_dep_tx: UnboundedSender<PackageInstallData>,
355) -> tracing::instrument::Instrumented<JoinHandle<Result<(), InstallError>>> {
356 tokio::spawn({
357 let config = config.clone();
358 let lockfile = Arc::new(lockfile);
359 let build_lockfile = Arc::new(build_lockfile);
360 async move {
361 Resolve::new()
362 .dependencies_tx(dep_tx)
363 .build_dependencies_tx(build_dep_tx)
364 .packages(packages)
365 .package_db(package_db)
366 .lockfile(lockfile)
367 .build_lockfile(build_lockfile)
368 .config(&config)
369 .get_all_dependencies()
370 .await?;
371 Ok::<(), InstallError>(())
372 }
373 })
374 .instrument(tracing::trace_span!("resolve_worker"))
375}
376
377fn spawn_build_deps_worker<T>(
378 config: &Config,
379 tree: &T,
380 lua: Arc<LuaInstallation>,
381 mut build_dep_rx: UnboundedReceiver<PackageInstallData>,
382 build_dep_install_done_tx: UnboundedSender<PackageName>,
383) -> tracing::instrument::Instrumented<JoinHandle<Result<(), InstallError>>>
384where
385 T: InstallTree + Clone + Send + Sync + 'static,
386{
387 tokio::spawn({
388 let config = config.clone();
389 let tree = tree.clone();
390 let lua = lua.clone();
391 async move {
392 while let Some(build_dep_spec) = build_dep_rx.recv().await {
393 let rockspec = build_dep_spec.downloaded_rock.rockspec();
394 let package = rockspec.package().clone();
395 let span = tracing::info_span!(
396 "Installing build dependency",
397 package = package.to_string(),
398 version = rockspec.version().to_string()
399 );
400 async {
401 let build_tree = tree.build_tree(&config)?;
402 let mut build_lockfile = build_tree.lockfile()?.write_guard();
403 let pkg = Build::new()
404 .rockspec(rockspec)
405 .lua(&lua)
406 .tree(&build_tree)
407 .entry_type(tree::EntryType::Entrypoint)
408 .config(&config)
409 .constraint(build_dep_spec.spec.constraint())
410 .behaviour(build_dep_spec.build_behaviour)
411 .build()
412 .await
413 .map_err(|err| InstallError::BuildDependency(package.clone(), err))?;
414 build_lockfile.add_entrypoint(&pkg);
415 Ok::<_, InstallError>(())
416 }
417 .instrument(span)
418 .await?;
419 let _ = build_dep_install_done_tx.send(package);
420 }
421 Ok::<(), InstallError>(())
422 }
423 })
424 .instrument(tracing::trace_span!("build_deps_worker"))
425}
426
427fn ready_to_install(
428 all_packages: &HashMap<LocalPackageId, PackageInstallData>,
429 scheduled: &HashSet<LocalPackageId>,
430 build_lockfile: &Lockfile<ReadOnly>,
431 installed_build_deps: &HashSet<PackageName>,
432) -> Vec<(LocalPackageId, PackageInstallData)> {
433 all_packages
434 .iter()
435 .filter(|(id, data)| {
436 !scheduled.contains(*id)
437 && match &data.downloaded_rock {
438 RemoteRockDownload::BinaryRock { .. } => true,
439 _ => build_dependencies_ready(
440 data.downloaded_rock.rockspec(),
441 data.build_behaviour,
442 build_lockfile,
443 installed_build_deps,
444 ),
445 }
446 })
447 .map(|(id, data)| (id.clone(), data.clone()))
448 .collect()
449}
450
451fn spawn_install_worker<T>(
452 data: PackageInstallData,
453 lua: &Arc<LuaInstallation>,
454 tree: &T,
455 config: &Config,
456) -> tracing::instrument::Instrumented<JoinHandle<InstallWorkerOutput>>
457where
458 T: InstallTree + Clone + Send + Sync + 'static,
459{
460 let config = config.clone();
461 let tree = tree.clone();
462 let lua = lua.clone();
463 let entry_type = data.entry_type;
464 tokio::spawn(async move {
465 let pkg = install_package(data, &lua, &tree, &config).await?;
466 Ok::<_, InstallError>((pkg.id(), (pkg, entry_type)))
467 })
468 .instrument(tracing::trace_span!("install_worker"))
469}
470
471#[tracing::instrument(level = "trace", skip_all)]
472async fn install_package<T>(
473 data: PackageInstallData,
474 lua: &Arc<LuaInstallation>,
475 tree: &T,
476 config: &Config,
477) -> Result<LocalPackage, InstallError>
478where
479 T: InstallTree + Sync,
480{
481 match data.downloaded_rock {
482 RemoteRockDownload::RockspecOnly { rockspec_download } => {
483 install_rockspec(
484 rockspec_download,
485 None,
486 data.spec.constraint(),
487 data.build_behaviour,
488 data.pin,
489 data.opt,
490 data.entry_type,
491 lua,
492 tree,
493 config,
494 )
495 .await
496 }
497 RemoteRockDownload::BinaryRock {
498 rockspec_download,
499 packed_rock,
500 } => {
501 install_binary_rock(
502 rockspec_download,
503 packed_rock,
504 data.spec.constraint(),
505 data.build_behaviour,
506 data.pin,
507 data.opt,
508 data.entry_type,
509 config,
510 tree,
511 )
512 .await
513 }
514 RemoteRockDownload::SrcRock {
515 rockspec_download,
516 src_rock,
517 source_url,
518 } => {
519 let src_rock_source = SrcRockSource {
520 bytes: src_rock,
521 source_url,
522 };
523 install_rockspec(
524 rockspec_download,
525 Some(src_rock_source),
526 data.spec.constraint(),
527 data.build_behaviour,
528 data.pin,
529 data.opt,
530 data.entry_type,
531 lua,
532 tree,
533 config,
534 )
535 .await
536 }
537 }
538}
539
540fn worker_result(
541 result: Result<Result<(), InstallError>, JoinError>,
542) -> Result<(), Box<InstallError>> {
543 match result {
544 Ok(Ok(())) => Ok(()),
545 Ok(Err(err)) => Err(err.into()),
546 Err(join) => Err(InstallError::from(join).into()),
547 }
548}
549
550async fn wait_for_next_install(
551 ongoing_installs: &mut FuturesUnordered<
552 tracing::instrument::Instrumented<tokio::task::JoinHandle<InstallWorkerOutput>>,
553 >,
554 installed_packages: &mut HashMap<LocalPackageId, (LocalPackage, tree::EntryType)>,
555) -> Result<bool, InstallError> {
556 if let Some(result) = ongoing_installs.next().await {
557 match result {
558 Ok(Ok((id, installed))) => {
559 installed_packages.insert(id, installed);
560 Ok(true)
561 }
562 Ok(Err(err)) => Err(err),
563 Err(join) => Err(InstallError::from(join)),
564 }
565 } else {
566 Ok(false)
567 }
568}
569trait LockfileExt {
570 fn add_dependencies(
571 self,
572 id: &LocalPackageId,
573 pkg: &LocalPackage,
574 entry_type: tree::EntryType,
575 all_packages: &HashMap<LocalPackageId, PackageInstallData>,
576 installed_packages: &HashMap<LocalPackageId, (LocalPackage, tree::EntryType)>,
577 ) -> io::Result<()>;
578}
579
580impl LockfileExt for &mut Lockfile<ReadWrite> {
581 fn add_dependencies(
582 self,
583 id: &LocalPackageId,
584 pkg: &LocalPackage,
585 entry_type: tree::EntryType,
586 all_packages: &HashMap<LocalPackageId, PackageInstallData>,
587 installed_packages: &HashMap<LocalPackageId, (LocalPackage, tree::EntryType)>,
588 ) -> io::Result<()> {
589 if entry_type == tree::EntryType::Entrypoint {
590 self.add_entrypoint(pkg);
591 }
592
593 for dependency_id in all_packages
594 .get(id)
595 .map(|pkg| pkg.spec.dependencies())
596 .unwrap_or_default()
597 .into_iter()
598 {
599 self.add_dependency(
600 pkg,
601 installed_packages
602 .get(dependency_id)
603 .map(|(pkg, _)| pkg)
604 .ok_or(io::Error::other(
605 r#"
606error writing dependencies to the lockfile.
607A required dependency was not installed correctly.
608This is likely because an install thread panicked and was interrupted unexpectedly.
609
610[THIS IS A BUG!]
611"#,
612 ))?,
613 );
614 }
615 Ok(())
616 }
617}
618
619fn build_dependencies_ready(
626 rockspec: &impl Rockspec,
627 behaviour: BuildBehaviour,
628 build_lockfile: &Lockfile<ReadOnly>,
629 installed: &HashSet<PackageName>,
630) -> bool {
631 let build_deps = rockspec.build_dependencies().current_platform();
632 build_dependencies_to_install(rockspec).iter().all(|name| {
633 installed.contains(name)
634 || (behaviour != BuildBehaviour::Force
635 && build_lockfile
636 .has_rock(
637 &build_deps
638 .iter()
639 .find(|dep| dep.name() == name)
640 .map(|dep| dep.package_req().clone())
641 .unwrap_or_else(|| PackageReq::from(name.clone())),
642 None,
643 )
644 .is_some())
645 })
646}
647
648#[allow(clippy::too_many_arguments)]
649#[tracing::instrument(
650 name = "Installing",
651 level = "info",
652 skip_all,
653 fields(
654 package = rockspec_download.rockspec.package().to_string(),
655 version = rockspec_download.rockspec.version().to_string(),
656 ),
657)]
658async fn install_rockspec<T>(
659 rockspec_download: DownloadedRockspec,
660 src_rock_source: Option<SrcRockSource>,
661 constraint: LockConstraint,
662 behaviour: BuildBehaviour,
663 pin: PinnedState,
664 opt: OptState,
665 entry_type: tree::EntryType,
666 lua: &LuaInstallation,
667 tree: &T,
668 config: &Config,
669) -> Result<LocalPackage, InstallError>
670where
671 T: InstallTree + Sync,
672{
673 let package = rockspec_download.rockspec.package().clone();
674 let rockspec = rockspec_download.rockspec;
675 let source = rockspec_download.source;
676
677 if let Some(BuildBackendSpec::LuaRock(_)) = &rockspec.build().current_platform().build_backend {
678 let luarocks_tree = tree.build_tree(config)?;
679 let luarocks = LuaRocksInstallation::new(config, luarocks_tree)?;
680 luarocks.ensure_installed(lua).await?;
681 }
682
683 let source_spec = match src_rock_source {
684 Some(src_rock_source) => RemotePackageSourceSpec::SrcRock(src_rock_source),
685 None => RemotePackageSourceSpec::RockSpec(rockspec_download.source_url),
686 };
687
688 let pkg = Build::new()
689 .rockspec(&rockspec)
690 .lua(lua)
691 .tree(tree)
692 .entry_type(entry_type)
693 .config(config)
694 .pin(pin)
695 .opt(opt)
696 .constraint(constraint)
697 .behaviour(behaviour)
698 .source(source)
699 .source_spec(source_spec)
700 .build()
701 .await
702 .map_err(|err| InstallError::Build(package, err))?;
703 Ok(pkg)
704}
705
706#[allow(clippy::too_many_arguments)]
707#[tracing::instrument(
708 name = "Installing (pre-built)",
709 level = "info",
710 skip_all,
711 fields(
712 package = rockspec_download.rockspec.package().to_string(),
713 version = rockspec_download.rockspec.version().to_string(),
714 ),
715)]
716async fn install_binary_rock(
717 rockspec_download: DownloadedRockspec,
718 packed_rock: Bytes,
719 constraint: LockConstraint,
720 behaviour: BuildBehaviour,
721 pin: PinnedState,
722 opt: OptState,
723 entry_type: tree::EntryType,
724 config: &Config,
725 tree: &impl InstallTree,
726) -> Result<LocalPackage, InstallError> {
727 let rockspec = rockspec_download.rockspec;
728 let package = rockspec.package().clone();
729 let pkg = BinaryRockInstall::new(
730 &rockspec,
731 rockspec_download.source,
732 packed_rock,
733 entry_type,
734 config,
735 tree,
736 )
737 .pin(pin)
738 .opt(opt)
739 .constraint(constraint)
740 .behaviour(behaviour)
741 .install()
742 .await
743 .map_err(|err| InstallError::InstallBinaryRock(package, err))?;
744 Ok(pkg)
745}