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