Skip to main content

lux_lib/pipeline/
materialise.rs

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}