From 126a6e0d761fbcd3b24418f35993b7cfb2923f0e Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Thu, 8 Jan 2026 18:15:50 +0100 Subject: [PATCH] deb: ensure universe is enabled on Ubuntu by default Added apt source parser, module apt --- src/apt.rs | 319 +++++++++++++++++++++++++++++++++++++++++++++++ src/deb/local.rs | 14 +++ src/lib.rs | 2 + 3 files changed, 335 insertions(+) create mode 100644 src/apt.rs diff --git a/src/apt.rs b/src/apt.rs new file mode 100644 index 0000000..097b2cc --- /dev/null +++ b/src/apt.rs @@ -0,0 +1,319 @@ +/// APT sources.list management +/// Provides a simple structure for managing APT repository sources +use crate::context; +use std::error::Error; +use std::path::Path; +use std::sync::Arc; + +/// Represents a single source entry in sources.list +#[derive(Debug, Clone)] +pub struct SourceEntry { + /// Is the source enabled? + pub enabled: bool, + /// Source components (universe, main, contrib) + pub components: Vec, + /// Source architectures (amd64, riscv64, arm64) + pub architectures: Vec, + /// Source URI + pub uri: String, + /// Source suite (series-pocket) + pub suite: String, +} + +impl SourceEntry { + /// Parse a string describing a source entry in deb822 format + pub fn from_deb822(data: &str) -> Option { + let mut current_entry = SourceEntry { + enabled: true, + components: Vec::new(), + architectures: Vec::new(), + uri: String::new(), + suite: String::new(), + }; + + for line in data.lines() { + let line = line.trim(); + if line.starts_with('#') { + continue; + } + + // Empty line: end of an entry, or beginning + if line.is_empty() { + if !current_entry.uri.is_empty() { + return Some(current_entry); + } else { + continue; + } + } + + if let Some((key, value)) = line.split_once(':') { + let key = key.trim(); + let value = value.trim(); + + match key { + "Types" => { + // We only care about deb types + } + "URIs" => current_entry.uri = value.to_string(), + "Suites" => current_entry.suite = value.to_string(), + "Components" => { + current_entry.components = + value.split_whitespace().map(|s| s.to_string()).collect(); + } + "Architectures" => { + current_entry.architectures = + value.split_whitespace().map(|s| s.to_string()).collect(); + } + _ => {} + } + } + } + + // End of entry, or empty file? + if !current_entry.uri.is_empty() { + Some(current_entry) + } else { + None + } + } + + /// Parse a line describing a legacy source entry + pub fn from_legacy(data: &str) -> Option { + let line = data.lines().next()?.trim(); + + if line.is_empty() || line.starts_with("#") { + return None; + } + + // Parse legacy deb line format: deb [arch=... / signed_by=] uri suite [components...] + + // Extract bracket parameters first + let mut architectures = Vec::new(); + let mut line_without_brackets = line.to_string(); + + // Find and process bracket parameters + if let Some(start_bracket) = line.find('[') + && let Some(end_bracket) = line.find(']') + { + let bracket_content = &line[start_bracket + 1..end_bracket]; + + // Parse parameters inside brackets + for param in bracket_content.split_whitespace() { + if param.starts_with("arch=") { + let arch_values = param.split('=').nth(1).unwrap_or(""); + architectures = arch_values + .split(',') + .map(|s| s.trim().to_string()) + .collect(); + } + // signed-by parameter is parsed but not stored + } + + // Remove the bracket section from the line + line_without_brackets = line[..start_bracket].to_string() + &line[end_bracket + 1..]; + } + + // Trim and split the remaining line + let line_without_brackets = line_without_brackets.trim(); + let parts: Vec<&str> = line_without_brackets.split_whitespace().collect(); + + // We need at least: deb, uri, suite + if parts.len() < 3 || parts[0] != "deb" { + return None; + } + + let uri = parts[1].to_string(); + let suite = parts[2].to_string(); + let components: Vec = parts[3..].iter().map(|&s| s.to_string()).collect(); + + Some(SourceEntry { + enabled: true, + components, + architectures, + uri, + suite, + }) + } + + /// Convert this source entry to legacy format + pub fn to_legacy(&self) -> String { + let mut result = String::new(); + + // Start with "deb" type + result.push_str("deb"); + + // Add architectures if present + if !self.architectures.is_empty() { + result.push_str(" [arch="); + result.push_str(&self.architectures.join(",")); + result.push(']'); + } + + // Add URI and suite + result.push(' '); + result.push_str(&self.uri); + result.push(' '); + result.push_str(&self.suite); + + // Add components + if !self.components.is_empty() { + result.push(' '); + result.push_str(&self.components.join(" ")); + } + + result + } +} + +/// Parse a 'source list' string in deb822 format into a SourceEntry vector +pub fn parse_deb822(data: &str) -> Vec { + data.split("\n\n") + .flat_map(SourceEntry::from_deb822) + .collect() +} + +/// Parse a 'source list' string in legacy format into a SourceEntry vector +pub fn parse_legacy(data: &str) -> Vec { + data.split("\n") + .flat_map(SourceEntry::from_legacy) + .collect() +} + +/// Load sources from context (or current context by default) +pub fn load(ctx: Option>) -> Result, Box> { + let mut sources = Vec::new(); + let ctx = ctx.unwrap_or_else(context::current); + + // Try DEB822 format first (Ubuntu 24.04+ and Debian Trixie+) + if let Ok(entries) = load_deb822(&ctx, "/etc/apt/sources.list.d/ubuntu.sources") { + sources.extend(entries); + } else if let Ok(entries) = load_deb822(&ctx, "/etc/apt/sources.list.d/debian.sources") { + sources.extend(entries); + } + + // Fall back to legacy format + if let Ok(entries) = load_legacy(&ctx, "/etc/apt/sources.list") { + sources.extend(entries); + } + + Ok(sources) +} + +/// Save sources back to context +pub fn save_legacy( + ctx: Option>, + sources: Vec, + path: &str, +) -> Result<(), Box> { + let ctx = if let Some(c) = ctx { + c + } else { + context::current() + }; + + let content = sources + .into_iter() + .map(|s| s.to_legacy()) + .collect::>() + .join("\n"); + ctx.write_file(Path::new(path), &content)?; + Ok(()) +} + +/// Load sources from DEB822 format +fn load_deb822(ctx: &context::Context, path: &str) -> Result, Box> { + let path = Path::new(path); + if path.exists() { + let content = ctx.read_file(path)?; + return Ok(parse_deb822(&content)); + } + + Ok(Vec::new()) +} + +/// Load sources from legacy format +fn load_legacy(ctx: &context::Context, path: &str) -> Result, Box> { + let path = Path::new(path); + if path.exists() { + let content = ctx.read_file(path)?; + return Ok(content.lines().flat_map(SourceEntry::from_legacy).collect()); + } + + Ok(Vec::new()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_parse_deb822() { + let deb822 = "\ + Types: deb\n\ + URIs: http://fr.archive.ubuntu.com/ubuntu/\n\ + Suites: questing questing-updates questing-backports\n\ + Components: main restricted universe multiverse\n\ + Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\n\ + Architectures: amd64\n\ + \n\ + Types: deb\n\ + URIs: http://security.ubuntu.com/ubuntu/\n\ + Suites: questing-security\n\ + Components: main restricted universe multiverse\n\ + Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\n\ + Architectures: amd64\n\ + \n\ + Types: deb\n\ + URIs: http://ports.ubuntu.com/ubuntu-ports/\n\ + Suites: questing questing-updates questing-backports\n\ + Components: main restricted universe multiverse\n\ + Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\n\ + Architectures: riscv64\n\ + "; + + let sources = parse_deb822(deb822); + assert_eq!(sources.len(), 3); + assert_eq!(sources[0].uri, "http://fr.archive.ubuntu.com/ubuntu/"); + assert_eq!(sources[0].architectures, vec!["amd64"]); + assert_eq!( + sources[0].components, + vec!["main", "restricted", "universe", "multiverse"] + ); + assert_eq!(sources[1].uri, "http://security.ubuntu.com/ubuntu/"); + assert_eq!(sources[1].architectures, vec!["amd64"]); + assert_eq!( + sources[1].components, + vec!["main", "restricted", "universe", "multiverse"] + ); + assert_eq!(sources[2].uri, "http://ports.ubuntu.com/ubuntu-ports/"); + assert_eq!(sources[2].architectures.len(), 1); + assert_eq!(sources[2].architectures, vec!["riscv64"]); + assert_eq!( + sources[2].components, + vec!["main", "restricted", "universe", "multiverse"] + ); + } + + #[tokio::test] + async fn test_parse_legacy() { + let legacy = "\ + deb [signed-by=\"/usr/share/keyrings/ubuntu-archive-keyring.gpg\" arch=amd64] http://archive.ubuntu.com/ubuntu resolute main universe\n\ + deb [arch=amd64,i386 signed-by=\"/usr/share/keyrings/ubuntu-archive-keyring.gpg\"] http://archive.ubuntu.com/ubuntu resolute-updates main\n\ + deb [signed-by=\"/usr/share/keyrings/ubuntu-archive-keyring.gpg\"] http://security.ubuntu.com/ubuntu resolute-security main\n\ + "; + + let sources = parse_legacy(legacy); + assert_eq!(sources.len(), 3); + assert_eq!(sources[0].uri, "http://archive.ubuntu.com/ubuntu"); + assert_eq!(sources[0].suite, "resolute"); + assert_eq!(sources[0].components, vec!["main", "universe"]); + assert_eq!(sources[0].architectures, vec!["amd64"]); + assert_eq!(sources[1].uri, "http://archive.ubuntu.com/ubuntu"); + assert_eq!(sources[1].suite, "resolute-updates"); + assert_eq!(sources[1].components, vec!["main"]); + assert_eq!(sources[1].architectures, vec!["amd64", "i386"]); + assert_eq!(sources[2].uri, "http://security.ubuntu.com/ubuntu"); + assert_eq!(sources[2].suite, "resolute-security"); + assert_eq!(sources[2].components, vec!["main"]); + } +} diff --git a/src/deb/local.rs b/src/deb/local.rs index 290c46d..9e0e7b7 100644 --- a/src/deb/local.rs +++ b/src/deb/local.rs @@ -6,6 +6,7 @@ use std::collections::HashMap; use std::error::Error; use std::path::Path; +use crate::apt; use crate::deb::cross; pub fn build( @@ -29,6 +30,19 @@ pub fn build( cross::ensure_repositories(arch, series)?; } + // UBUNTU: Ensure 'universe' repository is enabled + let mut sources = apt::load(None)?; + let mut modified = false; + for source in &mut sources { + if source.uri.contains("ubuntu") && !source.components.contains(&"universe".to_string()) { + source.components.push("universe".to_string()); + modified = true; + } + } + if modified { + apt::save_legacy(None, sources, "/etc/apt/sources.list")?; + } + // Update package lists log::debug!("Updating package lists for local build..."); let status = ctx diff --git a/src/lib.rs b/src/lib.rs index 738d985..03bdf34 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,8 @@ //! pkh allows working with Debian packages, with multiple actions/submodules #![deny(missing_docs)] +/// Handle apt data (apt sources) +pub mod apt; /// Build a Debian source package (into a .dsc) pub mod build; /// Parse or edit a Debian changelog of a source package