1use std::{collections::HashMap, io, sync::Arc};
2
3use crate::{
4 build::{Build, BuildBehaviour, BuildError, RemotePackageSourceSpec, SrcRockSource},
5 config::Config,
6 lockfile::{
7 FlushLockfileError, LocalPackage, LocalPackageId, LockConstraint, Lockfile, OptState,
8 PinnedState, ReadWrite,
9 },
10 lua_installation::{LuaInstallation, LuaInstallationError},
11 lua_rockspec::BuildBackendSpec,
12 lua_version::LuaVersionUnset,
13 luarocks::{
14 install_binary_rock::{BinaryRockInstall, InstallBinaryRockError},
15 luarocks_installation::{LuaRocksError, LuaRocksInstallError, LuaRocksInstallation},
16 },
17 operations::resolve::{Resolve, ResolveDependenciesError},
18 package::{PackageName, PackageNameList},
19 progress::{MultiProgress, Progress, ProgressBar},
20 remote_package_db::{RemotePackageDB, RemotePackageDBError, RemotePackageDbIntegrityError},
21 rockspec::Rockspec,
22 tree::{self, InstallTree, Tree, TreeError},
23 workspace::{Workspace, WorkspaceTreeError},
24};
25
26pub use crate::operations::install::spec::PackageInstallSpec;
27
28use super::{DownloadedRockspec, RemoteRockDownload};
29use bon::Builder;
30use bytes::Bytes;
31use futures::StreamExt;
32use itertools::Itertools;
33use miette::Diagnostic;
34use thiserror::Error;
35
36pub mod spec;
37
38#[derive(Builder)]
42#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
43pub struct Install<'a, T>
44where
45 T: InstallTree + Clone + Send + Sync,
46{
47 #[builder(start_fn)]
48 config: &'a Config,
49 #[builder(field)]
50 packages: Vec<PackageInstallSpec>,
51 #[builder(setters(name = "_tree", vis = ""))]
52 tree: T,
53 package_db: Option<RemotePackageDB>,
54 progress: Option<Arc<Progress<MultiProgress>>>,
55}
56
57impl<'a, State> InstallBuilder<'a, Tree, State>
58where
59 State: install_builder::State,
60{
61 pub fn workspace(
62 self,
63 workspace: &'a Workspace,
64 ) -> Result<InstallBuilder<'a, Tree, install_builder::SetTree<State>>, WorkspaceTreeError>
65 where
66 State::Tree: install_builder::IsUnset,
67 {
68 let config = self.config;
69 Ok(self._tree(workspace.tree(config)?))
70 }
71}
72
73impl<'a, T, State> InstallBuilder<'a, T, State>
74where
75 State: install_builder::State,
76 T: InstallTree + Clone + Send + Sync,
77{
78 pub fn tree(self, tree: T) -> InstallBuilder<'a, T, install_builder::SetTree<State>>
79 where
80 State::Tree: install_builder::IsUnset,
81 {
82 self._tree(tree)
83 }
84
85 pub fn packages(self, packages: Vec<PackageInstallSpec>) -> Self {
86 Self { packages, ..self }
87 }
88
89 pub fn package(self, package: PackageInstallSpec) -> Self {
90 Self {
91 packages: self
92 .packages
93 .into_iter()
94 .chain(std::iter::once(package))
95 .collect(),
96 ..self
97 }
98 }
99}
100
101impl<State, T> InstallBuilder<'_, T, State>
102where
103 State: install_builder::State + install_builder::IsComplete,
104 T: InstallTree + Clone + Send + Sync + 'static,
105{
106 pub async fn install(self) -> Result<Vec<LocalPackage>, InstallError> {
108 let install_built = self._build();
109 if install_built.packages.is_empty() {
110 return Ok(Vec::default());
111 }
112 let progress = match install_built.progress {
113 Some(p) => p,
114 None => MultiProgress::new_arc(install_built.config),
115 };
116 let package_db = match install_built.package_db {
117 Some(db) => db,
118 None => {
119 let bar = progress.map(|p| p.new_bar());
120 RemotePackageDB::from_config(install_built.config, &bar).await?
121 }
122 };
123
124 let duplicate_entrypoints = install_built
125 .packages
126 .iter()
127 .filter(|pkg| pkg.entry_type == tree::EntryType::Entrypoint)
128 .map(|pkg| pkg.package.name())
129 .duplicates()
130 .cloned()
131 .collect_vec();
132
133 if !duplicate_entrypoints.is_empty() {
134 return Err(InstallError::DuplicateEntrypoints(PackageNameList::new(
135 duplicate_entrypoints,
136 )));
137 }
138
139 install_impl(
140 install_built.packages,
141 Arc::new(package_db),
142 install_built.config,
143 &install_built.tree,
144 progress,
145 )
146 .await
147 }
148}
149
150#[derive(Error, Debug, Diagnostic)]
151pub enum InstallError {
152 #[error("unable to resolve dependencies:\n{0}")]
153 #[diagnostic(forward(0))]
154 ResolveDependencies(#[from] ResolveDependenciesError),
155 #[error(transparent)]
156 #[diagnostic(transparent)]
157 LuaVersionUnset(#[from] LuaVersionUnset),
158 #[error(transparent)]
159 #[diagnostic(transparent)]
160 LuaInstallation(#[from] LuaInstallationError),
161 #[error(transparent)]
162 #[diagnostic(transparent)]
163 FlushLockfile(#[from] FlushLockfileError),
164 #[error(transparent)]
165 #[diagnostic(transparent)]
166 Tree(#[from] TreeError),
167 #[error(transparent)]
168 #[diagnostic(transparent)]
169 WorkspaceTree(#[from] WorkspaceTreeError),
170 #[error("error instantiating LuaRocks compatibility layer:\n{0}")]
171 #[diagnostic(forward(0))]
172 LuaRocks(#[from] LuaRocksError),
173 #[error("error installing LuaRocks compatibility layer:\n{0}")]
174 #[diagnostic(forward(0))]
175 LuaRocksInstall(#[from] LuaRocksInstallError),
176 #[error("failed to build {0}: {1}")]
177 Build(PackageName, BuildError),
178 #[error("failed to install build depencency {0}:\n{1}")]
179 BuildDependency(PackageName, BuildError),
180 #[error("error initialising remote package DB:\n{0}")]
181 #[diagnostic(forward(0))]
182 RemotePackageDB(#[from] RemotePackageDBError),
183 #[error("failed to install pre-built rock {0}:\n{1}")]
184 InstallBinaryRock(PackageName, InstallBinaryRockError),
185 #[error("integrity error for package '{package}'")]
186 Integrity {
187 package: PackageName,
188 #[diagnostic_source]
189 err: RemotePackageDbIntegrityError,
190 },
191 #[error("cannot install duplicate entrypoints:\n{0}")]
192 DuplicateEntrypoints(PackageNameList),
193}
194
195#[allow(clippy::too_many_arguments)]
197async fn install_impl<T>(
198 packages: Vec<PackageInstallSpec>,
199 package_db: Arc<RemotePackageDB>,
200 config: &Config,
201 tree: &T,
202 progress_arc: Arc<Progress<MultiProgress>>,
203) -> Result<Vec<LocalPackage>, InstallError>
204where
205 T: InstallTree + Clone + Send + Sync + 'static,
206{
207 let (dep_tx, mut dep_rx) = tokio::sync::mpsc::unbounded_channel();
208 let (build_dep_tx, mut build_dep_rx) = tokio::sync::mpsc::unbounded_channel();
209
210 let lockfile = tree.lockfile()?;
211 let build_lockfile = tree.build_tree(config)?.lockfile()?;
212
213 Resolve::new()
214 .dependencies_tx(dep_tx)
215 .build_dependencies_tx(build_dep_tx)
216 .packages(packages)
217 .package_db(package_db.clone())
218 .lockfile(Arc::new(lockfile.clone()))
219 .build_lockfile(Arc::new(build_lockfile.clone()))
220 .config(config)
221 .progress(progress_arc.clone())
222 .get_all_dependencies()
223 .await?;
224
225 let lua = Arc::new(
226 LuaInstallation::new_from_config(config, &progress_arc.map(|progress| progress.new_bar()))
227 .await?,
228 );
229
230 while let Some(build_dep_spec) = build_dep_rx.recv().await {
232 let rockspec = build_dep_spec.downloaded_rock.rockspec();
233 let bar = progress_arc.map(|p| {
234 p.add(ProgressBar::from(format!(
235 "💻 Installing build dependency: {}",
236 build_dep_spec.downloaded_rock.rockspec().package(),
237 )))
238 });
239 let package = rockspec.package().clone();
240 let build_tree = tree.build_tree(config)?;
241 let mut build_lockfile = build_tree.lockfile()?.write_guard();
245 let pkg = Build::new()
246 .rockspec(rockspec)
247 .lua(&lua)
248 .tree(&build_tree)
249 .entry_type(tree::EntryType::Entrypoint)
250 .config(config)
251 .progress(&bar)
252 .constraint(build_dep_spec.spec.constraint())
253 .behaviour(build_dep_spec.build_behaviour)
254 .build()
255 .await
256 .map_err(|err| InstallError::BuildDependency(package, err))?;
257 build_lockfile.add_entrypoint(&pkg);
258 }
259
260 let mut all_packages = HashMap::with_capacity(dep_rx.len());
261 while let Some(dep) = dep_rx.recv().await {
262 all_packages.insert(dep.spec.id(), dep);
263 }
264
265 let installed_packages =
266 futures::stream::iter(all_packages.clone().into_values().map(|install_spec| {
267 let progress_arc = progress_arc.clone();
268 let downloaded_rock = install_spec.downloaded_rock;
269 let config = config.clone();
270 let tree = tree.clone();
271 let lua = lua.clone();
272
273 tokio::spawn({
274 async move {
275 let pkg = match downloaded_rock {
276 RemoteRockDownload::RockspecOnly { rockspec_download } => {
277 install_rockspec(
278 rockspec_download,
279 None,
280 install_spec.spec.constraint(),
281 install_spec.build_behaviour,
282 install_spec.pin,
283 install_spec.opt,
284 install_spec.entry_type,
285 &lua,
286 &tree,
287 &config,
288 progress_arc,
289 )
290 .await?
291 }
292 RemoteRockDownload::BinaryRock {
293 rockspec_download,
294 packed_rock,
295 } => {
296 install_binary_rock(
297 rockspec_download,
298 packed_rock,
299 install_spec.spec.constraint(),
300 install_spec.build_behaviour,
301 install_spec.pin,
302 install_spec.opt,
303 install_spec.entry_type,
304 &config,
305 &tree,
306 progress_arc,
307 )
308 .await?
309 }
310 RemoteRockDownload::SrcRock {
311 rockspec_download,
312 src_rock,
313 source_url,
314 } => {
315 let src_rock_source = SrcRockSource {
316 bytes: src_rock,
317 source_url,
318 };
319 install_rockspec(
320 rockspec_download,
321 Some(src_rock_source),
322 install_spec.spec.constraint(),
323 install_spec.build_behaviour,
324 install_spec.pin,
325 install_spec.opt,
326 install_spec.entry_type,
327 &lua,
328 &tree,
329 &config,
330 progress_arc,
331 )
332 .await?
333 }
334 };
335
336 Ok::<_, InstallError>((pkg.id(), (pkg, install_spec.entry_type)))
337 }
338 })
339 }))
340 .buffered(config.max_jobs())
341 .collect::<Vec<_>>()
342 .await
343 .into_iter()
344 .flatten()
345 .try_collect::<_, HashMap<LocalPackageId, (LocalPackage, tree::EntryType)>, _>()?;
346
347 let write_dependency = |lockfile: &mut Lockfile<ReadWrite>,
348 id: &LocalPackageId,
349 pkg: &LocalPackage,
350 entry_type: tree::EntryType|
351 -> io::Result<()> {
352 if entry_type == tree::EntryType::Entrypoint {
353 lockfile.add_entrypoint(pkg);
354 }
355
356 for dependency_id in all_packages
357 .get(id)
358 .map(|pkg| pkg.spec.dependencies())
359 .unwrap_or_default()
360 .into_iter()
361 {
362 lockfile.add_dependency(
363 pkg,
364 installed_packages
365 .get(dependency_id)
366 .map(|(pkg, _)| pkg)
367 .ok_or(io::Error::other(
368 r#"
369error writing dependencies to the lockfile.
370A required dependency was not installed correctly.
371This is likely because an install thread panicked and was interrupted unexpectedly.
372
373[THIS IS A BUG!]
374"#,
375 ))?,
376 );
377 }
378 Ok(())
379 };
380
381 lockfile.map_then_flush(|lockfile| {
382 for (id, (pkg, is_entrypoint)) in installed_packages.iter() {
383 write_dependency(lockfile, id, pkg, *is_entrypoint)?;
384 }
385 Ok::<_, io::Error>(())
386 })?;
387
388 Ok(installed_packages
389 .into_values()
390 .map(|(pkg, _)| pkg)
391 .collect_vec())
392}
393
394#[allow(clippy::too_many_arguments)]
395async fn install_rockspec<T>(
396 rockspec_download: DownloadedRockspec,
397 src_rock_source: Option<SrcRockSource>,
398 constraint: LockConstraint,
399 behaviour: BuildBehaviour,
400 pin: PinnedState,
401 opt: OptState,
402 entry_type: tree::EntryType,
403 lua: &LuaInstallation,
404 tree: &T,
405 config: &Config,
406 progress_arc: Arc<Progress<MultiProgress>>,
407) -> Result<LocalPackage, InstallError>
408where
409 T: InstallTree + Sync,
410{
411 let progress = Arc::clone(&progress_arc);
412 let rockspec = rockspec_download.rockspec;
413 let source = rockspec_download.source;
414 let package = rockspec.package().clone();
415 let bar = progress.map(|p| p.add(ProgressBar::from(format!("💻 Installing {}", &package,))));
416
417 if let Some(BuildBackendSpec::LuaRock(_)) = &rockspec.build().current_platform().build_backend {
418 let luarocks_tree = tree.build_tree(config)?;
419 let luarocks = LuaRocksInstallation::new(config, luarocks_tree)?;
420 luarocks.ensure_installed(lua, &bar).await?;
421 }
422
423 let source_spec = match src_rock_source {
424 Some(src_rock_source) => RemotePackageSourceSpec::SrcRock(src_rock_source),
425 None => RemotePackageSourceSpec::RockSpec(rockspec_download.source_url),
426 };
427
428 let pkg = Build::new()
429 .rockspec(&rockspec)
430 .lua(lua)
431 .tree(tree)
432 .entry_type(entry_type)
433 .config(config)
434 .progress(&bar)
435 .pin(pin)
436 .opt(opt)
437 .constraint(constraint)
438 .behaviour(behaviour)
439 .source(source)
440 .source_spec(source_spec)
441 .build()
442 .await
443 .map_err(|err| InstallError::Build(package, err))?;
444
445 bar.map(|b| b.finish_and_clear());
446
447 Ok(pkg)
448}
449
450#[allow(clippy::too_many_arguments)]
451async fn install_binary_rock(
452 rockspec_download: DownloadedRockspec,
453 packed_rock: Bytes,
454 constraint: LockConstraint,
455 behaviour: BuildBehaviour,
456 pin: PinnedState,
457 opt: OptState,
458 entry_type: tree::EntryType,
459 config: &Config,
460 tree: &impl InstallTree,
461 progress_arc: Arc<Progress<MultiProgress>>,
462) -> Result<LocalPackage, InstallError> {
463 let progress = Arc::clone(&progress_arc);
464 let rockspec = rockspec_download.rockspec;
465 let package = rockspec.package().clone();
466 let bar = progress.map(|p| {
467 p.add(ProgressBar::from(format!(
468 "💻 Installing {} (pre-built)",
469 &package,
470 )))
471 });
472 let pkg = BinaryRockInstall::new(
473 &rockspec,
474 rockspec_download.source,
475 packed_rock,
476 entry_type,
477 config,
478 tree,
479 &bar,
480 )
481 .pin(pin)
482 .opt(opt)
483 .constraint(constraint)
484 .behaviour(behaviour)
485 .install()
486 .await
487 .map_err(|err| InstallError::InstallBinaryRock(package, err))?;
488
489 bar.map(|b| b.finish_and_clear());
490
491 Ok(pkg)
492}