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::{
14 InstallBuildDependenciesError, LuaRocksError, LuaRocksInstallError,
15 LuaRocksInstallation,
16 },
17 },
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
26use bon::Builder;
27use bytes::Bytes;
28use futures::future::join_all;
29use itertools::Itertools;
30use thiserror::Error;
31
32use super::{
33 install_spec::PackageInstallSpec, resolve::get_all_dependencies, DownloadedRockspec,
34 RemoteRockDownload, SearchAndDownloadError,
35};
36
37#[derive(Builder)]
41#[builder(start_fn = new, finish_fn(name = _install, 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._install();
98 let progress = match install_built.progress {
99 Some(p) => p,
100 None => MultiProgress::new_arc(),
101 };
102 let package_db = match install_built.package_db {
103 Some(db) => db,
104 None => {
105 let bar = progress.map(|p| p.new_bar());
106 RemotePackageDB::from_config(install_built.config, &bar).await?
107 }
108 };
109
110 let duplicate_entrypoints = install_built
111 .packages
112 .iter()
113 .filter(|pkg| pkg.entry_type == tree::EntryType::Entrypoint)
114 .map(|pkg| pkg.package.name())
115 .duplicates()
116 .cloned()
117 .collect_vec();
118
119 if !duplicate_entrypoints.is_empty() {
120 return Err(InstallError::DuplicateEntrypoints(PackageNameList::new(
121 duplicate_entrypoints,
122 )));
123 }
124
125 install_impl(
126 install_built.packages,
127 Arc::new(package_db),
128 install_built.config,
129 &install_built.tree,
130 install_built.tree.lockfile()?,
131 progress,
132 )
133 .await
134 }
135}
136
137#[derive(Error, Debug)]
138pub enum InstallError {
139 #[error(transparent)]
140 SearchAndDownloadError(#[from] SearchAndDownloadError),
141 #[error(transparent)]
142 LuaVersionUnset(#[from] LuaVersionUnset),
143 #[error(transparent)]
144 Io(#[from] io::Error),
145 #[error(transparent)]
146 Tree(#[from] TreeError),
147 #[error("error instantiating LuaRocks compatibility layer: {0}")]
148 LuaRocksError(#[from] LuaRocksError),
149 #[error("error installing LuaRocks compatibility layer: {0}")]
150 LuaRocksInstallError(#[from] LuaRocksInstallError),
151 #[error("error installing LuaRocks build dependencies: {0}")]
152 InstallBuildDependenciesError(#[from] InstallBuildDependenciesError),
153 #[error("failed to build {0}: {1}")]
154 BuildError(PackageName, BuildError),
155 #[error("error initialising remote package DB: {0}")]
156 RemotePackageDB(#[from] RemotePackageDBError),
157 #[error("failed to install pre-built rock {0}: {1}")]
158 InstallBinaryRockError(PackageName, InstallBinaryRockError),
159 #[error("integrity error for package {0}: {1}\n")]
160 Integrity(PackageName, RemotePackageDbIntegrityError),
161 #[error(transparent)]
162 ProjectTreeError(#[from] ProjectTreeError),
163 #[error("cannot install duplicate entrypoints: {0}")]
164 DuplicateEntrypoints(PackageNameList),
165}
166
167#[allow(clippy::too_many_arguments)]
169async fn install_impl(
170 packages: Vec<PackageInstallSpec>,
171 package_db: Arc<RemotePackageDB>,
172 config: &Config,
173 tree: &Tree,
174 lockfile: Lockfile<ReadOnly>,
175 progress_arc: Arc<Progress<MultiProgress>>,
176) -> Result<Vec<LocalPackage>, InstallError> {
177 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
178
179 get_all_dependencies(
180 tx,
181 packages,
182 package_db.clone(),
183 Arc::new(lockfile.clone()),
184 config,
185 progress_arc.clone(),
186 )
187 .await?;
188
189 let mut all_packages = HashMap::with_capacity(rx.len());
190
191 while let Some(dep) = rx.recv().await {
192 all_packages.insert(dep.spec.id(), dep);
193 }
194
195 let installed_packages = join_all(all_packages.clone().into_values().map(|install_spec| {
196 let progress_arc = progress_arc.clone();
197 let downloaded_rock = install_spec.downloaded_rock;
198 let config = config.clone();
199 let tree = tree.clone();
200
201 tokio::spawn(async move {
202 let rockspec = downloaded_rock.rockspec();
203 if let Some(BuildBackendSpec::LuaRock(build_backend)) =
204 &rockspec.build().current_platform().build_backend
205 {
206 let luarocks_tree = tree.build_tree(&config)?;
207 let luarocks = LuaRocksInstallation::new(&config, luarocks_tree)?;
208 luarocks
209 .install_build_dependencies(build_backend, rockspec, progress_arc.clone())
210 .await?;
211 }
212
213 let pkg = match downloaded_rock {
214 RemoteRockDownload::RockspecOnly { rockspec_download } => {
215 install_rockspec(
216 rockspec_download,
217 None,
218 install_spec.spec.constraint(),
219 install_spec.build_behaviour,
220 install_spec.pin,
221 install_spec.opt,
222 install_spec.entry_type,
223 &tree,
224 &config,
225 progress_arc,
226 )
227 .await?
228 }
229 RemoteRockDownload::BinaryRock {
230 rockspec_download,
231 packed_rock,
232 } => {
233 install_binary_rock(
234 rockspec_download,
235 packed_rock,
236 install_spec.spec.constraint(),
237 install_spec.build_behaviour,
238 install_spec.pin,
239 install_spec.opt,
240 install_spec.entry_type,
241 &config,
242 &tree,
243 progress_arc,
244 )
245 .await?
246 }
247 RemoteRockDownload::SrcRock {
248 rockspec_download,
249 src_rock,
250 source_url,
251 } => {
252 let src_rock_source = SrcRockSource {
253 bytes: src_rock,
254 source_url,
255 };
256 install_rockspec(
257 rockspec_download,
258 Some(src_rock_source),
259 install_spec.spec.constraint(),
260 install_spec.build_behaviour,
261 install_spec.pin,
262 install_spec.opt,
263 install_spec.entry_type,
264 &tree,
265 &config,
266 progress_arc,
267 )
268 .await?
269 }
270 };
271
272 Ok::<_, InstallError>((pkg.id(), (pkg, install_spec.entry_type)))
273 })
274 }))
275 .await
276 .into_iter()
277 .flatten()
278 .try_collect::<_, HashMap<LocalPackageId, (LocalPackage, tree::EntryType)>, _>()?;
279
280 let write_dependency = |lockfile: &mut Lockfile<ReadWrite>,
281 id: &LocalPackageId,
282 pkg: &LocalPackage,
283 entry_type: tree::EntryType| {
284 if entry_type == tree::EntryType::Entrypoint {
285 lockfile.add_entrypoint(pkg);
286 }
287
288 all_packages
289 .get(id)
290 .map(|pkg| pkg.spec.dependencies())
291 .unwrap_or_default()
292 .into_iter()
293 .for_each(|dependency_id| {
294 lockfile.add_dependency(
295 pkg,
296 installed_packages
297 .get(dependency_id)
298 .map(|(pkg, _)| pkg)
299 .expect("required dependency not found [This is a bug!]"),
301 );
302 });
303 };
304
305 lockfile.map_then_flush(|lockfile| {
306 installed_packages
307 .iter()
308 .for_each(|(id, (pkg, is_entrypoint))| {
309 write_dependency(lockfile, id, pkg, *is_entrypoint)
310 });
311
312 Ok::<_, io::Error>(())
313 })?;
314
315 Ok(installed_packages
316 .into_values()
317 .map(|(pkg, _)| pkg)
318 .collect_vec())
319}
320
321#[allow(clippy::too_many_arguments)]
322async fn install_rockspec(
323 rockspec_download: DownloadedRockspec,
324 src_rock_source: Option<SrcRockSource>,
325 constraint: LockConstraint,
326 behaviour: BuildBehaviour,
327 pin: PinnedState,
328 opt: OptState,
329 entry_type: tree::EntryType,
330 tree: &Tree,
331 config: &Config,
332 progress_arc: Arc<Progress<MultiProgress>>,
333) -> Result<LocalPackage, InstallError> {
334 let progress = Arc::clone(&progress_arc);
335 let rockspec = rockspec_download.rockspec;
336 let source = rockspec_download.source;
337 let package = rockspec.package().clone();
338 let bar = progress.map(|p| p.add(ProgressBar::from(format!("💻 Installing {}", &package,))));
339
340 if let Some(BuildBackendSpec::LuaRock(build_backend)) =
341 &rockspec.build().current_platform().build_backend
342 {
343 let luarocks_tree = tree.build_tree(config)?;
344 let luarocks = LuaRocksInstallation::new(config, luarocks_tree)?;
345 luarocks.ensure_installed(&bar).await?;
346 luarocks
347 .install_build_dependencies(build_backend, &rockspec, progress_arc)
348 .await?;
349 }
350
351 let source_spec = match src_rock_source {
352 Some(src_rock_source) => RemotePackageSourceSpec::SrcRock(src_rock_source),
353 None => RemotePackageSourceSpec::RockSpec(rockspec_download.source_url),
354 };
355
356 let pkg = Build::new(&rockspec, tree, entry_type, config, &bar)
357 .pin(pin)
358 .opt(opt)
359 .constraint(constraint)
360 .behaviour(behaviour)
361 .source(source)
362 .source_spec(source_spec)
363 .build()
364 .await
365 .map_err(|err| InstallError::BuildError(package, err))?;
366
367 bar.map(|b| b.finish_and_clear());
368
369 Ok(pkg)
370}
371
372#[allow(clippy::too_many_arguments)]
373async fn install_binary_rock(
374 rockspec_download: DownloadedRockspec,
375 packed_rock: Bytes,
376 constraint: LockConstraint,
377 behaviour: BuildBehaviour,
378 pin: PinnedState,
379 opt: OptState,
380 entry_type: tree::EntryType,
381 config: &Config,
382 tree: &Tree,
383 progress_arc: Arc<Progress<MultiProgress>>,
384) -> Result<LocalPackage, InstallError> {
385 let progress = Arc::clone(&progress_arc);
386 let rockspec = rockspec_download.rockspec;
387 let package = rockspec.package().clone();
388 let bar = progress.map(|p| {
389 p.add(ProgressBar::from(format!(
390 "💻 Installing {} (pre-built)",
391 &package,
392 )))
393 });
394 let pkg = BinaryRockInstall::new(
395 &rockspec,
396 rockspec_download.source,
397 packed_rock,
398 entry_type,
399 config,
400 tree,
401 &bar,
402 )
403 .pin(pin)
404 .opt(opt)
405 .constraint(constraint)
406 .behaviour(behaviour)
407 .install()
408 .await
409 .map_err(|err| InstallError::InstallBinaryRockError(package, err))?;
410
411 bar.map(|b| b.finish_and_clear());
412
413 Ok(pkg)
414}