1use std::collections::HashMap;
2use std::io;
3use std::io::Cursor;
4use std::path::PathBuf;
5
6use bon::Builder;
7use bytes::Bytes;
8use futures::{StreamExt, TryStreamExt};
9use miette::Diagnostic;
10use tempfile::TempDir;
11use thiserror::Error;
12
13use crate::{
14 build::{BuildError, RemotePackageSourceSpec, SrcRockSource},
15 config::Config,
16 fs::{self, FsError},
17 hash::HasIntegrity,
18 lockfile::{
19 LockedPackage, LockedPackageHashes, LockedPackageId, LockedPackageSpec,
20 RemotePackageSourceUrl,
21 },
22 lua_rockspec::RemoteLuaRockspec,
23 operations::{unpack_src_rock, FetchSrc, RemotePackageSourceMetadata},
24 remote_package_source::RemotePackageSource,
25 rockspec::Rockspec,
26 tree::EntryType,
27};
28
29use super::{
30 resolve::{ResolvedArtifacts, ResolvedPackage},
31 Artifacts,
32};
33
34#[derive(Clone, Debug)]
35pub(crate) enum PackageSource {
36 SourceDir(PathBuf),
37 Archive(Bytes),
38}
39
40pub(crate) struct MaterialisedPackage {
41 pub(crate) package: LockedPackage,
42 pub(crate) rockspec: RemoteLuaRockspec,
43 pub(crate) entry_type: EntryType,
44 pub(crate) build_behaviour: crate::build::BuildBehaviour,
45 pub(crate) artifact: PackageSource,
46 pub(crate) temp_dir: Option<TempDir>,
47}
48
49pub(crate) type MaterialiseArtifacts = Artifacts<HashMap<LockedPackageId, MaterialisedPackage>>;
50
51#[derive(Error, Debug, Diagnostic)]
52#[non_exhaustive]
53pub enum Error {
54 #[error(transparent)]
55 #[diagnostic(transparent)]
56 Fs(#[from] FsError),
57 #[error(transparent)]
58 #[diagnostic(transparent)]
59 Build(#[from] Box<BuildError>),
60 #[error("failed to hash the source for '{0}'")]
61 Hash(String, #[source] io::Error),
62 #[error("missing source url for '{0}'")]
63 MissingSourceUrl(String),
64}
65
66impl From<BuildError> for Error {
67 fn from(source: BuildError) -> Self {
68 Self::Build(Box::new(source))
69 }
70}
71
72#[derive(Builder)]
73#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
74pub(crate) struct Materialise<'a> {
75 #[builder(start_fn)]
76 pub(crate) config: &'a Config,
77 pub(crate) resolved: ResolvedArtifacts,
78}
79
80impl<State> MaterialiseBuilder<'_, State>
81where
82 State: materialise_builder::State + materialise_builder::IsComplete,
83{
84 pub(crate) async fn materialise(self) -> Result<MaterialiseArtifacts, Error> {
85 let args = self._build();
86 let config = args.config;
87
88 let artifacts: MaterialiseArtifacts = args
89 .resolved
90 .iter()
91 .map(|(section, packages)| (section, packages.map(|_| HashMap::new())))
92 .collect();
93
94 let jobs = args.resolved.into_iter().flat_map(|(section, packages)| {
95 packages
96 .into_iter()
97 .flatten()
98 .map(move |(id, package)| (section, id, package))
99 });
100
101 futures::stream::iter(jobs)
102 .map(|(section, id, package)| async move {
103 materialise(config, package)
104 .await
105 .map(|package| (section, id, package))
106 })
107 .buffered(config.max_jobs())
108 .try_fold(
109 artifacts,
110 |mut artifacts, (section, id, package)| async move {
111 artifacts
112 .get_mut(section)
113 .get_or_insert_with(HashMap::new)
114 .insert(id, package);
115 Ok(artifacts)
116 },
117 )
118 .await
119 }
120}
121
122async fn materialise(
123 config: &Config,
124 resolved: ResolvedPackage,
125) -> Result<MaterialisedPackage, Error> {
126 let ResolvedPackage {
127 spec,
128 rockspec,
129 source,
130 source_url,
131 entry_type,
132 build_behaviour,
133 artifact,
134 } = resolved;
135
136 let name = spec.name().to_string();
137
138 let (package, artifact, temp_dir) = match artifact {
139 Some(PackageSource::Archive(bytes)) => match &source {
140 RemotePackageSource::LuarocksBinaryRock(url) => {
141 let binary_url = url.clone();
142 let rockspec_hash = rockspec
143 .hash()
144 .await
145 .map_err(|err| Error::Hash(name.clone(), err))?;
146 let source_hash = bytes
147 .hash()
148 .await
149 .map_err(|err| Error::Hash(name.clone(), err))?;
150 let hashes = LockedPackageHashes {
151 rockspec: rockspec_hash,
152 source: source_hash,
153 };
154 let package = LockedPackage::new(
155 spec,
156 source,
157 Some(RemotePackageSourceUrl::Url { url: binary_url }),
158 hashes,
159 );
160 (package, PackageSource::Archive(bytes), None)
161 }
162 _ => {
163 let source_url = source_url
164 .clone()
165 .ok_or_else(|| Error::MissingSourceUrl(name.clone()))?;
166 let (package, dir) = fetch_and_hash_source(
167 &rockspec,
168 spec,
169 Some(RemotePackageSourceSpec::SrcRock(SrcRockSource {
170 bytes,
171 source_url,
172 })),
173 Some(source),
174 config,
175 )
176 .await?;
177 let artifact = PackageSource::SourceDir(dir.path().to_path_buf());
178 (package, artifact, Some(dir))
179 }
180 },
181 Some(PackageSource::SourceDir(path)) => {
182 let rockspec_hash = rockspec
183 .hash()
184 .await
185 .map_err(|err| Error::Hash(name.clone(), err))?;
186 let source_hash = path
187 .hash()
188 .await
189 .map_err(|err| Error::Hash(name.clone(), err))?;
190 let hashes = LockedPackageHashes {
191 rockspec: rockspec_hash,
192 source: source_hash,
193 };
194 let package = LockedPackage::new(spec, source, source_url, hashes);
195 (package, PackageSource::SourceDir(path), None)
196 }
197 None => {
198 let (package, dir) = fetch_and_hash_source(
199 &rockspec,
200 spec,
201 Some(RemotePackageSourceSpec::RockSpec(source_url)),
202 Some(source),
203 config,
204 )
205 .await?;
206 let artifact = PackageSource::SourceDir(dir.path().to_path_buf());
207 (package, artifact, Some(dir))
208 }
209 };
210
211 Ok(MaterialisedPackage {
212 package,
213 rockspec,
214 entry_type,
215 build_behaviour,
216 artifact,
217 temp_dir,
218 })
219}
220
221pub(crate) async fn fetch_and_hash_source<R: Rockspec + HasIntegrity>(
222 rockspec: &R,
223 spec: LockedPackageSpec,
224 source_spec: Option<RemotePackageSourceSpec>,
225 source: Option<RemotePackageSource>,
226 config: &Config,
227) -> Result<(LockedPackage, TempDir), BuildError> {
228 let temp_dir = fs::tempfile::tempdir()?;
229
230 let source_metadata = match source_spec {
231 Some(RemotePackageSourceSpec::SrcRock(SrcRockSource { bytes, source_url })) => {
232 let hash = bytes.hash().await?;
233 let cursor = Cursor::new(bytes);
234 unpack_src_rock(cursor, temp_dir.path().to_path_buf())
235 .await
236 .map_err(BuildError::UnpackSrcRock)?;
237 RemotePackageSourceMetadata { hash, source_url }
238 }
239 Some(RemotePackageSourceSpec::RockSpec(source_url)) => {
240 FetchSrc::new(temp_dir.path(), rockspec, config)
241 .maybe_source_url(source_url)
242 .fetch_internal()
243 .await?
244 }
245 None => {
246 FetchSrc::new(temp_dir.path(), rockspec, config)
247 .fetch_internal()
248 .await?
249 }
250 };
251
252 let hashes = LockedPackageHashes {
253 rockspec: rockspec.hash().await?,
254 source: source_metadata.hash.clone(),
255 };
256
257 let package = LockedPackage::new(
258 spec,
259 source
260 .map(Result::Ok)
261 .unwrap_or_else(|| {
262 rockspec
263 .to_lua_remote_rockspec_string()
264 .map(RemotePackageSource::RockspecContent)
265 })
266 .unwrap_or(RemotePackageSource::Local),
267 Some(source_metadata.source_url.clone()),
268 hashes,
269 );
270
271 Ok((package, temp_dir))
272}