#![allow(clippy::result_large_err)]
use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use serde::{Deserialize, Serialize};
use url::Url;
use crate::{
action::{ActionError, Context},
action_impl::{spawn, ActionImpl},
qemu,
util::{http_get_to_file, mkdir, UtilError},
};
const DEFAULT_SUITE: &str = "stable";
const SUBDIR: &str = "debian";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DebGet {
packages: Vec<String>,
suite: Option<String>,
cert: Option<String>,
}
impl DebGet {
pub fn new(suite: &Option<String>, cert: &Option<String>, packages: &[String]) -> Self {
Self {
packages: packages.to_vec(),
suite: suite.clone(),
cert: cert.clone(),
}
}
fn suite(&self) -> &str {
self.suite.as_deref().unwrap_or(DEFAULT_SUITE)
}
fn packages_needed(
&self,
url: &str,
suite: &str,
arch: repo::Arch,
deps_dir: &Path,
) -> Result<Vec<repo::Needed>, ActionError> {
let debian = deps_dir.join(SUBDIR);
mkdir(&debian).map_err(|err| DebError::Mkdir2(debian, err))?;
let cached_in_release_file = deps_dir.join(SUBDIR).join("InRelease");
let url = Url::parse(url).map_err(|err| DebError::UrlParse(url.to_string(), err))?;
let mut certs = repo::Certs::empty();
if let Some(cert) = &self.cert {
certs.push(cert);
} else {
certs.push_for_suite(suite).map_err(DebError::Apt)?;
}
let debian_suite = repo::DebianSuite::new(&cached_in_release_file, url, suite, &certs)
.map_err(DebError::Apt)?;
let cached_packages_gz_file = deps_dir.join(SUBDIR).join("Packages.gz");
let packages_file = debian_suite
.packages_file(&cached_packages_gz_file, suite, arch)
.map_err(DebError::Apt)?;
let packages: HashMap<String, repo::Package> =
HashMap::from_iter(packages_file.split("\n\n").filter(|s| !s.is_empty()).map(
|stanza| match repo::Package::new(stanza) {
Ok(p) => (p.package.clone(), p),
Err(err) => panic!("can't parse package: {err}\nProblem stanza: {stanza:?}"),
},
));
Ok(debian_suite.needed(&self.packages, &packages))
}
fn download_needed(&self, deps_dir: &Path, needed: &[repo::Needed]) -> Result<(), ActionError> {
for n in needed {
let url =
Url::parse(n.url()).map_err(|err| DebError::UrlParse(n.url().to_string(), err))?;
let filename = deps_dir
.join("debian")
.join(format!("{}.deb", n.package_name()));
http_get_to_file(url.as_str(), &filename)
.map_err(|err| DebError::HttpGet(url, filename.clone(), err))?;
}
Ok(())
}
}
impl ActionImpl for DebGet {
fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
use repo::*;
const URL: &str = "http://deb.debian.org/debian";
const ARCH: Arch = Arch::Amd64;
let needed = self.packages_needed(URL, self.suite(), ARCH, context.deps_dir())?;
self.download_needed(context.deps_dir(), &needed)?;
context
.runlog()
.deb_get(crate::runlog::RunLogSource::PrePlan, &needed);
Ok(())
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DebInstall {}
impl ActionImpl for DebInstall {
fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
let shell = "apt-get install -y /ci/deps/debian/*.deb";
spawn(context, &["/bin/bash", "-c", shell])?;
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Deb {
packages: Option<PathBuf>,
}
impl Deb {
pub fn new<P: AsRef<Path>>(packages: P) -> Self {
Self {
packages: Some(packages.as_ref().to_path_buf()),
}
}
}
impl ActionImpl for Deb {
fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
let packages = Path::new(qemu::ARTIFACTS_DIR)
.join(self.packages.clone().unwrap_or(PathBuf::from(".")));
if !packages.exists() {
std::fs::create_dir(&packages).map_err(|err| DebError::mkdir(&packages, err))?;
}
let shell = format!(
r#"#!/usr/bin/env bash
set -xeuo pipefail
echo "PATH at start: $PATH"
export PATH="/root/.cargo/bin:$PATH"
export CARGO_HOME=/ci/deps
export DEBEMAIL=liw@liw.fi
export DEBFULLNAME="Lars Wirzenius"
/bin/env
command -v cargo
command -v rustc
cargo --version
rustc --version
# Get name and version of source package.
name="$(dpkg-parsechangelog -SSource)"
version="$(dpkg-parsechangelog -SVersion)"
# Get upstream version: everything before the last dash.
uv="$(echo "$version" | sed 's/-[^-]*$//')"
# Files that will be created.
arch="$(dpkg --print-architecture)"
orig="../${{name}}_${{uv}}.orig.tar.xz"
deb="../${{name}}_${{version}}_${{arch}}.deb"
changes="../${{name}}_${{version}}_${{arch}}.changes"
# Create "upstream tarball".
git archive HEAD | xz >"$orig"
# Build package.
dpkg-buildpackage -us -uc
# Dump some information to make it easier to visually verify
# everything looks OK. Also, test the package with the lintian tool.
ls -l ..
for x in ../*.deb; do dpkg -c "$x"; done
# FIXME: disabled while this prevents radicle-native-ci deb from being built.
# lintian -i --allow-root --fail-on warning ../*.changes
# Move files to artifacts directory.
mv ../*_* {}
"#,
packages.display()
);
spawn(context, &["/bin/bash", "-c", &shell])?;
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
pub enum DebError {
#[error("could not create artifacts directory {0}")]
Mkdir(PathBuf, #[source] std::io::Error),
#[error("could not create artifacts directory {0}")]
Mkdir2(PathBuf, #[source] crate::util::UtilError),
#[error(transparent)]
Apt(#[from] repo::AptRepoError),
#[error("failed to parse URL {0:?}")]
UrlParse(String, #[source] url::ParseError),
#[error("failed to download {0} to {1}")]
HttpGet(Url, PathBuf, #[source] UtilError),
}
impl DebError {
fn mkdir<P: Into<PathBuf>>(dirname: P, err: std::io::Error) -> Self {
Self::Mkdir(dirname.into(), err)
}
}
impl From<DebError> for ActionError {
fn from(value: DebError) -> Self {
Self::Deb(value)
}
}
pub mod repo {
use std::{
collections::{HashMap, HashSet},
io::Read,
path::{Path, PathBuf},
process::Command,
};
use rfc822_like::Deserializer;
use serde::{Deserialize, Serialize};
use clingwrap::runner::{CommandError, CommandRunner};
use flate2::read::GzDecoder;
use reqwest::StatusCode;
use url::Url;
use crate::util::{http_get_to_file, UtilError};
const DEBIAN_TRIXIE_CERT: &str = r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEZ+Gq1RYJKwYBBAHaRw8BAQdARlh1OX84KPJRedAP/M7WxPFEthWypAp8nved
FhqaX0q0R0RlYmlhbiBTdGFibGUgUmVsZWFzZSBLZXkgKDEzL3RyaXhpZSkgPGRl
Ymlhbi1yZWxlYXNlQGxpc3RzLmRlYmlhbi5vcmc+iJYEExYIAD4WIQRBWH99uMd0
vM8TFBZ2L2egssOd5AUCZ+Gq1QIbAwUJDwmcAAULCQgHAgYVCgkICwIEFgIDAQIe
AQIXgAAKCRB2L2egssOd5DjVAP49S/e9VAtn9ip2DXj5zx87MpUXnLHAhT/LsJ7J
odnKoQEA8murEqdcC0pPsAA6Gmev/lz0MWUyfe5Y6DMB6t16FQuIdQQTFgoAHRYh
BMphnWWnKnut/JbSgBlkGKrrdMihBQJn4atmAAoJEBlkGKrrdMihxnAA/3i7WOUU
Dyea6tABVGfFKfHj6yAbfcKVr5GL0WSOQiXNAQC2YTKoTfCY/WOZnCwEHebpM3Mr
6+A8lenj8pFzm8mFAokCMgQQAQoAHRYhBHIDYw4sjnJyUWhP68XOXcLFQs1ZBQJn
4v59AAoJEMXOXcLFQs1ZfdkP90fbXOydWNb1iXwu/vaUjxSx5Nk0Zwkjj7Pi74PZ
Ifd9c0Luf/j7dEHuJOzkOKvkrpTYeQN8Ms9ITVTMeNSOoQn8tnYsxhHqHUcI9ym/
vJ6liIsE2K95gwH2mOQ24ot59pMyQX7sXE4DuQqlrEW1JRemKs7WJB2E/4I7smqG
3+/mqXAoHKkpFBMm1v9tNVx1Tp3ER6C4VFszONlmX0NLB5If+wSits1LnsVgBK1D
Hd3Nxh78YQL0W2xBCOfsHryaKkLW6tp1efTpMHfe4lDEgTFvEWaO3NlPEeD67uxA
KpEG1b/CzBqx+Og1RT+0iZkW9oXYoKJI0Vx0HlYeMrhuE+Q0Fqy/nZYcWhq3y6Tu
GC9J+hyB7D3Z7ne48zNGaAU4x7+6pvkmlBWnNwPH/IlGbexa/F8o+rU0AWrvYCVR
cbbcSsgkq1Dl/Xjbvlx9gwkKnhacbQ5VUOA2Zkp6oBn/pFD1W8xE0X4dNHcM3J5k
CAeZuszjPIZXvwuvAjaOE7ZxKvGDlQTWK8zR5h9tvcwMfVND+EqK0qfDGDMtJqu/
W0lSJj3G/FMO3aGn0Ks6b7cmjbCGjmn7WBGJT2PvH4A4ml1qopWy7Ont6zt1+Cll
y/0Uf75AamJSrAAYFFIJlcpWH/7NRW2L+n1FoIOv5xj5h675uHh/WFlPTNQqrRaP
p1o=
=k1cs
-----END PGP PUBLIC KEY BLOCK-----
"#;
const DEBIAN_BOOKWORM_CERT: &str = r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: B8B8 0B5B 623E AB6A D877 5C45 B7C5 D7D6 3509 47F8
Comment: Debian Archive Automatic Signing Key (12/bookworm) <ftp
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
NlB2T6bKGcYGqL7L
=AKf0
-----END PGP PUBLIC KEY BLOCK-----
"#;
pub(super) struct Certs {
certs: Vec<String>,
}
impl Certs {
pub fn empty() -> Self {
Self { certs: vec![] }
}
pub fn push(&mut self, cert: impl Into<String>) {
self.certs.push(cert.into());
}
pub fn push_for_suite(&mut self, suite: &str) -> Result<(), AptRepoError> {
let cert = match suite {
"trixie" => DEBIAN_TRIXIE_CERT,
"bookworm" => DEBIAN_BOOKWORM_CERT,
_ => return Err(AptRepoError::UnknownRelease(suite.into())),
};
self.push(cert);
Ok(())
}
}
impl Default for Certs {
fn default() -> Self {
Self {
certs: vec![DEBIAN_TRIXIE_CERT.to_string()],
}
}
}
pub(super) struct DebianSuite {
base_url: Url,
in_release: InRelease,
}
impl DebianSuite {
pub fn new(
cached_in_release: &Path,
base_url: Url,
suite: &str,
allowed_certs: &Certs,
) -> Result<Self, AptRepoError> {
let in_release_url = Url::parse(&format!("{}/dists/{suite}/InRelease", base_url))?;
let signed = http_get_to_file(in_release_url.as_str(), cached_in_release)?;
let in_release = inline_verify(&signed, allowed_certs)?;
Ok(Self {
base_url,
in_release: InRelease::new(&in_release)?,
})
}
#[allow(clippy::unwrap_used)]
pub fn packages_file(
&self,
cached_file: &Path,
suite: &str,
arch: Arch,
) -> Result<String, AptRepoError> {
for line in self
.in_release
.sha256
.lines()
.filter(|line| !line.is_empty())
{
let mut words = line.split_ascii_whitespace();
let checksum = words.next().unwrap();
let _length = words.next().unwrap();
let filename = words.next().unwrap();
let wanted = format!("main/binary-{arch}/Packages.gz");
if filename == wanted {
let url = format!("{}/dists/{suite}/{filename}", self.base_url);
let data = http_get_to_file(&url, cached_file)?;
let actual = sha256::digest(&data);
if actual == checksum {
let mut gz = GzDecoder::new(&*data);
let mut packages = vec![];
gz.read_to_end(&mut packages)
.map_err(|source| AptRepoError::Inflate { source })?;
let data = String::from_utf8_lossy(&packages).to_string();
return Ok(data);
} else {
panic!("bad checksum");
}
}
}
Err(AptRepoError::NoPackagesGz)
}
pub fn needed(
&self,
package_names: &[String],
packages: &HashMap<String, Package>,
) -> Vec<Needed> {
let mut wanted = HashMap::new();
let mut todo: HashSet<String> =
HashSet::from_iter(package_names.iter().map(|s| s.to_string()));
while !todo.is_empty() {
let mut new = vec![];
for name in todo.drain() {
if !wanted.contains_key(&name) {
if let Some(p) = packages.get(&name) {
wanted.insert(name.to_string(), p.clone());
for need in p.needs() {
new.push(need);
}
}
}
}
for name in new {
todo.insert(name);
}
}
wanted
.values()
.map(|p| {
let url = format!("{}/{}", self.base_url, p.filename);
Needed::new(p.package.clone(), url)
})
.collect()
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct InRelease {
#[serde(rename = "SHA256")]
sha256: String,
}
impl InRelease {
fn new(s: impl AsRef<str>) -> Result<Self, rfc822_like::de::Error> {
Self::deserialize(Deserializer::new(s.as_ref().as_bytes()))
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub(super) struct Package {
pub package: String,
pre_depends: Option<String>,
depends: Option<String>,
filename: String,
}
impl Package {
pub fn new(s: impl AsRef<str>) -> Result<Self, rfc822_like::de::Error> {
Self::deserialize(Deserializer::new(s.as_ref().as_bytes()))
}
pub fn needs(&self) -> Vec<String> {
let mut needs = vec![];
if let Some(x) = &self.pre_depends {
self.parse_needs(&mut needs, x);
}
if let Some(x) = &self.depends {
self.parse_needs(&mut needs, x);
}
needs
}
#[allow(clippy::unwrap_used)]
fn parse_needs(&self, needs: &mut Vec<String>, x: &str) {
for item in x.split(',') {
let item = item.split_ascii_whitespace().next().unwrap();
needs.push(item.to_string());
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Needed {
package_name: String,
url: String,
}
impl Needed {
fn new(package_name: impl Into<String>, url: impl Into<String>) -> Self {
Self {
package_name: package_name.into(),
url: url.into(),
}
}
pub fn package_name(&self) -> &str {
&self.package_name
}
pub fn url(&self) -> &str {
&self.url
}
}
#[derive(Debug, Copy, Clone)]
pub(super) enum Arch {
Amd64,
}
impl std::fmt::Display for Arch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let arch = match self {
Self::Amd64 => "amd64",
};
write!(f, "{arch}")
}
}
fn inline_verify(data: &[u8], allowed_certs: &Certs) -> Result<String, AptRepoError> {
let mut cmd = Command::new("rsop");
cmd.arg("inline-verify");
let tmp = tempfile::tempdir().map_err(|source| AptRepoError::TempDir { source })?;
for (i, cert) in allowed_certs.certs.iter().enumerate() {
let filename = tmp.path().join(format!("cert-{i}"));
std::fs::write(&filename, cert.as_bytes())
.map_err(|source| AptRepoError::CertFile { source })?;
cmd.arg(filename);
}
let mut runner = CommandRunner::new(cmd);
runner.feed_stdin(data);
runner.capture_stdout();
let output = runner
.execute()
.map_err(|source| AptRepoError::InlineVerify { source })?;
String::from_utf8(output.stdout).map_err(|source| AptRepoError::InReleaseUtf8 { source })
}
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum AptRepoError {
#[error("failed to fetch InRelease file for {suite} from {url}")]
FetchInRelease {
suite: String,
url: Url,
source: reqwest::Error,
},
#[error("failed to retrieve InRelease text from HTTP response")]
InReleaseText { source: reqwest::Error },
#[error(transparent)]
Join(#[from] url::ParseError),
#[error("failed to verify InRelease inline signature")]
InlineVerify { source: CommandError },
#[error("InRelease file is not UTF8")]
InReleaseUtf8 { source: std::string::FromUtf8Error },
#[error("failed to create temporary directory for verifying InRelease signature")]
TempDir { source: std::io::Error },
#[error("failed to write temparary file for verifying InRelease signature")]
CertFile { source: std::io::Error },
#[error("InRelease file doesn't have a single stanza")]
NoStanzas,
#[error("InRelease does not have a SHA256 field")]
NoSha256Field,
#[error("InRelease SHA256 field line lacks {0}")]
Sha256Field(&'static str),
#[error("failed to download Packages.gz file")]
FetchPackagesGz { source: reqwest::Error },
#[error("failed to get Packages.gz file from HTTP request")]
PackagesGz { source: reqwest::Error },
#[error("failed to decompress Packages.gz file")]
Inflate { source: std::io::Error },
#[error("Packages file is not UTF8")]
PackagesUtf8 { source: std::string::FromUtf8Error },
#[error("Packages.gz has the wrong checksum")]
PackagesChecksum,
#[error("failed to find desired Packages.gz in InRelease file")]
NoPackagesGz,
#[error(transparent)]
HttpGet(#[from] crate::action_impl::HttpGetError),
#[error("failed to format timestamp")]
TimeFormat { source: time::error::Format },
#[error("failed to create HTTP client")]
ClientBuild(#[source] reqwest::Error),
#[error("failed to build a reqwest client")]
Client(#[source] reqwest::Error),
#[error("failed to build a reqwest request")]
BuildRequest(#[source] reqwest::Error),
#[error("failed to GET URL {0:?}")]
Get(String, reqwest::Error),
#[error("failed to get body of response from {0:?}")]
GetBody(String, reqwest::Error),
#[error("failure getting file with HTTP GET: status code {0}")]
UnwantedStatus(StatusCode),
#[error("failed to write file {filename}")]
Write {
filename: PathBuf,
source: std::io::Error,
},
#[error("failed to read file {filename}")]
Read {
filename: PathBuf,
source: std::io::Error,
},
#[error("entry for {package} in Packages file lacks Filename field")]
NoFilename { package: String },
#[error(transparent)]
Util(#[from] UtilError),
#[error("failed to parse URL {0:?}")]
UrlParse(String, #[source] url::ParseError),
#[error(transparent)]
Rfc822Like(#[from] rfc822_like::de::Error),
#[error("do not know archive signing key for release {0}")]
UnknownRelease(String),
}
}