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