1use std::{collections::HashMap, io};
2
3use bon::Builder;
4use itertools::Itertools;
5use miette::Diagnostic;
6use thiserror::Error;
7use tracing::Instrument;
8
9use crate::{
10 build::BuildBehaviour,
11 config::Config,
12 lockfile::{
13 FlushLockfileError, LockedPackage, LockedPackageId, LockedPackageLockType, Lockfile,
14 ReadWrite,
15 },
16 lua_installation::LuaInstallationError,
17 luarocks::luarocks_installation::{LuaRocksError, LuaRocksInstallError},
18 package::PackageNameList,
19 package_db::{PackageDB, PackageDBError},
20 tree::{self, InstallTree, TreeError},
21 workspace::WorkspaceTreeError,
22};
23
24use crate::pipeline::{
25 build::{Build as PipelineBuild, BuildError as PipelineBuildError},
26 emit_lockfile::LockfileHandle,
27 materialise::{Error as MaterialiseError, Materialise},
28 resolve::{ResolveError, ResolvePackageDependencies},
29};
30
31use crate::operations::PackageInstallSpec;
32
33#[derive(Builder)]
38#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
39pub struct InstallPackages<'a, T>
40where
41 T: InstallTree + Send + Sync,
42{
43 #[builder(start_fn)]
44 config: &'a Config,
45 #[builder(start_fn)]
46 tree: &'a T,
47 #[builder(field)]
48 build_packages: Vec<PackageInstallSpec>,
49 #[builder(field)]
50 test_packages: Option<Vec<PackageInstallSpec>>,
51 #[builder(field)]
52 packages: Vec<PackageInstallSpec>,
53 package_db: PackageDB,
54}
55
56impl<'a, T, State> InstallPackagesBuilder<'a, T, State>
57where
58 T: InstallTree + Send + Sync,
59 State: install_packages_builder::State,
60{
61 pub fn packages(mut self, packages: Vec<PackageInstallSpec>) -> Self {
62 self.packages = packages;
63 self
64 }
65
66 pub fn package(mut self, package: PackageInstallSpec) -> Self {
67 self.packages.push(package);
68 self
69 }
70
71 pub fn build_packages(mut self, packages: Vec<PackageInstallSpec>) -> Self {
72 self.build_packages = packages;
73 self
74 }
75
76 pub fn test_packages(mut self, packages: Vec<PackageInstallSpec>) -> Self {
77 self.test_packages = Some(packages);
78 self
79 }
80}
81
82impl<T, State> InstallPackagesBuilder<'_, T, State>
83where
84 State: install_packages_builder::State + install_packages_builder::IsComplete,
85 T: InstallTree + Send + Sync,
86{
87 pub async fn install(
88 self,
89 ) -> Result<(Vec<LockedPackage>, LockfileHandle), InstallPackagesError> {
90 let args = self._build();
91 if args.packages.is_empty()
92 && args.build_packages.is_empty()
93 && args.test_packages.is_none()
94 {
95 return Ok((Vec::new(), LockfileHandle::default()));
96 }
97 let span = match args.packages.as_slice() {
98 [install_spec] => {
99 tracing::info_span!("Installing", package = install_spec.package.to_string())
100 }
101 packages => tracing::info_span!("Installing", count = packages.len()),
102 };
103 install_packages(args).instrument(span).await
104 }
105}
106
107async fn install_packages<T>(
108 install: InstallPackages<'_, T>,
109) -> Result<(Vec<LockedPackage>, LockfileHandle), InstallPackagesError>
110where
111 T: InstallTree + Send + Sync,
112{
113 let package_db = install.package_db;
114
115 let duplicate_entrypoints = install
116 .packages
117 .iter()
118 .filter(|pkg| pkg.entry_type == tree::EntryType::Entrypoint)
119 .map(|pkg| pkg.package.name())
120 .duplicates()
121 .cloned()
122 .collect_vec();
123
124 if !duplicate_entrypoints.is_empty() {
125 return Err(InstallPackagesError::DuplicateEntrypoints(
126 PackageNameList::new(duplicate_entrypoints),
127 ));
128 }
129
130 let packages = install.packages;
131 let config = install.config;
132 let tree = install.tree;
133
134 let lockfile = tree.lockfile()?;
135 let build_tree = tree.build_tree(config)?;
136 let build_lockfile = build_tree.lockfile()?;
137
138 let mut installed: HashMap<LockedPackageLockType, Vec<LockedPackage>> = HashMap::new();
139 installed.insert(
140 LockedPackageLockType::Regular,
141 lockfile.rocks().values().cloned().collect_vec(),
142 );
143 installed.insert(
144 LockedPackageLockType::Build,
145 build_lockfile.rocks().values().cloned().collect_vec(),
146 );
147 if install.test_packages.is_some() {
148 installed.insert(
149 LockedPackageLockType::Test,
150 tree.test_tree(config)?
151 .lockfile()?
152 .rocks()
153 .values()
154 .cloned()
155 .collect_vec(),
156 );
157 }
158
159 let mut resolve = ResolvePackageDependencies::new(package_db, config)
160 .installed(installed)
161 .packages(packages)
162 .build_packages(install.build_packages);
163 if let Some(test_packages) = install.test_packages {
164 resolve = resolve.test_packages(test_packages);
165 }
166 let resolved = resolve.resolve().await?;
167 let artifacts = Materialise::new(config)
168 .resolved(resolved)
169 .materialise()
170 .await?;
171 let handle = LockfileHandle::from_artifacts(&artifacts);
172
173 let build_packages = artifacts
174 .build
175 .unwrap_or_default()
176 .into_values()
177 .collect_vec();
178 let regular_packages = artifacts
179 .regular
180 .unwrap_or_default()
181 .into_values()
182 .collect_vec();
183
184 let mut conflicting_entrypoints: HashMap<LockedPackageId, LockedPackage> = HashMap::new();
187 let mut unforced_conflicts = Vec::new();
188 for package in regular_packages
189 .iter()
190 .filter(|package| package.entry_type == tree::EntryType::Entrypoint)
191 {
192 match lockfile.entrypoint(package.package.name()) {
193 None => continue,
194 Some(existing) => match package.build_behaviour {
195 BuildBehaviour::Force => {
197 conflicting_entrypoints.insert(existing.id(), existing.clone());
198 }
199 BuildBehaviour::Ignore if existing.version() == package.package.version() => {}
201 BuildBehaviour::Ignore | BuildBehaviour::Conflict => {
203 unforced_conflicts.push(format!("{}@{}", existing.name(), existing.version()));
204 }
205 },
206 }
207 }
208
209 if !unforced_conflicts.is_empty() {
210 return Err(InstallPackagesError::ConflictingEntrypoints(
211 unforced_conflicts.join("\n"),
212 ));
213 }
214
215 let conflicting_entrypoints = conflicting_entrypoints.into_values().collect_vec();
218 for package in &conflicting_entrypoints {
219 tree.cleanup(package, tree::EntryType::Entrypoint)?;
220 }
221
222 let regular_entry_types: HashMap<LockedPackageId, tree::EntryType> = regular_packages
223 .iter()
224 .map(|pkg| (pkg.package.spec.id(), pkg.entry_type))
225 .collect();
226
227 let built_build_deps = PipelineBuild::new(config, &build_tree)
229 .packages(build_packages)
230 .build()
231 .await?;
232 let built = PipelineBuild::new(config, tree)
233 .packages(regular_packages)
234 .build()
235 .await?;
236 if let Some(test_packages) = artifacts.test {
237 let test_tree = tree.test_tree(config)?;
238 PipelineBuild::new(config, &test_tree)
239 .packages(test_packages.into_values().collect_vec())
240 .build()
241 .await?;
242 }
243
244 let mut installed_packages: HashMap<LockedPackageId, LockedPackage> = lockfile
245 .rocks()
246 .iter()
247 .map(|(id, package)| (id.clone(), package.clone()))
248 .collect();
249 installed_packages.extend(built.iter().map(|pkg| (pkg.spec().id(), pkg.clone())));
250 let mut installed_build_deps: HashMap<LockedPackageId, LockedPackage> = build_lockfile
251 .rocks()
252 .iter()
253 .map(|(id, package)| (id.clone(), package.clone()))
254 .collect();
255 installed_build_deps.extend(
256 built_build_deps
257 .iter()
258 .map(|pkg| (pkg.spec().id(), pkg.clone())),
259 );
260
261 lockfile.map_then_flush(|lockfile| {
262 for package in &conflicting_entrypoints {
263 lockfile.remove_by_id(&package.id());
264 }
265 for package in &built {
266 let entry_type = regular_entry_types[&package.spec().id()];
267 lockfile.add_dependencies(package, entry_type, &installed_packages)?;
268 lockfile.add_build_dependencies(package, &installed_build_deps)?;
269 }
270 Ok::<_, io::Error>(())
271 })?;
272
273 Ok((built, handle))
274}
275
276#[derive(Error, Debug, Diagnostic)]
277pub enum InstallPackagesError {
278 #[error(transparent)]
279 #[diagnostic(transparent)]
280 LuaVersionUnset(#[from] crate::lua_version::LuaVersionUnset),
281 #[error(transparent)]
282 #[diagnostic(transparent)]
283 LuaInstallation(#[from] LuaInstallationError),
284 #[error(transparent)]
285 #[diagnostic(transparent)]
286 FlushLockfile(#[from] FlushLockfileError),
287 #[error(transparent)]
288 #[diagnostic(transparent)]
289 Tree(#[from] TreeError),
290 #[error(transparent)]
291 #[diagnostic(transparent)]
292 WorkspaceTree(#[from] WorkspaceTreeError),
293 #[error("error instantiating LuaRocks compatibility layer")]
294 #[diagnostic(forward(0))]
295 LuaRocks(#[from] LuaRocksError),
296 #[error("error installing LuaRocks compatibility layer")]
297 #[diagnostic(forward(0))]
298 LuaRocksInstall(#[from] Box<LuaRocksInstallError>),
299 #[error("error initialising remote package DB")]
300 #[diagnostic(forward(0))]
301 PackageDB(#[from] PackageDBError),
302 #[error("cannot install duplicate entrypoints:\n{0}")]
303 DuplicateEntrypoints(PackageNameList),
304 #[error("cannot install conflicting entrypoints:\n{0}")]
305 #[diagnostic(help(
306 "only a single entrypoint per package is allowed.\nif you would like to replace the existing entrypoints, retry with `--force` to overwrite the existing entrypoint"
307 ))]
308 ConflictingEntrypoints(String),
309 #[error(transparent)]
310 #[diagnostic(transparent)]
311 Resolve(#[from] ResolveError),
312 #[error(transparent)]
313 #[diagnostic(transparent)]
314 Download(#[from] MaterialiseError),
315 #[error(transparent)]
316 #[diagnostic(transparent)]
317 Build(#[from] PipelineBuildError),
318}
319
320impl From<LuaRocksInstallError> for InstallPackagesError {
321 fn from(source: LuaRocksInstallError) -> Self {
322 Self::LuaRocksInstall(Box::new(source))
323 }
324}
325
326trait LockfileExt {
327 fn add_dependencies(
328 self,
329 pkg: &LockedPackage,
330 entry_type: tree::EntryType,
331 installed_packages: &HashMap<LockedPackageId, LockedPackage>,
332 ) -> io::Result<()>;
333
334 fn add_build_dependencies(
335 self,
336 pkg: &LockedPackage,
337 build_dependencies: &HashMap<LockedPackageId, LockedPackage>,
338 ) -> io::Result<()>;
339}
340
341impl LockfileExt for &mut Lockfile<ReadWrite> {
342 fn add_dependencies(
343 self,
344 pkg: &LockedPackage,
345 entry_type: tree::EntryType,
346 installed_packages: &HashMap<LockedPackageId, LockedPackage>,
347 ) -> io::Result<()> {
348 if entry_type == tree::EntryType::Entrypoint {
349 self.add_entrypoint(pkg);
350 }
351
352 for dependency_id in pkg.spec().dependencies() {
353 self.add_dependency(
354 pkg,
355 installed_packages
356 .get(dependency_id)
357 .ok_or(io::Error::other(
358 r#"
359error writing dependencies to the lockfile.
360A required dependency was not installed correctly.
361This is likely a bug in Lux.
362"#,
363 ))?,
364 );
365 }
366 Ok(())
367 }
368
369 fn add_build_dependencies(
370 self,
371 pkg: &LockedPackage,
372 build_dependencies: &HashMap<LockedPackageId, LockedPackage>,
373 ) -> io::Result<()> {
374 for dependency_id in pkg.spec().build_dependencies() {
375 self.add_build_dependency(
376 pkg,
377 build_dependencies
378 .get(dependency_id)
379 .ok_or(io::Error::other(
380 r#"
381error writing build dependencies to the lockfile.
382A required build dependency was not installed correctly.
383This is likely a bug in Lux.
384"#,
385 ))?,
386 );
387 }
388 Ok(())
389 }
390}