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