Skip to main content

repro_env/
build.rs

1use crate::args;
2use crate::container::{self, Container};
3use crate::errors::*;
4use crate::fetch;
5use crate::lockfile::PackageLock;
6use crate::paths;
7use crate::pgp;
8use crate::pkgs::archlinux;
9use data_encoding::BASE64;
10use std::env;
11use std::path::Path;
12use std::time::Duration;
13use tempfile::TempDir;
14use time::format_description::well_known;
15use time::OffsetDateTime;
16use tokio::fs;
17
18#[derive(Debug, PartialEq, Default)]
19pub struct Install {
20    alpine: Vec<(PackageLock, String)>,
21    archlinux: Vec<(PackageLock, String)>,
22    debian: Vec<(PackageLock, String)>,
23}
24
25impl Install {
26    fn add_pkg(&mut self, pkg: PackageLock, filename: String) -> Result<()> {
27        let list = match pkg.system.as_str() {
28            "alpine" => &mut self.alpine,
29            "archlinux" => &mut self.archlinux,
30            "debian" => &mut self.debian,
31            system => bail!("Unknown package system: {system:?}"),
32        };
33        list.push((pkg, filename));
34        Ok(())
35    }
36}
37
38pub async fn setup_extra_folder(path: &Path, dependencies: Vec<PackageLock>) -> Result<Install> {
39    let pkgs_cache_dir = paths::pkgs_cache_dir()?;
40
41    let mut install = Install::default();
42    for package in dependencies {
43        // determine filename
44        let url = package
45            .url
46            .parse::<reqwest::Url>()
47            .with_context(|| anyhow!("Failed to parse string as url: {:?}", package.url))?;
48        let filename = url
49            .path_segments()
50            .context("Failed to get path from url")?
51            .next_back()
52            .context("Failed to find filename from url")?;
53        if filename.is_empty() {
54            bail!("Filename from url is empty");
55        }
56
57        // setup /extra/ directory
58        let source = pkgs_cache_dir.sha256_path(&package.sha256)?;
59        let dest = path.join(filename);
60        let dest_sig = path.join(filename.to_owned() + ".sig");
61
62        debug!("Trying to reflink {source:?} -> {dest:?}...");
63        if let Err(err) = clone_file::clone_file(&source, &dest) {
64            debug!("Failed to reflink, trying traditional copy: {err:#}");
65            fs::copy(&source, &dest)
66                .await
67                .context("Failed to copy package from cache to temporary folder")?;
68        }
69
70        // setup extra data
71        match package.system.as_str() {
72            "alpine" => (),
73            "archlinux" => {
74                let base64 = package
75                    .signature
76                    .as_ref()
77                    .context("Package in dependency lockfile is missing signature")?;
78                let signature = BASE64
79                    .decode(base64.as_bytes())
80                    .with_context(|| anyhow!("Failed to decode signature as base64: {base64:?}"))?;
81
82                debug!(
83                    "Writing signature ({} bytes) to {dest_sig:?}...",
84                    signature.len()
85                );
86                fs::write(dest_sig, signature).await?;
87            }
88            "debian" => (),
89            system => bail!("Unknown package system: {system:?}"),
90        }
91
92        // verify pkg content matches pin metadata
93        let pkg = fs::read(&dest).await?;
94        fetch::verify_pin_metadata(&pkg, &package)
95            .with_context(|| anyhow!("Failed to verify metadata for {filename:?}"))?;
96
97        install.add_pkg(package, filename.to_string())?;
98    }
99
100    Ok(install)
101}
102
103pub async fn run_build(
104    container: &Container,
105    build: &args::Build,
106    extra: Option<&(TempDir, Install)>,
107) -> Result<()> {
108    if let Some((_, install)) = extra {
109        if !install.alpine.is_empty() {
110            let mut cmd = vec![
111                "apk".to_string(),
112                "add".to_string(),
113                "--no-network".to_string(),
114                "--".to_string(),
115            ];
116            for (_, filename) in &install.alpine {
117                cmd.push(format!("/extra/{filename}"));
118            }
119
120            info!("Installing dependencies...");
121            container.exec(&cmd, container::Exec::default()).await?;
122        }
123
124        if !install.archlinux.is_empty() {
125            // determine verification timestamp and add it to gpg.conf
126            let filename_iter = install.archlinux.iter().map(|(pkg, _)| pkg);
127            if let Some(time) = pgp::find_max_signature_time(filename_iter)? {
128                let time = time
129                    .checked_add(Duration::from_secs(1))
130                    .with_context(|| anyhow!("Failed to increase time by 1 second {time:?}"))?;
131                let datetime = OffsetDateTime::from(time).format(&well_known::Rfc3339)?;
132
133                info!("Derived signature verification timestamp: {datetime:?}");
134                archlinux::set_pacman_verification_datetime(container, time).await?;
135            }
136
137            // prepare and execute the install command
138            let mut cmd = vec![
139                "pacman".to_string(),
140                "-U".to_string(),
141                "--noconfirm".to_string(),
142                "--".to_string(),
143            ];
144            for (_, filename) in &install.archlinux {
145                cmd.push(format!("/extra/{filename}"));
146            }
147
148            info!("Installing dependencies...");
149            container.exec(&cmd, container::Exec::default()).await?;
150        }
151
152        if !install.debian.is_empty() {
153            let mut cmd = vec![
154                "apt-get".to_string(),
155                "install".to_string(),
156                "--".to_string(),
157            ];
158            for (_, filename) in &install.debian {
159                cmd.push(format!("/extra/{filename}"));
160            }
161
162            info!("Installing dependencies...");
163            container.exec(&cmd, container::Exec::default()).await?;
164        }
165    }
166
167    info!("Running build...");
168    container
169        .exec(
170            &build.cmd,
171            container::Exec {
172                cwd: Some("/build"),
173                env: &build.env,
174                ..Default::default()
175            },
176        )
177        .await?;
178
179    Ok(())
180}
181
182pub async fn build(build: &args::Build) -> Result<()> {
183    container::test_for_unprivileged_userns_clone().await?;
184
185    // ensure arguments make sense
186    build.validate()?;
187
188    // load lockfile
189    let (manifest, lockfile) = build.load_files().await?;
190    if let Some(manifest) = &manifest {
191        if let Err(err) = manifest.satisfied_by(&lockfile) {
192            warn!("Lockfile might be out-of-sync: {err:#}");
193        }
194    }
195
196    // mount current directory into container
197    let pwd = env::current_dir()?;
198    let pwd = pwd
199        .into_os_string()
200        .into_string()
201        .map_err(|_| anyhow!("Failed to convert current path to utf-8"))?;
202
203    let mut mounts = vec![(pwd, "/build".to_string())];
204
205    // ignore packages that are already present in the container
206    let dependencies = lockfile
207        .packages
208        .into_iter()
209        .filter(|p| !p.installed)
210        .collect::<Vec<_>>();
211
212    let extra = if !dependencies.is_empty() {
213        fetch::download_dependencies(&dependencies).await?;
214
215        let path = paths::repro_env_dir()?;
216        let temp_dir = tempfile::Builder::new().prefix("env.").tempdir_in(path)?;
217        let pkgs = setup_extra_folder(temp_dir.path(), dependencies).await?;
218
219        let path = temp_dir
220            .path()
221            .to_owned()
222            .into_os_string()
223            .into_string()
224            .map_err(|_| anyhow!("Failed to convert temporary path to utf-8"))?;
225        mounts.push((path, "/extra".to_string()));
226
227        Some((temp_dir, pkgs))
228    } else {
229        None
230    };
231
232    let container = Container::create(
233        &lockfile.container.image,
234        container::Config {
235            mounts: &mounts,
236            expose_fuse: false,
237        },
238    )
239    .await?;
240    container
241        .run(run_build(&container, build, extra.as_ref()), build.keep)
242        .await
243}