Skip to main content

repro_env/resolver/
archlinux.rs

1use crate::args;
2use crate::container::{self, Container};
3use crate::errors::*;
4use crate::lockfile::{ContainerLock, PackageLock};
5use crate::manifest::PackagesManifest;
6use flate2::read::GzDecoder;
7use std::collections::{HashMap, HashSet};
8use std::io::Read;
9
10#[derive(Debug, Default, PartialEq)]
11pub struct Package {
12    pub values: HashMap<String, Vec<String>>,
13}
14
15impl Package {
16    pub fn parse(buf: &str) -> Result<Self> {
17        let mut pkg = Self::default();
18
19        let mut lines = buf.lines();
20        while let Some(section) = lines.next() {
21            let mut values = Vec::new();
22            for line in &mut lines {
23                if line.is_empty() {
24                    break;
25                }
26                values.push(line.to_string());
27            }
28            pkg.values.insert(section.to_string(), values);
29        }
30
31        Ok(pkg)
32    }
33
34    pub fn add_values(&mut self, key: &str, values: &[&str]) {
35        let values = values.iter().map(|s| s.to_string()).collect();
36        self.values.insert(key.to_string(), values);
37    }
38
39    pub fn single_value(&self, key: &str) -> Result<&str> {
40        let values = self
41            .values
42            .get(key)
43            .with_context(|| anyhow!("Failed to find key in package metadata: {key:?}"))?;
44        let mut values = values.iter();
45
46        let value = values
47            .next()
48            .with_context(|| anyhow!("No value available for {key:?}"))?;
49
50        if let Some(trailing) = values.next() {
51            bail!("Unexpected trailing value in {key:?}: {trailing:?}");
52        }
53
54        Ok(value)
55    }
56
57    pub fn name(&self) -> Result<&str> {
58        self.single_value("%NAME%")
59    }
60
61    pub fn archive_url(&self) -> Result<String> {
62        let filename = self.single_value("%FILENAME%")?;
63        let pkgname = self.name()?;
64        let idx = pkgname
65            .chars()
66            .next()
67            .context("Name for package is empty")?;
68        Ok(format!(
69            "https://archive.archlinux.org/packages/{idx}/{pkgname}/{filename}"
70        ))
71    }
72
73    pub fn sha256(&self) -> Result<&str> {
74        self.single_value("%SHA256SUM%")
75    }
76
77    pub fn signature(&self) -> Result<&str> {
78        self.single_value("%PGPSIG%")
79    }
80}
81
82#[derive(Debug, Default)]
83pub struct DatabaseCache {
84    imported_repositories: HashSet<String>,
85    packages: HashMap<String, Package>,
86}
87
88impl DatabaseCache {
89    pub fn has_repo(&self, repo: &str) -> bool {
90        self.imported_repositories.contains(repo)
91    }
92
93    pub fn import_repo(&mut self, repo: &str, buf: &[u8]) -> Result<()> {
94        let d = GzDecoder::new(buf);
95        let mut tar = tar::Archive::new(d);
96
97        for entry in tar.entries()? {
98            let mut entry = entry?;
99            if entry.header().entry_type() == tar::EntryType::Regular {
100                let mut buf = String::new();
101                trace!("Reading package from archive: {:?}", entry.path());
102                entry
103                    .read_to_string(&mut buf)
104                    .context("Failed to read database entry")?;
105
106                let pkg =
107                    Package::parse(&buf).context("Failed to parse database entry as package")?;
108
109                self.packages.insert(pkg.name()?.to_string(), pkg);
110            }
111        }
112
113        self.imported_repositories.insert(repo.to_string());
114        Ok(())
115    }
116
117    pub fn get_package(&self, name: &str) -> Result<&Package> {
118        self.packages
119            .get(name)
120            .context("Failed to find package in any database: {name:?}")
121    }
122}
123
124pub async fn resolve_dependencies(
125    container: &Container,
126    manifest: &PackagesManifest,
127    dependencies: &mut Vec<PackageLock>,
128) -> Result<()> {
129    info!("Syncing package datatabase...");
130    container
131        .exec(&["pacman", "-Sy"], container::Exec::default())
132        .await?;
133
134    info!("Resolving dependencies...");
135    let mut cmd = vec![
136        "pacman",
137        "-Sup",
138        "--noconfirm",
139        "--print-format",
140        "%r %n %v",
141        "--",
142    ];
143    for dep in &manifest.dependencies {
144        cmd.push(dep.as_str());
145    }
146    let buf = container
147        .exec(
148            &cmd,
149            container::Exec {
150                capture_stdout: true,
151                ..Default::default()
152            },
153        )
154        .await?;
155    let buf = String::from_utf8(buf).context("Failed to decode pacman output as utf8")?;
156
157    let mut dbs = DatabaseCache::default();
158    for line in buf.lines() {
159        let mut line = line.split(' ');
160        let repo = line.next().context("Missing repo in pacman output")?;
161        let name = line.next().context("Missing pkg name in pacman output")?;
162        let version = line.next().context("Missing version in pacman output")?;
163        if let Some(trailing) = line.next() {
164            bail!("Trailing data in pacman output: {trailing:?}");
165        }
166
167        debug!("Detected dependency name={name:?} version={version:?} repo={repo:?}");
168        if !dbs.has_repo(repo) {
169            let buf = container
170                .cat(&format!("/var/lib/pacman/sync/{repo}.db"))
171                .await?;
172            dbs.import_repo(repo, &buf)?;
173        }
174
175        let pkg = dbs.get_package(name)?;
176
177        // record provides if it mentions a dependency
178        let mut provides = Vec::new();
179        for value in pkg.values.get("%PROVIDES%").into_iter().flatten() {
180            if manifest.dependencies.contains(value) {
181                provides.push(value.to_string());
182            }
183        }
184
185        dependencies.push(PackageLock {
186            name: name.to_string(),
187            version: version.to_string(),
188            system: "archlinux".to_string(),
189            url: pkg.archive_url()?,
190            provides,
191            sha256: pkg.sha256()?.to_string(),
192            signature: Some(pkg.signature()?.to_string()),
193            installed: false,
194        });
195    }
196
197    Ok(())
198}
199
200pub async fn resolve(
201    update: &args::Update,
202    manifest: &PackagesManifest,
203    container: &ContainerLock,
204    dependencies: &mut Vec<PackageLock>,
205) -> Result<()> {
206    let container = Container::create(
207        &container.image,
208        container::Config {
209            mounts: &[],
210            expose_fuse: false,
211        },
212    )
213    .await?;
214    container
215        .run(
216            resolve_dependencies(&container, manifest, dependencies),
217            update.keep,
218        )
219        .await
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use flate2::write::GzEncoder;
226
227    #[test]
228    fn parse_pkg_entry() -> Result<()> {
229        let buf = r#"%FILENAME%
230zstd-1.5.5-1-x86_64.pkg.tar.zst
231
232%NAME%
233zstd
234
235%BASE%
236zstd
237
238%VERSION%
2391.5.5-1
240
241%DESC%
242Zstandard - Fast real-time compression algorithm
243
244%CSIZE%
245493009
246
247%ISIZE%
2481500453
249
250%MD5SUM%
2512ba620ed7816b97bcad1a721a2a9f6c4
252
253%SHA256SUM%
2541891970afabc725e72c6a9bb2c127d906c1d3cc70309336fbe87adbd460c05b8
255
256%PGPSIG%
257iQEzBAABCgAdFiEE5JnHn1PJalTlcv7hwGCGM3xQdz4FAmQ79ZMACgkQwGCGM3xQdz4V+Qf/Yz7Y+3WwSDKtspwcaEr3j95n1nN5+SAThl/OHe94WwmInDWV09GwM+Lrw6Y1RFDK1PI1ZLON3hOo/81udW0uCHJ4n0bnU/2x3B4UW82dcBqFBjiEqNEF1x6KcQGf9PE9seZndsiAxVzrbEH9u48RIHx0SuwWnzlryCoHPYTgYsPrpkH0IzLUerP2Lc8rjUR2eAKn6zoomb3mR74dPNMn2yx9gS0l+79EshQR8kWtOVvTv7xgRriWeJMBNoTTvDfiDq5B8395vPaBmSfrU0O3tvVF3eDAGtpxIb8hqfhtRqy3XqTcRrYaoj44KtJraGCbq5DrsImEdx5byS7qBhoheQ==
258
259%URL%
260https://facebook.github.io/zstd/
261
262%LICENSE%
263BSD
264GPL2
265
266%ARCH%
267x86_64
268
269%BUILDDATE%
2701681646714
271
272%PACKAGER%
273Jelle van der Waa <jelle@archlinux.org>
274
275%PROVIDES%
276libzstd.so=1-64
277
278%DEPENDS%
279glibc
280gcc-libs
281zlib
282xz
283lz4
284
285%MAKEDEPENDS%
286cmake
287gtest
288ninja
289"#;
290        let pkg = Package::parse(buf)?;
291        assert_eq!(pkg.name()?, "zstd");
292        assert_eq!(
293            pkg.archive_url()?,
294            "https://archive.archlinux.org/packages/z/zstd/zstd-1.5.5-1-x86_64.pkg.tar.zst"
295        );
296        assert_eq!(
297            pkg.sha256()?,
298            "1891970afabc725e72c6a9bb2c127d906c1d3cc70309336fbe87adbd460c05b8"
299        );
300        assert_eq!(pkg.signature()?, "iQEzBAABCgAdFiEE5JnHn1PJalTlcv7hwGCGM3xQdz4FAmQ79ZMACgkQwGCGM3xQdz4V+Qf/Yz7Y+3WwSDKtspwcaEr3j95n1nN5+SAThl/OHe94WwmInDWV09GwM+Lrw6Y1RFDK1PI1ZLON3hOo/81udW0uCHJ4n0bnU/2x3B4UW82dcBqFBjiEqNEF1x6KcQGf9PE9seZndsiAxVzrbEH9u48RIHx0SuwWnzlryCoHPYTgYsPrpkH0IzLUerP2Lc8rjUR2eAKn6zoomb3mR74dPNMn2yx9gS0l+79EshQR8kWtOVvTv7xgRriWeJMBNoTTvDfiDq5B8395vPaBmSfrU0O3tvVF3eDAGtpxIb8hqfhtRqy3XqTcRrYaoj44KtJraGCbq5DrsImEdx5byS7qBhoheQ==");
301        assert!(pkg.single_value("%DEPENDS%").is_err());
302
303        let mut expected = Package::default();
304        expected.add_values("%FILENAME%", &["zstd-1.5.5-1-x86_64.pkg.tar.zst"]);
305        expected.add_values("%NAME%", &["zstd"]);
306        expected.add_values("%BASE%", &["zstd"]);
307        expected.add_values("%VERSION%", &["1.5.5-1"]);
308        expected.add_values(
309            "%DESC%",
310            &["Zstandard - Fast real-time compression algorithm"],
311        );
312
313        expected.add_values("%CSIZE%", &["493009"]);
314        expected.add_values("%ISIZE%", &["1500453"]);
315        expected.add_values("%MD5SUM%", &["2ba620ed7816b97bcad1a721a2a9f6c4"]);
316        expected.add_values(
317            "%SHA256SUM%",
318            &["1891970afabc725e72c6a9bb2c127d906c1d3cc70309336fbe87adbd460c05b8"],
319        );
320        expected.add_values("%PGPSIG%", &[
321"iQEzBAABCgAdFiEE5JnHn1PJalTlcv7hwGCGM3xQdz4FAmQ79ZMACgkQwGCGM3xQdz4V+Qf/Yz7Y+3WwSDKtspwcaEr3j95n1nN5+SAThl/OHe94WwmInDWV09GwM+Lrw6Y1RFDK1PI1ZLON3hOo/81udW0uCHJ4n0bnU/2x3B4UW82dcBqFBjiEqNEF1x6KcQGf9PE9seZndsiAxVzrbEH9u48RIHx0SuwWnzlryCoHPYTgYsPrpkH0IzLUerP2Lc8rjUR2eAKn6zoomb3mR74dPNMn2yx9gS0l+79EshQR8kWtOVvTv7xgRriWeJMBNoTTvDfiDq5B8395vPaBmSfrU0O3tvVF3eDAGtpxIb8hqfhtRqy3XqTcRrYaoj44KtJraGCbq5DrsImEdx5byS7qBhoheQ=="]);
322        expected.add_values("%URL%", &["https://facebook.github.io/zstd/"]);
323        expected.add_values("%LICENSE%", &["BSD", "GPL2"]);
324        expected.add_values("%ARCH%", &["x86_64"]);
325        expected.add_values("%BUILDDATE%", &["1681646714"]);
326        expected.add_values("%PACKAGER%", &["Jelle van der Waa <jelle@archlinux.org>"]);
327        expected.add_values("%PROVIDES%", &["libzstd.so=1-64"]);
328        expected.add_values("%DEPENDS%", &["glibc", "gcc-libs", "zlib", "xz", "lz4"]);
329        expected.add_values("%MAKEDEPENDS%", &["cmake", "gtest", "ninja"]);
330
331        assert_eq!(pkg, expected);
332        Ok(())
333    }
334
335    #[test]
336    fn test_database_cache_import() -> Result<()> {
337        let mut db = DatabaseCache::default();
338        assert!(!db.has_repo("core"));
339
340        let data = {
341            let mut tar =
342                tar::Builder::new(GzEncoder::new(Vec::new(), flate2::Compression::default()));
343
344            let data = br#"%FILENAME%
345rust-1:1.70.0-1-x86_64.pkg.tar.zst
346
347%NAME%
348rust
349
350%BASE%
351rust
352
353%VERSION%
3541:1.70.0-1
355
356%DESC%
357Systems programming language focused on safety, speed and concurrency
358
359%CSIZE%
36090509601
361
362%ISIZE%
363483950051
364
365%MD5SUM%
366a8498a6e40c64d7b08d493133941e918
367
368%SHA256SUM%
3698d018b14d2226d76ee46ecd6e28f51ddfa7bfd930463e517eabd5d86f8a17851
370
371%PGPSIG%
372iIsEABYIADMWIQQGaHodnU+rCLUP2Ss7lKgOUKR3xwUCZHkDRRUcaGVmdGlnQGFyY2hsaW51eC5vcmcACgkQO5SoDlCkd8eCrQEA8y2X/SVbHhchDdfBUp+KBOFoqN63haT6TNq7MIFDvXoA/AwzQe1rwL0RfvxMh130A2wzrid77YXTOjk36QHPmGIL
373
374%URL%
375https://www.rust-lang.org/
376
377%LICENSE%
378Apache
379MIT
380
381%ARCH%
382x86_64
383
384%BUILDDATE%
3851685646983
386
387%PACKAGER%
388Jan Alexander Steffens (heftig) <heftig@archlinux.org>
389
390%REPLACES%
391cargo
392cargo-tree
393rust-docs<1:1.56.1-3
394rustfmt
395
396%CONFLICTS%
397cargo
398rust-docs<1:1.56.1-3
399rustfmt
400
401%PROVIDES%
402cargo
403rustfmt
404
405%DEPENDS%
406curl
407gcc
408gcc-libs
409libssh2
410llvm-libs
411
412%OPTDEPENDS%
413gdb: rust-gdb script
414lldb: rust-lldb script
415
416%MAKEDEPENDS%
417cmake
418lib32-gcc-libs
419libffi
420lld
421llvm
422musl
423ninja
424perl
425python
426rust
427wasi-libc
428
429%CHECKDEPENDS%
430gdb
431procps-ng
432
433"#;
434
435            let mut header = tar::Header::new_gnu();
436            header.set_path("rust-1:1.70.0-1/desc")?;
437            header.set_size(data.len() as u64);
438            header.set_cksum();
439            tar.append(&header, &data[..])?;
440
441            tar.into_inner()?.finish()?
442        };
443        db.import_repo("core", &data)?;
444        assert!(db.has_repo("core"));
445
446        let pkg = db.get_package("rust")?;
447        let mut expected = Package::default();
448        expected.add_values("%FILENAME%", &["rust-1:1.70.0-1-x86_64.pkg.tar.zst"]);
449        expected.add_values("%NAME%", &["rust"]);
450        expected.add_values("%BASE%", &["rust"]);
451        expected.add_values("%VERSION%", &["1:1.70.0-1"]);
452        expected.add_values(
453            "%DESC%",
454            &["Systems programming language focused on safety, speed and concurrency"],
455        );
456        expected.add_values("%CSIZE%", &["90509601"]);
457        expected.add_values("%ISIZE%", &["483950051"]);
458        expected.add_values("%MD5SUM%", &["a8498a6e40c64d7b08d493133941e918"]);
459        expected.add_values(
460            "%SHA256SUM%",
461            &["8d018b14d2226d76ee46ecd6e28f51ddfa7bfd930463e517eabd5d86f8a17851"],
462        );
463        expected.add_values("%PGPSIG%", &["iIsEABYIADMWIQQGaHodnU+rCLUP2Ss7lKgOUKR3xwUCZHkDRRUcaGVmdGlnQGFyY2hsaW51eC5vcmcACgkQO5SoDlCkd8eCrQEA8y2X/SVbHhchDdfBUp+KBOFoqN63haT6TNq7MIFDvXoA/AwzQe1rwL0RfvxMh130A2wzrid77YXTOjk36QHPmGIL"]);
464        expected.add_values("%URL%", &["https://www.rust-lang.org/"]);
465        expected.add_values("%LICENSE%", &["Apache", "MIT"]);
466        expected.add_values("%ARCH%", &["x86_64"]);
467        expected.add_values("%BUILDDATE%", &["1685646983"]);
468        expected.add_values(
469            "%PACKAGER%",
470            &["Jan Alexander Steffens (heftig) <heftig@archlinux.org>"],
471        );
472        expected.add_values(
473            "%REPLACES%",
474            &["cargo", "cargo-tree", "rust-docs<1:1.56.1-3", "rustfmt"],
475        );
476        expected.add_values("%CONFLICTS%", &["cargo", "rust-docs<1:1.56.1-3", "rustfmt"]);
477        expected.add_values("%PROVIDES%", &["cargo", "rustfmt"]);
478        expected.add_values(
479            "%DEPENDS%",
480            &["curl", "gcc", "gcc-libs", "libssh2", "llvm-libs"],
481        );
482        expected.add_values(
483            "%OPTDEPENDS%",
484            &["gdb: rust-gdb script", "lldb: rust-lldb script"],
485        );
486        expected.add_values(
487            "%MAKEDEPENDS%",
488            &[
489                "cmake",
490                "lib32-gcc-libs",
491                "libffi",
492                "lld",
493                "llvm",
494                "musl",
495                "ninja",
496                "perl",
497                "python",
498                "rust",
499                "wasi-libc",
500            ],
501        );
502        expected.add_values("%CHECKDEPENDS%", &["gdb", "procps-ng"]);
503        assert_eq!(pkg, &expected);
504
505        Ok(())
506    }
507}