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},
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")]
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")]
154 #[diagnostic(forward(0))]
155 LuaRocks(#[from] LuaRocksError),
156 #[error("error installing LuaRocks compatibility layer")]
157 #[diagnostic(forward(0))]
158 LuaRocksInstall(#[from] LuaRocksInstallError),
159 #[error("failed to build {0}")]
160 Build(PackageName, #[source] BuildError),
161 #[error("failed to install build depencency {0}")]
162 BuildDependency(PackageName, #[source] BuildError),
163 #[error("error initialising remote package DB")]
164 #[diagnostic(forward(0))]
165 RemotePackageDB(#[from] RemotePackageDBError),
166 #[error("failed to install pre-built rock {0}")]
167 InstallBinaryRock(PackageName, #[source] InstallBinaryRockError),
168 #[error("cannot install duplicate entrypoints:\n{0}")]
169 DuplicateEntrypoints(PackageNameList),
170 #[error("install worker panicked")]
171 #[diagnostic(help(
172 r#"this is a bug in Lux, please report it, ideally with `RUST_BACKTRACE=1`.
173retrying with fewer parallel jobs (`--max-jobs`) may avoid the panic in the meantime"#
174 ))]
175 Join(#[from] tokio::task::JoinError),
176}
177
178async fn install_impl<T>(install: Install<'_, T>) -> Result<Vec<LocalPackage>, InstallError>
179where
180 T: InstallTree + Clone + Send + Sync + 'static,
181{
182 let package_db = match install.package_db {
183 Some(db) => db,
184 None => RemotePackageDB::from_config(install.config).await?,
185 };
186
187 let duplicate_entrypoints = install
188 .packages
189 .iter()
190 .filter(|pkg| pkg.entry_type == tree::EntryType::Entrypoint)
191 .map(|pkg| pkg.package.name())
192 .duplicates()
193 .cloned()
194 .collect_vec();
195
196 if !duplicate_entrypoints.is_empty() {
197 return Err(InstallError::DuplicateEntrypoints(PackageNameList::new(
198 duplicate_entrypoints,
199 )));
200 }
201
202 let packages = install.packages;
203 let package_db = Arc::new(package_db);
204 let config = install.config;
205 let tree = &install.tree;
206
207 let (dep_tx, mut dep_rx) = tokio::sync::mpsc::unbounded_channel();
208 let (build_dep_tx, build_dep_rx) = tokio::sync::mpsc::unbounded_channel();
209 let (build_dep_install_done_tx, mut build_dep_install_done_rx) =
210 tokio::sync::mpsc::unbounded_channel::<PackageName>();
211
212 let lockfile = tree.lockfile()?;
213 let build_lockfile = tree.build_tree(config)?.lockfile()?;
214
215 let lua = Arc::new(LuaInstallation::new_from_config(config).await?);
216
217 let mut resolve_worker = spawn_resolve_worker(
218 config,
219 packages,
220 package_db,
221 lockfile.clone(),
222 build_lockfile.clone(),
223 dep_tx,
224 build_dep_tx,
225 );
226 let mut build_deps_worker = spawn_build_deps_worker(
227 config,
228 tree,
229 lua.clone(),
230 build_dep_rx,
231 build_dep_install_done_tx,
232 );
233
234 let mut all_packages: HashMap<LocalPackageId, PackageInstallData> = HashMap::new();
235 let mut scheduled_packages: HashSet<LocalPackageId> = HashSet::new();
236 let mut installed_packages: HashMap<LocalPackageId, (LocalPackage, tree::EntryType)> =
237 HashMap::new();
238 let mut installed_build_deps: HashSet<PackageName> = HashSet::new();
239 let mut ongoing_installs: FuturesUnordered<
240 tracing::instrument::Instrumented<tokio::task::JoinHandle<InstallWorkerOutput>>,
241 > = FuturesUnordered::new();
242 let mut resolve_done = false;
243 let mut build_deps_done = false;
244 let mut dep_rx_drained = false;
245 let mut build_dep_rx_drained = false;
246 let mut install_loop_result: Result<(), InstallError> = Ok(());
247 let max_jobs = config.max_jobs();
248
249 'install: loop {
250 if resolve_done && build_deps_done && dep_rx_drained && build_dep_rx_drained {
251 break;
252 }
253 tokio::select! {
254 resolve_result = &mut resolve_worker, if !resolve_done => {
255 if let Err(err) = worker_result(resolve_result) {
256 install_loop_result = Err(*err);
257 break 'install;
258 }
259 resolve_done = true;
260 }
261 build_deps_result = &mut build_deps_worker, if !build_deps_done => {
262 if let Err(err) = worker_result(build_deps_result) {
263 install_loop_result = Err(*err);
264 break 'install;
265 }
266 build_deps_done = true;
267 }
268 name = build_dep_install_done_rx.recv(), if !build_dep_rx_drained => {
269 if let Some(name) = name {
270 installed_build_deps.insert(name);
271 } else {
272 build_dep_rx_drained = true;
273 }
274 }
275 dep = dep_rx.recv(), if !dep_rx_drained => {
276 if let Some(dep) = dep {
277 all_packages.insert(dep.spec.id(), dep);
278 } else {
279 dep_rx_drained = true;
280 }
281 }
282 }
283
284 for (package_id, package_install_data) in ready_to_install(
285 &all_packages,
286 &scheduled_packages,
287 &build_lockfile,
288 &installed_build_deps,
289 ) {
290 if max_jobs > 0 && ongoing_installs.len() >= max_jobs {
291 if let Err(err) =
292 wait_for_next_install(&mut ongoing_installs, &mut installed_packages).await
293 {
294 install_loop_result = Err(err);
295 break 'install;
296 }
297 }
298 scheduled_packages.insert(package_id);
299 ongoing_installs.push(spawn_install_worker(
300 package_install_data,
301 &lua,
302 tree,
303 config,
304 ));
305 }
306 }
307
308 match install_loop_result {
309 Ok(_) => {
310 while wait_for_next_install(&mut ongoing_installs, &mut installed_packages).await? {}
311
312 lockfile.map_then_flush(|lockfile| {
313 for (package_id, (package, is_entrypoint)) in installed_packages.iter().unique() {
314 lockfile.add_dependencies(
315 package_id,
316 package,
317 *is_entrypoint,
318 &all_packages,
319 &installed_packages,
320 )?;
321 }
322 Ok::<_, io::Error>(())
323 })?;
324
325 Ok(installed_packages
326 .into_values()
327 .map(|(pkg, _)| pkg)
328 .collect_vec())
329 }
330 Err(err) => {
331 resolve_worker.into_inner().abort();
332 build_deps_worker.into_inner().abort();
333 for install in ongoing_installs {
334 install.into_inner().abort();
335 }
336 Err(err)
337 }
338 }
339}
340
341fn spawn_resolve_worker(
342 config: &Config,
343 packages: Vec<PackageInstallSpec>,
344 package_db: Arc<RemotePackageDB>,
345 lockfile: Lockfile<ReadOnly>,
346 build_lockfile: Lockfile<ReadOnly>,
347 dep_tx: UnboundedSender<PackageInstallData>,
348 build_dep_tx: UnboundedSender<PackageInstallData>,
349) -> tracing::instrument::Instrumented<JoinHandle<Result<(), InstallError>>> {
350 tokio::spawn({
351 let config = config.clone();
352 let lockfile = Arc::new(lockfile);
353 let build_lockfile = Arc::new(build_lockfile);
354 async move {
355 Resolve::new()
356 .dependencies_tx(dep_tx)
357 .build_dependencies_tx(build_dep_tx)
358 .packages(packages)
359 .package_db(package_db)
360 .lockfile(lockfile)
361 .build_lockfile(build_lockfile)
362 .config(&config)
363 .get_all_dependencies()
364 .await?;
365 Ok::<(), InstallError>(())
366 }
367 })
368 .instrument(tracing::trace_span!("resolve_worker"))
369}
370
371fn spawn_build_deps_worker<T>(
372 config: &Config,
373 tree: &T,
374 lua: Arc<LuaInstallation>,
375 mut build_dep_rx: UnboundedReceiver<PackageInstallData>,
376 build_dep_install_done_tx: UnboundedSender<PackageName>,
377) -> tracing::instrument::Instrumented<JoinHandle<Result<(), InstallError>>>
378where
379 T: InstallTree + Clone + Send + Sync + 'static,
380{
381 tokio::spawn({
382 let config = config.clone();
383 let tree = tree.clone();
384 let lua = lua.clone();
385 async move {
386 while let Some(build_dep_spec) = build_dep_rx.recv().await {
387 let rockspec = build_dep_spec.downloaded_rock.rockspec();
388 let package = rockspec.package().clone();
389 let span = tracing::info_span!(
390 "Installing build dependency",
391 package = package.to_string(),
392 version = rockspec.version().to_string()
393 );
394 async {
395 let build_tree = tree.build_tree(&config)?;
396 let mut build_lockfile = build_tree.lockfile()?.write_guard();
397 let pkg = Build::new()
398 .rockspec(rockspec)
399 .lua(&lua)
400 .tree(&build_tree)
401 .entry_type(tree::EntryType::Entrypoint)
402 .config(&config)
403 .constraint(build_dep_spec.spec.constraint())
404 .behaviour(build_dep_spec.build_behaviour)
405 .build()
406 .await
407 .map_err(|err| InstallError::BuildDependency(package.clone(), err))?;
408 build_lockfile.add_entrypoint(&pkg);
409 Ok::<_, InstallError>(())
410 }
411 .instrument(span)
412 .await?;
413 let _ = build_dep_install_done_tx.send(package);
414 }
415 Ok::<(), InstallError>(())
416 }
417 })
418 .instrument(tracing::trace_span!("build_deps_worker"))
419}
420
421fn ready_to_install(
422 all_packages: &HashMap<LocalPackageId, PackageInstallData>,
423 scheduled: &HashSet<LocalPackageId>,
424 build_lockfile: &Lockfile<ReadOnly>,
425 installed_build_deps: &HashSet<PackageName>,
426) -> Vec<(LocalPackageId, PackageInstallData)> {
427 all_packages
428 .iter()
429 .filter(|(id, data)| {
430 !scheduled.contains(*id)
431 && match &data.downloaded_rock {
432 RemoteRockDownload::BinaryRock { .. } => true,
433 _ => build_dependencies_ready(
434 data.downloaded_rock.rockspec(),
435 data.build_behaviour,
436 build_lockfile,
437 installed_build_deps,
438 ),
439 }
440 })
441 .map(|(id, data)| (id.clone(), data.clone()))
442 .collect()
443}
444
445fn spawn_install_worker<T>(
446 data: PackageInstallData,
447 lua: &Arc<LuaInstallation>,
448 tree: &T,
449 config: &Config,
450) -> tracing::instrument::Instrumented<JoinHandle<InstallWorkerOutput>>
451where
452 T: InstallTree + Clone + Send + Sync + 'static,
453{
454 let config = config.clone();
455 let tree = tree.clone();
456 let lua = lua.clone();
457 let entry_type = data.entry_type;
458 tokio::spawn(async move {
459 let pkg = install_package(data, &lua, &tree, &config).await?;
460 Ok::<_, InstallError>((pkg.id(), (pkg, entry_type)))
461 })
462 .instrument(tracing::trace_span!("install_worker"))
463}
464
465#[tracing::instrument(level = "trace", skip_all)]
466async fn install_package<T>(
467 data: PackageInstallData,
468 lua: &Arc<LuaInstallation>,
469 tree: &T,
470 config: &Config,
471) -> Result<LocalPackage, InstallError>
472where
473 T: InstallTree + Sync,
474{
475 match data.downloaded_rock {
476 RemoteRockDownload::RockspecOnly { rockspec_download } => {
477 install_rockspec(
478 rockspec_download,
479 None,
480 data.spec.constraint(),
481 data.build_behaviour,
482 data.pin,
483 data.opt,
484 data.entry_type,
485 lua,
486 tree,
487 config,
488 )
489 .await
490 }
491 RemoteRockDownload::BinaryRock {
492 rockspec_download,
493 packed_rock,
494 } => {
495 install_binary_rock(
496 rockspec_download,
497 packed_rock,
498 data.spec.constraint(),
499 data.build_behaviour,
500 data.pin,
501 data.opt,
502 data.entry_type,
503 config,
504 tree,
505 )
506 .await
507 }
508 RemoteRockDownload::SrcRock {
509 rockspec_download,
510 src_rock,
511 source_url,
512 } => {
513 let src_rock_source = SrcRockSource {
514 bytes: src_rock,
515 source_url,
516 };
517 install_rockspec(
518 rockspec_download,
519 Some(src_rock_source),
520 data.spec.constraint(),
521 data.build_behaviour,
522 data.pin,
523 data.opt,
524 data.entry_type,
525 lua,
526 tree,
527 config,
528 )
529 .await
530 }
531 }
532}
533
534fn worker_result(
535 result: Result<Result<(), InstallError>, JoinError>,
536) -> Result<(), Box<InstallError>> {
537 match result {
538 Ok(Ok(())) => Ok(()),
539 Ok(Err(err)) => Err(err.into()),
540 Err(join) => Err(InstallError::from(join).into()),
541 }
542}
543
544async fn wait_for_next_install(
545 ongoing_installs: &mut FuturesUnordered<
546 tracing::instrument::Instrumented<tokio::task::JoinHandle<InstallWorkerOutput>>,
547 >,
548 installed_packages: &mut HashMap<LocalPackageId, (LocalPackage, tree::EntryType)>,
549) -> Result<bool, InstallError> {
550 if let Some(result) = ongoing_installs.next().await {
551 match result {
552 Ok(Ok((id, installed))) => {
553 installed_packages.insert(id, installed);
554 Ok(true)
555 }
556 Ok(Err(err)) => Err(err),
557 Err(join) => Err(InstallError::from(join)),
558 }
559 } else {
560 Ok(false)
561 }
562}
563trait LockfileExt {
564 fn add_dependencies(
565 self,
566 id: &LocalPackageId,
567 pkg: &LocalPackage,
568 entry_type: tree::EntryType,
569 all_packages: &HashMap<LocalPackageId, PackageInstallData>,
570 installed_packages: &HashMap<LocalPackageId, (LocalPackage, tree::EntryType)>,
571 ) -> io::Result<()>;
572}
573
574impl LockfileExt for &mut Lockfile<ReadWrite> {
575 fn add_dependencies(
576 self,
577 id: &LocalPackageId,
578 pkg: &LocalPackage,
579 entry_type: tree::EntryType,
580 all_packages: &HashMap<LocalPackageId, PackageInstallData>,
581 installed_packages: &HashMap<LocalPackageId, (LocalPackage, tree::EntryType)>,
582 ) -> io::Result<()> {
583 if entry_type == tree::EntryType::Entrypoint {
584 self.add_entrypoint(pkg);
585 }
586
587 for dependency_id in all_packages
588 .get(id)
589 .map(|pkg| pkg.spec.dependencies())
590 .unwrap_or_default()
591 .into_iter()
592 {
593 self.add_dependency(
594 pkg,
595 installed_packages
596 .get(dependency_id)
597 .map(|(pkg, _)| pkg)
598 .ok_or(io::Error::other(
599 r#"
600error writing dependencies to the lockfile.
601A required dependency was not installed correctly.
602This is likely because an install thread panicked and was interrupted unexpectedly.
603
604[THIS IS A BUG!]
605"#,
606 ))?,
607 );
608 }
609 Ok(())
610 }
611}
612
613fn build_dependencies_ready(
620 rockspec: &impl Rockspec,
621 behaviour: BuildBehaviour,
622 build_lockfile: &Lockfile<ReadOnly>,
623 installed: &HashSet<PackageName>,
624) -> bool {
625 let build_deps = rockspec.build_dependencies().current_platform();
626 build_dependencies_to_install(rockspec).iter().all(|name| {
627 installed.contains(name)
628 || (behaviour != BuildBehaviour::Force
629 && build_lockfile
630 .has_rock(
631 &build_deps
632 .iter()
633 .find(|dep| dep.name() == name)
634 .map(|dep| dep.package_req().clone())
635 .unwrap_or_else(|| PackageReq::from(name.clone())),
636 None,
637 )
638 .is_some())
639 })
640}
641
642#[allow(clippy::too_many_arguments)]
643#[tracing::instrument(
644 name = "Installing",
645 level = "info",
646 skip_all,
647 fields(
648 package = rockspec_download.rockspec.package().to_string(),
649 version = rockspec_download.rockspec.version().to_string(),
650 ),
651)]
652async fn install_rockspec<T>(
653 rockspec_download: DownloadedRockspec,
654 src_rock_source: Option<SrcRockSource>,
655 constraint: LockConstraint,
656 behaviour: BuildBehaviour,
657 pin: PinnedState,
658 opt: OptState,
659 entry_type: tree::EntryType,
660 lua: &LuaInstallation,
661 tree: &T,
662 config: &Config,
663) -> Result<LocalPackage, InstallError>
664where
665 T: InstallTree + Sync,
666{
667 let package = rockspec_download.rockspec.package().clone();
668 let rockspec = rockspec_download.rockspec;
669 let source = rockspec_download.source;
670
671 if let Some(BuildBackendSpec::LuaRock(_)) = &rockspec.build().current_platform().build_backend {
672 let luarocks_tree = tree.build_tree(config)?;
673 let luarocks = LuaRocksInstallation::new(config, luarocks_tree)?;
674 luarocks.ensure_installed(lua).await?;
675 }
676
677 let source_spec = match src_rock_source {
678 Some(src_rock_source) => RemotePackageSourceSpec::SrcRock(src_rock_source),
679 None => RemotePackageSourceSpec::RockSpec(rockspec_download.source_url),
680 };
681
682 let pkg = Build::new()
683 .rockspec(&rockspec)
684 .lua(lua)
685 .tree(tree)
686 .entry_type(entry_type)
687 .config(config)
688 .pin(pin)
689 .opt(opt)
690 .constraint(constraint)
691 .behaviour(behaviour)
692 .source(source)
693 .source_spec(source_spec)
694 .build()
695 .await
696 .map_err(|err| InstallError::Build(package, err))?;
697 Ok(pkg)
698}
699
700#[allow(clippy::too_many_arguments)]
701#[tracing::instrument(
702 name = "Installing (pre-built)",
703 level = "info",
704 skip_all,
705 fields(
706 package = rockspec_download.rockspec.package().to_string(),
707 version = rockspec_download.rockspec.version().to_string(),
708 ),
709)]
710async fn install_binary_rock(
711 rockspec_download: DownloadedRockspec,
712 packed_rock: Bytes,
713 constraint: LockConstraint,
714 behaviour: BuildBehaviour,
715 pin: PinnedState,
716 opt: OptState,
717 entry_type: tree::EntryType,
718 config: &Config,
719 tree: &impl InstallTree,
720) -> Result<LocalPackage, InstallError> {
721 let rockspec = rockspec_download.rockspec;
722 let package = rockspec.package().clone();
723 let pkg = BinaryRockInstall::new(
724 &rockspec,
725 rockspec_download.source,
726 packed_rock,
727 entry_type,
728 config,
729 tree,
730 )
731 .pin(pin)
732 .opt(opt)
733 .constraint(constraint)
734 .behaviour(behaviour)
735 .install()
736 .await
737 .map_err(|err| InstallError::InstallBinaryRock(package, err))?;
738 Ok(pkg)
739}