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