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 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}