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