Skip to main content

repro_env/
fetch.rs

1use crate::args;
2use crate::container;
3use crate::errors::*;
4use crate::http;
5use crate::lockfile::{Lockfile, PackageLock};
6use crate::paths;
7use crate::pkgs;
8use sha2::{Digest, Sha256};
9use std::path::Path;
10use tokio::fs;
11use tokio::io::{AsyncSeekExt, AsyncWriteExt};
12
13pub async fn download_dependencies(dependencies: &[PackageLock]) -> Result<()> {
14    let client = http::Client::new()?;
15    let pkgs_cache_dir = paths::pkgs_cache_dir()?;
16
17    for package in dependencies {
18        trace!("Found dependencies: {package:?}");
19        let path = pkgs_cache_dir.sha256_path(&package.sha256)?;
20        if path.exists() {
21            debug!(
22                "Package already in cache: {:?} {:?}",
23                package.name, package.version
24            );
25        } else {
26            let parent = path
27                .parent()
28                .context("Failed to determine parent directory")?;
29            fs::create_dir_all(parent).await.with_context(|| {
30                anyhow!("Failed to create parent directories for file: {path:?}")
31            })?;
32
33            let mut dl_path = path.clone();
34            dl_path.as_mut_os_string().push(".tmp");
35
36            let file = fs::OpenOptions::new()
37                .write(true)
38                .create(true)
39                .truncate(false)
40                .open(&dl_path)
41                .await?;
42
43            let mut lock = fd_lock::RwLock::new(file);
44            debug!("Trying to acquire write lock for file: {path:?}");
45            let mut lock = lock
46                .write()
47                .with_context(|| anyhow!("Failed to acquire lock for {dl_path:?}"))?;
48
49            // check if file became available in meantime
50            if path.exists() {
51                debug!("File became available in the meantime, nothing to do");
52            } else {
53                debug!(
54                    "Downloading package into cache: {:?} {:?}",
55                    package.name, package.version
56                );
57                lock.set_len(0).await.context("Failed to truncate file")?;
58                lock.rewind()
59                    .await
60                    .context("Failed to rewind file to beginning")?;
61
62                let mut response = client.request(&package.url).await.with_context(|| {
63                    anyhow!("Failed to download package from url: {:?}", package.url)
64                })?;
65
66                let mut hasher = Sha256::new();
67                while let Some(chunk) = response
68                    .chunk()
69                    .await
70                    .context("Failed to read from download stream")?
71                {
72                    lock.write_all(&chunk)
73                        .await
74                        .context("Failed to write to downloaded data to disk")?;
75                    hasher.update(&chunk);
76                }
77                let result = hex::encode(hasher.finalize());
78
79                if package.sha256 != result {
80                    lock.set_len(0)
81                        .await
82                        .context("Mismatch of sha256, failed to truncate file")?;
83                    bail!(
84                        "Mismatch of sha256, expected={:?}, downloaded={:?}",
85                        package.sha256,
86                        result
87                    );
88                }
89
90                lock.sync_all()
91                    .await
92                    .context("Failed to sync downloaded data to disk")?;
93                fs::rename(&dl_path, &path)
94                    .await
95                    .with_context(|| anyhow!("Failed to rename {dl_path:?} to {path:?}"))?;
96            }
97        }
98    }
99
100    Ok(())
101}
102
103pub fn verify_pin_metadata(pkg: &[u8], pin: &PackageLock) -> Result<()> {
104    let pkg = match pin.system.as_str() {
105        "alpine" => pkgs::alpine::parse(pkg).context("Failed to parse data as alpine package")?,
106        "archlinux" => {
107            pkgs::archlinux::parse(pkg).context("Failed to parse data as archlinux package")?
108        }
109        "debian" => pkgs::debian::parse(pkg).context("Failed to parse data as debian package")?,
110        system => bail!("Unknown package system: {system:?}"),
111    };
112
113    debug!("Parsed embedded metadata from package: {pkg:?}");
114
115    if pin.name != pkg.name {
116        bail!(
117            "Package name in metadata doesn't match lockfile: expected={:?}, embedded={:?}",
118            pin.name,
119            pkg.name
120        );
121    }
122
123    if pin.version != pkg.version {
124        bail!(
125            "Package version in metadata doesn't match lockfile: expected={:?}, embedded={:?}",
126            pin.version,
127            pkg.version
128        );
129    }
130
131    Ok(())
132}
133
134pub async fn fetch(fetch: &args::Fetch) -> Result<()> {
135    // load lockfile
136    let path = fetch.file.as_deref().unwrap_or(Path::new("repro-env.lock"));
137    let buf = fs::read_to_string(path)
138        .await
139        .with_context(|| anyhow!("Failed to read dependency lockfile: {path:?}"))?;
140
141    let lockfile = Lockfile::deserialize(&buf)?;
142    trace!("Loaded dependency lockfile from file: {lockfile:?}");
143
144    if !fetch.no_pull {
145        let image = &lockfile.container.image;
146        if let Err(err) = container::inspect(image).await {
147            debug!("Could not find image in cache: {err:#}");
148            container::pull(image).await?;
149        } else {
150            info!("Found container image in local cache: {image:?}");
151        }
152    }
153
154    // ignore packages that are already present in the container
155    let dependencies = lockfile
156        .packages
157        .into_iter()
158        .filter(|p| !p.installed)
159        .collect::<Vec<_>>();
160
161    if !dependencies.is_empty() {
162        download_dependencies(&dependencies).await?;
163    }
164
165    Ok(())
166}