1use std::{collections::HashMap, io, sync::Arc};
2
3use crate::{
4 build::{Build, BuildBehaviour, BuildError, RemotePackageSourceSpec, SrcRockSource},
5 config::{Config, LuaVersionUnset},
6 lockfile::{
7 LocalPackage, LocalPackageId, LockConstraint, Lockfile, OptState, PinnedState, ReadOnly,
8 ReadWrite,
9 },
10 lua_rockspec::BuildBackendSpec,
11 luarocks::{
12 install_binary_rock::{BinaryRockInstall, InstallBinaryRockError},
13 luarocks_installation::{LuaRocksError, LuaRocksInstallError, LuaRocksInstallation},
14 },
15 operations::build_dependencies::InstallBuildDependencies,
16 package::{PackageName, PackageNameList},
17 progress::{MultiProgress, Progress, ProgressBar},
18 project::{Project, ProjectTreeError},
19 remote_package_db::{RemotePackageDB, RemotePackageDBError, RemotePackageDbIntegrityError},
20 rockspec::Rockspec,
21 tree::{self, Tree, TreeError},
22};
23
24pub use crate::operations::install::build_dependencies::InstallBuildDependenciesError;
25pub use crate::operations::install::spec::PackageInstallSpec;
26
27use bon::Builder;
28use bytes::Bytes;
29use futures::future::join_all;
30use itertools::Itertools;
31use thiserror::Error;
32
33use super::{
34 resolve::get_all_dependencies, DownloadedRockspec, RemoteRockDownload, SearchAndDownloadError,
35};
36
37pub mod spec;
38
39pub(crate) mod build_dependencies;
40
41#[derive(Builder)]
45#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
46pub struct Install<'a> {
47 #[builder(start_fn)]
48 config: &'a Config,
49 #[builder(field)]
50 packages: Vec<PackageInstallSpec>,
51 #[builder(setters(name = "_tree", vis = ""))]
52 tree: Tree,
53 package_db: Option<RemotePackageDB>,
54 progress: Option<Arc<Progress<MultiProgress>>>,
55}
56
57impl<'a, State> InstallBuilder<'a, State>
58where
59 State: install_builder::State,
60{
61 pub fn tree(self, tree: Tree) -> InstallBuilder<'a, install_builder::SetTree<State>>
62 where
63 State::Tree: install_builder::IsUnset,
64 {
65 self._tree(tree)
66 }
67
68 pub fn project(
69 self,
70 project: &'a Project,
71 ) -> Result<InstallBuilder<'a, install_builder::SetTree<State>>, ProjectTreeError>
72 where
73 State::Tree: install_builder::IsUnset,
74 {
75 let config = self.config;
76 Ok(self._tree(project.tree(config)?))
77 }
78
79 pub fn packages(self, packages: Vec<PackageInstallSpec>) -> Self {
80 Self { packages, ..self }
81 }
82
83 pub fn package(self, package: PackageInstallSpec) -> Self {
84 Self {
85 packages: self
86 .packages
87 .into_iter()
88 .chain(std::iter::once(package))
89 .collect(),
90 ..self
91 }
92 }
93}
94
95impl<State> InstallBuilder<'_, State>
96where
97 State: install_builder::State + install_builder::IsComplete,
98{
99 pub async fn install(self) -> Result<Vec<LocalPackage>, InstallError> {
101 let install_built = self._build();
102 let progress = match install_built.progress {
103 Some(p) => p,
104 None => MultiProgress::new_arc(),
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 install_built.tree.lockfile()?,
135 progress,
136 )
137 .await
138 }
139}
140
141#[derive(Error, Debug)]
142pub enum InstallError {
143 #[error(transparent)]
144 SearchAndDownloadError(#[from] SearchAndDownloadError),
145 #[error(transparent)]
146 LuaVersionUnset(#[from] LuaVersionUnset),
147 #[error(transparent)]
148 Io(#[from] io::Error),
149 #[error(transparent)]
150 Tree(#[from] TreeError),
151 #[error("error instantiating LuaRocks compatibility layer: {0}")]
152 LuaRocksError(#[from] LuaRocksError),
153 #[error("error installing LuaRocks compatibility layer: {0}")]
154 LuaRocksInstallError(#[from] LuaRocksInstallError),
155 #[error("error installing LuaRocks build dependencies: {0}")]
156 InstallBuildDependenciesError(#[from] InstallBuildDependenciesError),
157 #[error("failed to build {0}: {1}")]
158 BuildError(PackageName, BuildError),
159 #[error("error initialising remote package DB: {0}")]
160 RemotePackageDB(#[from] RemotePackageDBError),
161 #[error("failed to install pre-built rock {0}: {1}")]
162 InstallBinaryRockError(PackageName, InstallBinaryRockError),
163 #[error("integrity error for package {0}: {1}\n")]
164 Integrity(PackageName, RemotePackageDbIntegrityError),
165 #[error(transparent)]
166 ProjectTreeError(#[from] ProjectTreeError),
167 #[error("cannot install duplicate entrypoints: {0}")]
168 DuplicateEntrypoints(PackageNameList),
169}
170
171#[allow(clippy::too_many_arguments)]
173async fn install_impl(
174 packages: Vec<PackageInstallSpec>,
175 package_db: Arc<RemotePackageDB>,
176 config: &Config,
177 tree: &Tree,
178 lockfile: Lockfile<ReadOnly>,
179 progress_arc: Arc<Progress<MultiProgress>>,
180) -> Result<Vec<LocalPackage>, InstallError> {
181 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
182
183 get_all_dependencies(
184 tx,
185 packages,
186 package_db.clone(),
187 Arc::new(lockfile.clone()),
188 config,
189 progress_arc.clone(),
190 )
191 .await?;
192
193 let mut all_packages = HashMap::with_capacity(rx.len());
194
195 while let Some(dep) = rx.recv().await {
196 all_packages.insert(dep.spec.id(), dep);
197 }
198
199 let installed_packages = join_all(all_packages.clone().into_values().map(|install_spec| {
200 let progress_arc = progress_arc.clone();
201 let downloaded_rock = install_spec.downloaded_rock;
202 let config = config.clone();
203 let tree = tree.clone();
204
205 tokio::spawn({
206 let package_db = package_db.clone();
207 async move {
208 let rockspec = downloaded_rock.rockspec();
209 if let Some(BuildBackendSpec::LuaRock(build_backend)) =
210 &rockspec.build().current_platform().build_backend
211 {
212 let luarocks_tree = tree.build_tree(&config)?;
213 let luarocks = LuaRocksInstallation::new(&config, luarocks_tree)?;
214 luarocks
215 .install_build_dependencies(build_backend, rockspec, progress_arc.clone())
216 .await?;
217 } else {
218 let build_dependencies = rockspec
219 .build_dependencies()
220 .current_platform()
221 .to_vec()
222 .into_iter()
223 .map(|dep| {
224 PackageInstallSpec::new(dep.package_req, tree::EntryType::Entrypoint)
225 .build()
226 })
227 .collect_vec();
228 if !build_dependencies.is_empty() {
229 InstallBuildDependencies::new()
230 .config(&config)
231 .tree(&tree.build_tree(&config)?)
232 .package_db(&package_db)
233 .progress(progress_arc.clone())
234 .packages(build_dependencies)
235 .install()
236 .await?;
237 }
238 }
239
240 let pkg = match downloaded_rock {
241 RemoteRockDownload::RockspecOnly { rockspec_download } => {
242 install_rockspec(
243 rockspec_download,
244 None,
245 install_spec.spec.constraint(),
246 install_spec.build_behaviour,
247 install_spec.pin,
248 install_spec.opt,
249 install_spec.entry_type,
250 &tree,
251 &config,
252 progress_arc,
253 )
254 .await?
255 }
256 RemoteRockDownload::BinaryRock {
257 rockspec_download,
258 packed_rock,
259 } => {
260 install_binary_rock(
261 rockspec_download,
262 packed_rock,
263 install_spec.spec.constraint(),
264 install_spec.build_behaviour,
265 install_spec.pin,
266 install_spec.opt,
267 install_spec.entry_type,
268 &config,
269 &tree,
270 progress_arc,
271 )
272 .await?
273 }
274 RemoteRockDownload::SrcRock {
275 rockspec_download,
276 src_rock,
277 source_url,
278 } => {
279 let src_rock_source = SrcRockSource {
280 bytes: src_rock,
281 source_url,
282 };
283 install_rockspec(
284 rockspec_download,
285 Some(src_rock_source),
286 install_spec.spec.constraint(),
287 install_spec.build_behaviour,
288 install_spec.pin,
289 install_spec.opt,
290 install_spec.entry_type,
291 &tree,
292 &config,
293 progress_arc,
294 )
295 .await?
296 }
297 };
298
299 Ok::<_, InstallError>((pkg.id(), (pkg, install_spec.entry_type)))
300 }
301 })
302 }))
303 .await
304 .into_iter()
305 .flatten()
306 .try_collect::<_, HashMap<LocalPackageId, (LocalPackage, tree::EntryType)>, _>()?;
307
308 let write_dependency = |lockfile: &mut Lockfile<ReadWrite>,
309 id: &LocalPackageId,
310 pkg: &LocalPackage,
311 entry_type: tree::EntryType| {
312 if entry_type == tree::EntryType::Entrypoint {
313 lockfile.add_entrypoint(pkg);
314 }
315
316 all_packages
317 .get(id)
318 .map(|pkg| pkg.spec.dependencies())
319 .unwrap_or_default()
320 .into_iter()
321 .for_each(|dependency_id| {
322 lockfile.add_dependency(
323 pkg,
324 installed_packages
325 .get(dependency_id)
326 .map(|(pkg, _)| pkg)
327 .expect("required dependency not found [This is a bug!]"),
329 );
330 });
331 };
332
333 lockfile.map_then_flush(|lockfile| {
334 installed_packages
335 .iter()
336 .for_each(|(id, (pkg, is_entrypoint))| {
337 write_dependency(lockfile, id, pkg, *is_entrypoint)
338 });
339
340 Ok::<_, io::Error>(())
341 })?;
342
343 Ok(installed_packages
344 .into_values()
345 .map(|(pkg, _)| pkg)
346 .collect_vec())
347}
348
349#[allow(clippy::too_many_arguments)]
350async fn install_rockspec(
351 rockspec_download: DownloadedRockspec,
352 src_rock_source: Option<SrcRockSource>,
353 constraint: LockConstraint,
354 behaviour: BuildBehaviour,
355 pin: PinnedState,
356 opt: OptState,
357 entry_type: tree::EntryType,
358 tree: &Tree,
359 config: &Config,
360 progress_arc: Arc<Progress<MultiProgress>>,
361) -> Result<LocalPackage, InstallError> {
362 let progress = Arc::clone(&progress_arc);
363 let rockspec = rockspec_download.rockspec;
364 let source = rockspec_download.source;
365 let package = rockspec.package().clone();
366 let bar = progress.map(|p| p.add(ProgressBar::from(format!("💻 Installing {}", &package,))));
367
368 if let Some(BuildBackendSpec::LuaRock(build_backend)) =
369 &rockspec.build().current_platform().build_backend
370 {
371 let luarocks_tree = tree.build_tree(config)?;
372 let luarocks = LuaRocksInstallation::new(config, luarocks_tree)?;
373 luarocks.ensure_installed(&bar).await?;
374 luarocks
375 .install_build_dependencies(build_backend, &rockspec, progress_arc)
376 .await?;
377 }
378
379 let source_spec = match src_rock_source {
380 Some(src_rock_source) => RemotePackageSourceSpec::SrcRock(src_rock_source),
381 None => RemotePackageSourceSpec::RockSpec(rockspec_download.source_url),
382 };
383
384 let pkg = Build::new(&rockspec, tree, entry_type, config, &bar)
385 .pin(pin)
386 .opt(opt)
387 .constraint(constraint)
388 .behaviour(behaviour)
389 .source(source)
390 .source_spec(source_spec)
391 .build()
392 .await
393 .map_err(|err| InstallError::BuildError(package, err))?;
394
395 bar.map(|b| b.finish_and_clear());
396
397 Ok(pkg)
398}
399
400#[allow(clippy::too_many_arguments)]
401async fn install_binary_rock(
402 rockspec_download: DownloadedRockspec,
403 packed_rock: Bytes,
404 constraint: LockConstraint,
405 behaviour: BuildBehaviour,
406 pin: PinnedState,
407 opt: OptState,
408 entry_type: tree::EntryType,
409 config: &Config,
410 tree: &Tree,
411 progress_arc: Arc<Progress<MultiProgress>>,
412) -> Result<LocalPackage, InstallError> {
413 let progress = Arc::clone(&progress_arc);
414 let rockspec = rockspec_download.rockspec;
415 let package = rockspec.package().clone();
416 let bar = progress.map(|p| {
417 p.add(ProgressBar::from(format!(
418 "💻 Installing {} (pre-built)",
419 &package,
420 )))
421 });
422 let pkg = BinaryRockInstall::new(
423 &rockspec,
424 rockspec_download.source,
425 packed_rock,
426 entry_type,
427 config,
428 tree,
429 &bar,
430 )
431 .pin(pin)
432 .opt(opt)
433 .constraint(constraint)
434 .behaviour(behaviour)
435 .install()
436 .await
437 .map_err(|err| InstallError::InstallBinaryRockError(package, err))?;
438
439 bar.map(|b| b.finish_and_clear());
440
441 Ok(pkg)
442}