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