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 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 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 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 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 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 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 build.validate()?;
187
188 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 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 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}