Skip to main content

ambient_ci/action_impl/
debian.rs

1//! Actions related to Debian packages.
2
3#![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/// Download `deb` packages and their dependencies.
24#[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    /// Create a new `DebGet` action.
33    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/// Install downloaded `deb` packages.
112#[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/// Build a `deb` package.
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125pub struct Deb {
126    packages: Option<PathBuf>,
127}
128
129impl Deb {
130    /// Create a new `Deb` action.
131    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/// Errors from the `deb` action.
202#[derive(Debug, thiserror::Error)]
203pub enum DebError {
204    /// Failed to create the artifacts directory for `deb` packages.
205    #[error("could not create artifacts directory {0}")]
206    Mkdir(PathBuf, #[source] std::io::Error),
207
208    /// Failed to create the artifacts directory for `deb` packages.
209    #[error("could not create artifacts directory {0}")]
210    Mkdir2(PathBuf, #[source] crate::util::UtilError),
211
212    /// APT repository error.
213    #[error(transparent)]
214    Apt(#[from] repo::AptRepoError),
215
216    /// Parse URL.
217    #[error("failed to parse URL {0:?}")]
218    UrlParse(String, #[source] url::ParseError),
219
220    /// Download file
221    #[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
237/// Represent a Debian package archive ("APT repository").
238pub 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    /// A deb dependency that needs to be downloaded.
649    #[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        /// Name of package.
664        pub fn package_name(&self) -> &str {
665            &self.package_name
666        }
667
668        /// URL from where to download package.
669        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}