use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct NativeDepsContract {
#[serde(default)]
pub apt_repos: Vec<AptRepoContract>,
#[serde(default)]
pub apt_packages: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AptRepoContract {
pub key_url: String,
pub keyring: String,
pub url: String,
#[serde(default)]
pub codename: String,
pub packages: Vec<String>,
}
fn confluent_repo(codename: &str) -> AptRepoContract {
AptRepoContract {
key_url: "https://packages.confluent.io/clients/deb/archive.key".into(),
keyring: "/usr/share/keyrings/confluent-clients.gpg".into(),
url: "https://packages.confluent.io/clients/deb".into(),
codename: codename.into(),
packages: vec!["librdkafka1".into()],
}
}
fn codename_from_base_image(base_image: &str) -> &'static str {
if base_image.contains("bookworm") {
"bookworm"
} else if base_image.contains("jammy") {
"jammy"
} else if base_image.contains("focal") {
"focal"
} else {
"noble"
}
}
impl NativeDepsContract {
#[must_use]
pub fn for_rustlib_features(features: &[&str], base_image: &str) -> Self {
let codename = codename_from_base_image(base_image);
let mut apt_repos = Vec::new();
let mut packages: Vec<String> = Vec::new();
let mut seen = std::collections::HashSet::new();
let mut add = |pkg: &str| {
if seen.insert(pkg.to_string()) {
packages.push(pkg.into());
}
};
let needs_kafka = features
.iter()
.any(|f| *f == "transport-kafka" || f.starts_with("dlq-kafka"));
if needs_kafka {
apt_repos.push(confluent_repo(codename));
add("libssl3");
add("zlib1g");
}
let needs_zstd = features
.iter()
.any(|f| *f == "spool" || *f == "tiered-sink");
if needs_zstd {
add("libzstd1");
}
let needs_ssl = features.iter().any(|f| {
*f == "http"
|| f.starts_with("secrets")
|| f.starts_with("transport")
|| *f == "config-postgres"
|| f.starts_with("otel")
});
if needs_ssl {
add("libssl3");
add("zlib1g");
}
let needs_git2 = features.contains(&"directory-config-git");
if needs_git2 {
add("libgit2-1.7");
}
Self {
apt_repos,
apt_packages: packages,
}
}
#[must_use]
pub fn from_cargo_toml(cargo_toml_path: &std::path::Path, base_image: &str) -> Self {
let Ok(content) = std::fs::read_to_string(cargo_toml_path) else {
return Self::default();
};
let features = extract_rustlib_features(&content);
if features.is_empty() {
return Self::default();
}
let feature_refs: Vec<&str> = features.iter().map(String::as_str).collect();
Self::for_rustlib_features(&feature_refs, base_image)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.apt_repos.is_empty() && self.apt_packages.is_empty()
}
}
fn extract_rustlib_features(content: &str) -> Vec<String> {
let mut in_rustlib = false;
let mut features = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("hyperi-rustlib")
&& trimmed.contains("features")
&& let Some(start) = trimmed.find("features = [")
{
let after = &trimmed[start + 12..];
if let Some(end) = after.find(']') {
let feature_str = &after[..end];
for feat in feature_str.split(',') {
let f = feat.trim().trim_matches('"').trim();
if !f.is_empty() {
features.push(f.to_string());
}
}
return features;
}
}
if trimmed.starts_with("hyperi-rustlib") {
in_rustlib = true;
continue;
}
if in_rustlib {
if trimmed.starts_with(']') {
return features;
}
if trimmed.starts_with('"') {
let f = trimmed.trim_matches('"').trim_end_matches(',').trim();
if !f.is_empty() {
features.push(f.to_string());
}
}
if trimmed.starts_with('[') && !trimmed.starts_with("[dependencies") {
return features;
}
}
}
features
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_kafka_features_add_confluent_repo() {
let deps = NativeDepsContract::for_rustlib_features(&["transport-kafka"], "ubuntu:24.04");
assert_eq!(deps.apt_repos.len(), 1);
assert!(deps.apt_repos[0].url.contains("confluent"));
assert!(deps.apt_repos[0].packages.contains(&"librdkafka1".into()));
assert_eq!(deps.apt_repos[0].codename, "noble");
assert!(deps.apt_packages.contains(&"libssl3".into()));
assert!(deps.apt_packages.contains(&"zlib1g".into()));
}
#[test]
fn test_spool_adds_zstd() {
let deps = NativeDepsContract::for_rustlib_features(&["spool"], "ubuntu:24.04");
assert!(deps.apt_packages.contains(&"libzstd1".into()));
}
#[test]
fn test_tiered_sink_adds_zstd() {
let deps = NativeDepsContract::for_rustlib_features(&["tiered-sink"], "ubuntu:24.04");
assert!(deps.apt_packages.contains(&"libzstd1".into()));
}
#[test]
fn test_no_features_empty() {
let deps = NativeDepsContract::for_rustlib_features(&[], "ubuntu:24.04");
assert!(deps.is_empty());
}
#[test]
fn test_pure_rust_features_empty() {
let deps = NativeDepsContract::for_rustlib_features(
&["cli", "deployment", "logger"],
"ubuntu:24.04",
);
assert!(deps.is_empty());
}
#[test]
fn test_bookworm_codename() {
let deps =
NativeDepsContract::for_rustlib_features(&["transport-kafka"], "debian:bookworm-slim");
assert_eq!(deps.apt_repos[0].codename, "bookworm");
}
#[test]
fn test_no_duplicate_packages() {
let deps = NativeDepsContract::for_rustlib_features(
&["transport-kafka", "http", "secrets"],
"ubuntu:24.04",
);
let ssl_count = deps.apt_packages.iter().filter(|p| *p == "libssl3").count();
assert_eq!(ssl_count, 1);
}
#[test]
fn test_dlq_kafka_adds_confluent() {
let deps = NativeDepsContract::for_rustlib_features(&["dlq-kafka"], "ubuntu:24.04");
assert_eq!(deps.apt_repos.len(), 1);
}
#[test]
fn test_git2_feature() {
let deps =
NativeDepsContract::for_rustlib_features(&["directory-config-git"], "ubuntu:24.04");
assert!(deps.apt_packages.contains(&"libgit2-1.7".into()));
}
#[test]
fn test_full_receiver_features() {
let deps = NativeDepsContract::for_rustlib_features(
&[
"config",
"config-reload",
"logger",
"metrics",
"http-server",
"transport-kafka",
"transport-grpc",
"dlq-kafka",
"spool",
"tiered-sink",
"runtime",
"secrets",
"scaling",
"cli",
"deployment",
],
"ubuntu:24.04",
);
assert_eq!(deps.apt_repos.len(), 1); assert!(!deps.apt_packages.contains(&"librdkafka1".to_string())); assert!(deps.apt_repos[0].packages.contains(&"librdkafka1".into()));
assert!(deps.apt_packages.contains(&"libssl3".into()));
assert!(deps.apt_packages.contains(&"libzstd1".into()));
assert!(deps.apt_packages.contains(&"zlib1g".into()));
}
}