1#![allow(clippy::result_large_err)]
4
5use std::{
6 collections::HashMap,
7 path::{Path, PathBuf},
8};
9
10use serde::{Deserialize, Serialize};
11use url::Url;
12
13use crate::{
14 action::{ActionError, Context},
15 action_impl::{spawn, ActionImpl},
16 qemu,
17 util::{http_get_to_file, mkdir, UtilError},
18};
19
20const DEFAULT_SUITE: &str = "stable";
21const SUBDIR: &str = "debian";
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub struct DebGet {
26 packages: Vec<String>,
27 suite: Option<String>,
28 cert: Option<String>,
29}
30
31impl DebGet {
32 pub fn new(suite: &Option<String>, cert: &Option<String>, packages: &[String]) -> Self {
34 Self {
35 packages: packages.to_vec(),
36 suite: suite.clone(),
37 cert: cert.clone(),
38 }
39 }
40
41 fn suite(&self) -> &str {
42 self.suite.as_deref().unwrap_or(DEFAULT_SUITE)
43 }
44
45 fn packages_needed(
46 &self,
47 url: &str,
48 suite: &str,
49 arch: repo::Arch,
50 deps_dir: &Path,
51 ) -> Result<Vec<repo::Needed>, ActionError> {
52 let debian = deps_dir.join(SUBDIR);
53 mkdir(&debian).map_err(|err| DebError::Mkdir2(debian, err))?;
54
55 let cached_in_release_file = deps_dir.join(SUBDIR).join("InRelease");
56 let url = Url::parse(url).map_err(|err| DebError::UrlParse(url.to_string(), err))?;
57 let mut certs = repo::Certs::empty();
58 if let Some(cert) = &self.cert {
59 certs.push(cert);
60 } else {
61 certs.push_for_suite(suite).map_err(DebError::Apt)?;
62 }
63 let debian_suite = repo::DebianSuite::new(&cached_in_release_file, url, suite, &certs)
64 .map_err(DebError::Apt)?;
65
66 let cached_packages_gz_file = deps_dir.join(SUBDIR).join("Packages.gz");
67 let packages_file = debian_suite
68 .packages_file(&cached_packages_gz_file, suite, arch)
69 .map_err(DebError::Apt)?;
70 let packages: HashMap<String, repo::Package> =
71 HashMap::from_iter(packages_file.split("\n\n").filter(|s| !s.is_empty()).map(
72 |stanza| match repo::Package::new(stanza) {
73 Ok(p) => (p.package.clone(), p),
74 Err(err) => panic!("can't parse package: {err}\nProblem stanza: {stanza:?}"),
75 },
76 ));
77
78 Ok(debian_suite.needed(&self.packages, &packages))
79 }
80
81 fn download_needed(&self, deps_dir: &Path, needed: &[repo::Needed]) -> Result<(), ActionError> {
82 for n in needed {
83 let url =
84 Url::parse(n.url()).map_err(|err| DebError::UrlParse(n.url().to_string(), err))?;
85 let filename = deps_dir
86 .join("debian")
87 .join(format!("{}.deb", n.package_name()));
88 http_get_to_file(url.as_str(), &filename)
89 .map_err(|err| DebError::HttpGet(url, filename.clone(), err))?;
90 }
91 Ok(())
92 }
93}
94
95impl ActionImpl for DebGet {
96 fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
97 use repo::*;
98
99 const URL: &str = "http://deb.debian.org/debian";
100 const ARCH: Arch = Arch::Amd64;
101
102 let needed = self.packages_needed(URL, self.suite(), ARCH, context.deps_dir())?;
103 self.download_needed(context.deps_dir(), &needed)?;
104 context
105 .runlog()
106 .deb_get(crate::runlog::RunLogSource::PrePlan, &needed);
107 Ok(())
108 }
109}
110
111#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
113pub struct DebInstall {}
114
115impl ActionImpl for DebInstall {
116 fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
117 let shell = "apt-get install -y /ci/deps/debian/*.deb";
118 spawn(context, &["/bin/bash", "-c", shell])?;
119 Ok(())
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125pub struct Deb {
126 packages: Option<PathBuf>,
127}
128
129impl Deb {
130 pub fn new<P: AsRef<Path>>(packages: P) -> Self {
132 Self {
133 packages: Some(packages.as_ref().to_path_buf()),
134 }
135 }
136}
137
138impl ActionImpl for Deb {
139 fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
140 let packages = Path::new(qemu::ARTIFACTS_DIR)
141 .join(self.packages.clone().unwrap_or(PathBuf::from(".")));
142 if !packages.exists() {
143 std::fs::create_dir(&packages).map_err(|err| DebError::mkdir(&packages, err))?;
144 }
145 let shell = format!(
146 r#"#!/usr/bin/env bash
147set -xeuo pipefail
148
149echo "PATH at start: $PATH"
150export PATH="/root/.cargo/bin:$PATH"
151export CARGO_HOME=/ci/deps
152export DEBEMAIL=liw@liw.fi
153export DEBFULLNAME="Lars Wirzenius"
154/bin/env
155
156command -v cargo
157command -v rustc
158
159cargo --version
160rustc --version
161
162# Get name and version of source package.
163name="$(dpkg-parsechangelog -SSource)"
164version="$(dpkg-parsechangelog -SVersion)"
165
166# Get upstream version: everything before the last dash.
167uv="$(echo "$version" | sed 's/-[^-]*$//')"
168
169# Files that will be created.
170arch="$(dpkg --print-architecture)"
171orig="../${{name}}_${{uv}}.orig.tar.xz"
172deb="../${{name}}_${{version}}_${{arch}}.deb"
173changes="../${{name}}_${{version}}_${{arch}}.changes"
174
175# Create "upstream tarball".
176git archive HEAD | xz >"$orig"
177
178# Build package.
179dpkg-buildpackage -us -uc
180
181# Dump some information to make it easier to visually verify
182# everything looks OK. Also, test the package with the lintian tool.
183
184ls -l ..
185for x in ../*.deb; do dpkg -c "$x"; done
186# FIXME: disabled while this prevents radicle-native-ci deb from being built.
187# lintian -i --allow-root --fail-on warning ../*.changes
188
189# Move files to artifacts directory.
190mv ../*_* {}
191 "#,
192 packages.display()
193 );
194
195 spawn(context, &["/bin/bash", "-c", &shell])?;
196
197 Ok(())
198 }
199}
200
201#[derive(Debug, thiserror::Error)]
203pub enum DebError {
204 #[error("could not create artifacts directory {0}")]
206 Mkdir(PathBuf, #[source] std::io::Error),
207
208 #[error("could not create artifacts directory {0}")]
210 Mkdir2(PathBuf, #[source] crate::util::UtilError),
211
212 #[error(transparent)]
214 Apt(#[from] repo::AptRepoError),
215
216 #[error("failed to parse URL {0:?}")]
218 UrlParse(String, #[source] url::ParseError),
219
220 #[error("failed to download {0} to {1}")]
222 HttpGet(Url, PathBuf, #[source] UtilError),
223}
224
225impl DebError {
226 fn mkdir<P: Into<PathBuf>>(dirname: P, err: std::io::Error) -> Self {
227 Self::Mkdir(dirname.into(), err)
228 }
229}
230
231impl From<DebError> for ActionError {
232 fn from(value: DebError) -> Self {
233 Self::Deb(value)
234 }
235}
236
237pub mod repo {
239 use std::{
240 collections::{HashMap, HashSet},
241 io::Read,
242 path::{Path, PathBuf},
243 process::Command,
244 };
245
246 use rfc822_like::Deserializer;
247 use serde::{Deserialize, Serialize};
248
249 use clingwrap::runner::{CommandError, CommandRunner};
250 use flate2::read::GzDecoder;
251 use reqwest::StatusCode;
252 use url::Url;
253
254 use crate::util::{http_get_to_file, UtilError};
255
256 const DEBIAN_TRIXIE_CERT: &str = r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
257
258mDMEZ+Gq1RYJKwYBBAHaRw8BAQdARlh1OX84KPJRedAP/M7WxPFEthWypAp8nved
259FhqaX0q0R0RlYmlhbiBTdGFibGUgUmVsZWFzZSBLZXkgKDEzL3RyaXhpZSkgPGRl
260Ymlhbi1yZWxlYXNlQGxpc3RzLmRlYmlhbi5vcmc+iJYEExYIAD4WIQRBWH99uMd0
261vM8TFBZ2L2egssOd5AUCZ+Gq1QIbAwUJDwmcAAULCQgHAgYVCgkICwIEFgIDAQIe
262AQIXgAAKCRB2L2egssOd5DjVAP49S/e9VAtn9ip2DXj5zx87MpUXnLHAhT/LsJ7J
263odnKoQEA8murEqdcC0pPsAA6Gmev/lz0MWUyfe5Y6DMB6t16FQuIdQQTFgoAHRYh
264BMphnWWnKnut/JbSgBlkGKrrdMihBQJn4atmAAoJEBlkGKrrdMihxnAA/3i7WOUU
265Dyea6tABVGfFKfHj6yAbfcKVr5GL0WSOQiXNAQC2YTKoTfCY/WOZnCwEHebpM3Mr
2666+A8lenj8pFzm8mFAokCMgQQAQoAHRYhBHIDYw4sjnJyUWhP68XOXcLFQs1ZBQJn
2674v59AAoJEMXOXcLFQs1ZfdkP90fbXOydWNb1iXwu/vaUjxSx5Nk0Zwkjj7Pi74PZ
268Ifd9c0Luf/j7dEHuJOzkOKvkrpTYeQN8Ms9ITVTMeNSOoQn8tnYsxhHqHUcI9ym/
269vJ6liIsE2K95gwH2mOQ24ot59pMyQX7sXE4DuQqlrEW1JRemKs7WJB2E/4I7smqG
2703+/mqXAoHKkpFBMm1v9tNVx1Tp3ER6C4VFszONlmX0NLB5If+wSits1LnsVgBK1D
271Hd3Nxh78YQL0W2xBCOfsHryaKkLW6tp1efTpMHfe4lDEgTFvEWaO3NlPEeD67uxA
272KpEG1b/CzBqx+Og1RT+0iZkW9oXYoKJI0Vx0HlYeMrhuE+Q0Fqy/nZYcWhq3y6Tu
273GC9J+hyB7D3Z7ne48zNGaAU4x7+6pvkmlBWnNwPH/IlGbexa/F8o+rU0AWrvYCVR
274cbbcSsgkq1Dl/Xjbvlx9gwkKnhacbQ5VUOA2Zkp6oBn/pFD1W8xE0X4dNHcM3J5k
275CAeZuszjPIZXvwuvAjaOE7ZxKvGDlQTWK8zR5h9tvcwMfVND+EqK0qfDGDMtJqu/
276W0lSJj3G/FMO3aGn0Ks6b7cmjbCGjmn7WBGJT2PvH4A4ml1qopWy7Ont6zt1+Cll
277y/0Uf75AamJSrAAYFFIJlcpWH/7NRW2L+n1FoIOv5xj5h675uHh/WFlPTNQqrRaP
278p1o=
279=k1cs
280-----END PGP PUBLIC KEY BLOCK-----
281"#;
282
283 const DEBIAN_BOOKWORM_CERT: &str = r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
284Comment: B8B8 0B5B 623E AB6A D877 5C45 B7C5 D7D6 3509 47F8
285Comment: Debian Archive Automatic Signing Key (12/bookworm) <ftp
286
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
468NlB2T6bKGcYGqL7L
469=AKf0
470-----END PGP PUBLIC KEY BLOCK-----
471"#;
472
473 pub(super) struct Certs {
474 certs: Vec<String>,
475 }
476
477 impl Certs {
478 pub fn empty() -> Self {
479 Self { certs: vec![] }
480 }
481
482 pub fn push(&mut self, cert: impl Into<String>) {
483 self.certs.push(cert.into());
484 }
485
486 pub fn push_for_suite(&mut self, suite: &str) -> Result<(), AptRepoError> {
487 let cert = match suite {
488 "trixie" => DEBIAN_TRIXIE_CERT,
489 "bookworm" => DEBIAN_BOOKWORM_CERT,
490 _ => return Err(AptRepoError::UnknownRelease(suite.into())),
491 };
492 self.push(cert);
493 Ok(())
494 }
495 }
496
497 impl Default for Certs {
498 fn default() -> Self {
499 Self {
500 certs: vec![DEBIAN_TRIXIE_CERT.to_string()],
501 }
502 }
503 }
504
505 pub(super) struct DebianSuite {
506 base_url: Url,
507 in_release: InRelease,
508 }
509
510 impl DebianSuite {
511 pub fn new(
512 cached_in_release: &Path,
513 base_url: Url,
514 suite: &str,
515 allowed_certs: &Certs,
516 ) -> Result<Self, AptRepoError> {
517 let in_release_url = Url::parse(&format!("{}/dists/{suite}/InRelease", base_url))?;
518
519 let signed = http_get_to_file(in_release_url.as_str(), cached_in_release)?;
520 let in_release = inline_verify(&signed, allowed_certs)?;
521
522 Ok(Self {
523 base_url,
524 in_release: InRelease::new(&in_release)?,
525 })
526 }
527
528 #[allow(clippy::unwrap_used)]
529 pub fn packages_file(
530 &self,
531 cached_file: &Path,
532 suite: &str,
533 arch: Arch,
534 ) -> Result<String, AptRepoError> {
535 for line in self
536 .in_release
537 .sha256
538 .lines()
539 .filter(|line| !line.is_empty())
540 {
541 let mut words = line.split_ascii_whitespace();
542 let checksum = words.next().unwrap();
543 let _length = words.next().unwrap();
544 let filename = words.next().unwrap();
545 let wanted = format!("main/binary-{arch}/Packages.gz");
546 if filename == wanted {
547 let url = format!("{}/dists/{suite}/{filename}", self.base_url);
548 let data = http_get_to_file(&url, cached_file)?;
549 let actual = sha256::digest(&data);
550
551 if actual == checksum {
552 let mut gz = GzDecoder::new(&*data);
553 let mut packages = vec![];
554 gz.read_to_end(&mut packages)
555 .map_err(|source| AptRepoError::Inflate { source })?;
556 let data = String::from_utf8_lossy(&packages).to_string();
557 return Ok(data);
558 } else {
559 panic!("bad checksum");
560 }
561 }
562 }
563 Err(AptRepoError::NoPackagesGz)
564 }
565
566 pub fn needed(
567 &self,
568 package_names: &[String],
569 packages: &HashMap<String, Package>,
570 ) -> Vec<Needed> {
571 let mut wanted = HashMap::new();
572 let mut todo: HashSet<String> =
573 HashSet::from_iter(package_names.iter().map(|s| s.to_string()));
574 while !todo.is_empty() {
575 let mut new = vec![];
576 for name in todo.drain() {
577 if !wanted.contains_key(&name) {
578 if let Some(p) = packages.get(&name) {
579 wanted.insert(name.to_string(), p.clone());
580 for need in p.needs() {
581 new.push(need);
582 }
583 }
584 }
585 }
586 for name in new {
587 todo.insert(name);
588 }
589 }
590
591 wanted
592 .values()
593 .map(|p| {
594 let url = format!("{}/{}", self.base_url, p.filename);
595 Needed::new(p.package.clone(), url)
596 })
597 .collect()
598 }
599 }
600
601 #[derive(Debug, Deserialize)]
602 #[serde(rename_all = "PascalCase")]
603 struct InRelease {
604 #[serde(rename = "SHA256")]
605 sha256: String,
606 }
607
608 impl InRelease {
609 fn new(s: impl AsRef<str>) -> Result<Self, rfc822_like::de::Error> {
610 Self::deserialize(Deserializer::new(s.as_ref().as_bytes()))
611 }
612 }
613
614 #[derive(Debug, Clone, Deserialize)]
615 #[serde(rename_all = "PascalCase")]
616 pub(super) struct Package {
617 pub package: String,
618 pre_depends: Option<String>,
619 depends: Option<String>,
620 filename: String,
621 }
622
623 impl Package {
624 pub fn new(s: impl AsRef<str>) -> Result<Self, rfc822_like::de::Error> {
625 Self::deserialize(Deserializer::new(s.as_ref().as_bytes()))
626 }
627
628 pub fn needs(&self) -> Vec<String> {
629 let mut needs = vec![];
630 if let Some(x) = &self.pre_depends {
631 self.parse_needs(&mut needs, x);
632 }
633 if let Some(x) = &self.depends {
634 self.parse_needs(&mut needs, x);
635 }
636 needs
637 }
638
639 #[allow(clippy::unwrap_used)]
640 fn parse_needs(&self, needs: &mut Vec<String>, x: &str) {
641 for item in x.split(',') {
642 let item = item.split_ascii_whitespace().next().unwrap();
643 needs.push(item.to_string());
644 }
645 }
646 }
647
648 #[derive(Debug, Clone, Deserialize, Serialize)]
650 pub struct Needed {
651 package_name: String,
652 url: String,
653 }
654
655 impl Needed {
656 fn new(package_name: impl Into<String>, url: impl Into<String>) -> Self {
657 Self {
658 package_name: package_name.into(),
659 url: url.into(),
660 }
661 }
662
663 pub fn package_name(&self) -> &str {
665 &self.package_name
666 }
667
668 pub fn url(&self) -> &str {
670 &self.url
671 }
672 }
673
674 #[derive(Debug, Copy, Clone)]
675 pub(super) enum Arch {
676 Amd64,
677 }
678
679 impl std::fmt::Display for Arch {
680 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
681 let arch = match self {
682 Self::Amd64 => "amd64",
683 };
684 write!(f, "{arch}")
685 }
686 }
687
688 fn inline_verify(data: &[u8], allowed_certs: &Certs) -> Result<String, AptRepoError> {
689 let mut cmd = Command::new("rsop");
690 cmd.arg("inline-verify");
691
692 let tmp = tempfile::tempdir().map_err(|source| AptRepoError::TempDir { source })?;
693 for (i, cert) in allowed_certs.certs.iter().enumerate() {
694 let filename = tmp.path().join(format!("cert-{i}"));
695 std::fs::write(&filename, cert.as_bytes())
696 .map_err(|source| AptRepoError::CertFile { source })?;
697 cmd.arg(filename);
698 }
699
700 let mut runner = CommandRunner::new(cmd);
701 runner.feed_stdin(data);
702 runner.capture_stdout();
703 let output = runner
704 .execute()
705 .map_err(|source| AptRepoError::InlineVerify { source })?;
706 String::from_utf8(output.stdout).map_err(|source| AptRepoError::InReleaseUtf8 { source })
707 }
708
709 #[derive(Debug, thiserror::Error)]
710 #[allow(missing_docs)]
711 pub enum AptRepoError {
712 #[error("failed to fetch InRelease file for {suite} from {url}")]
713 FetchInRelease {
714 suite: String,
715 url: Url,
716 source: reqwest::Error,
717 },
718
719 #[error("failed to retrieve InRelease text from HTTP response")]
720 InReleaseText { source: reqwest::Error },
721
722 #[error(transparent)]
723 Join(#[from] url::ParseError),
724
725 #[error("failed to verify InRelease inline signature")]
726 InlineVerify { source: CommandError },
727
728 #[error("InRelease file is not UTF8")]
729 InReleaseUtf8 { source: std::string::FromUtf8Error },
730
731 #[error("failed to create temporary directory for verifying InRelease signature")]
732 TempDir { source: std::io::Error },
733
734 #[error("failed to write temparary file for verifying InRelease signature")]
735 CertFile { source: std::io::Error },
736
737 #[error("InRelease file doesn't have a single stanza")]
738 NoStanzas,
739
740 #[error("InRelease does not have a SHA256 field")]
741 NoSha256Field,
742
743 #[error("InRelease SHA256 field line lacks {0}")]
744 Sha256Field(&'static str),
745
746 #[error("failed to download Packages.gz file")]
747 FetchPackagesGz { source: reqwest::Error },
748
749 #[error("failed to get Packages.gz file from HTTP request")]
750 PackagesGz { source: reqwest::Error },
751
752 #[error("failed to decompress Packages.gz file")]
753 Inflate { source: std::io::Error },
754
755 #[error("Packages file is not UTF8")]
756 PackagesUtf8 { source: std::string::FromUtf8Error },
757
758 #[error("Packages.gz has the wrong checksum")]
759 PackagesChecksum,
760
761 #[error("failed to find desired Packages.gz in InRelease file")]
762 NoPackagesGz,
763
764 #[error(transparent)]
765 HttpGet(#[from] crate::action_impl::HttpGetError),
766
767 #[error("failed to format timestamp")]
768 TimeFormat { source: time::error::Format },
769
770 #[error("failed to create HTTP client")]
771 ClientBuild(#[source] reqwest::Error),
772
773 #[error("failed to build a reqwest client")]
774 Client(#[source] reqwest::Error),
775
776 #[error("failed to build a reqwest request")]
777 BuildRequest(#[source] reqwest::Error),
778
779 #[error("failed to GET URL {0:?}")]
780 Get(String, reqwest::Error),
781
782 #[error("failed to get body of response from {0:?}")]
783 GetBody(String, reqwest::Error),
784
785 #[error("failure getting file with HTTP GET: status code {0}")]
786 UnwantedStatus(StatusCode),
787
788 #[error("failed to write file {filename}")]
789 Write {
790 filename: PathBuf,
791 source: std::io::Error,
792 },
793
794 #[error("failed to read file {filename}")]
795 Read {
796 filename: PathBuf,
797 source: std::io::Error,
798 },
799
800 #[error("entry for {package} in Packages file lacks Filename field")]
801 NoFilename { package: String },
802
803 #[error(transparent)]
804 Util(#[from] UtilError),
805
806 #[error("failed to parse URL {0:?}")]
807 UrlParse(String, #[source] url::ParseError),
808
809 #[error(transparent)]
810 Rfc822Like(#[from] rfc822_like::de::Error),
811
812 #[error("do not know archive signing key for release {0}")]
813 UnknownRelease(String),
814 }
815}