docs: added documentation, enforced documentation
All checks were successful
CI / build (push) Successful in 7m21s

This commit is contained in:
2026-01-01 18:37:40 +01:00
parent 5e1b0988fd
commit b3365afe5b
10 changed files with 113 additions and 29 deletions

View File

@@ -2,6 +2,7 @@ use std::error::Error;
use std::path::Path; use std::path::Path;
use std::process::Command; use std::process::Command;
/// Build a Debian source package (to a .dsc)
pub fn build_source_package(cwd: Option<&Path>) -> Result<(), Box<dyn Error>> { pub fn build_source_package(cwd: Option<&Path>) -> Result<(), Box<dyn Error>> {
let cwd = cwd.unwrap_or_else(|| Path::new(".")); let cwd = cwd.unwrap_or_else(|| Path::new("."));

View File

@@ -5,9 +5,7 @@ use std::fs::File;
use std::io::{self, BufRead, Read, Write}; use std::io::{self, BufRead, Read, Write};
use std::path::Path; use std::path::Path;
/* /// Automatically generate a changelog entry from a commit history and previous changelog
* Automatically generate a changelog entry from a commit history and previous changelog
*/
pub fn generate_entry( pub fn generate_entry(
changelog_file: &str, changelog_file: &str,
cwd: Option<&Path>, cwd: Option<&Path>,
@@ -61,10 +59,8 @@ pub fn generate_entry(
Ok(()) Ok(())
} }
/* /// Compute the next (most probable) version number of a package, from old version and
* Compute the next (most probable) version number of a package, from old version and /// conditions on changes (is ubuntu upload, is a no change rebuild, is a non-maintainer upload)
* conditions on changes (is ubuntu upload, is a no change rebuild, is a non-maintainer upload)
*/
fn compute_new_version( fn compute_new_version(
old_version: &str, old_version: &str,
is_ubuntu: bool, is_ubuntu: bool,
@@ -87,9 +83,7 @@ fn compute_new_version(
increment_suffix(old_version, "") increment_suffix(old_version, "")
} }
/* /// Increment a version number by 1, for a given suffix
* Increment a version number by 1, for a given suffix
*/
fn increment_suffix(version: &str, suffix: &str) -> String { fn increment_suffix(version: &str, suffix: &str) -> String {
// If suffix is empty, we just look for trailing digits // If suffix is empty, we just look for trailing digits
// If suffix is not empty, we look for suffix followed by digits // If suffix is not empty, we look for suffix followed by digits
@@ -120,9 +114,7 @@ fn increment_suffix(version: &str, suffix: &str) -> String {
} }
} }
/* /// Parse a changelog file first entry header, to obtain (package, version, series)
* Parse a changelog file first entry header, to obtain (package, version, series)
*/
pub fn parse_changelog_header( pub fn parse_changelog_header(
path: &Path, path: &Path,
) -> Result<(String, String, String), Box<dyn std::error::Error>> { ) -> Result<(String, String, String), Box<dyn std::error::Error>> {

View File

@@ -10,6 +10,7 @@ use super::schroot::SchrootDriver;
use super::ssh::SshDriver; use super::ssh::SshDriver;
use super::unshare::UnshareDriver; use super::unshare::UnshareDriver;
/// A ContextDriver is the interface for the logic happening inside a context
pub trait ContextDriver { pub trait ContextDriver {
fn ensure_available(&self, src: &Path, dest_root: &str) -> io::Result<PathBuf>; fn ensure_available(&self, src: &Path, dest_root: &str) -> io::Result<PathBuf>;
fn retrieve_path(&self, src: &Path, dest: &Path) -> io::Result<()>; fn retrieve_path(&self, src: &Path, dest: &Path) -> io::Result<()>;
@@ -41,34 +42,52 @@ pub trait ContextDriver {
#[serde(tag = "type")] #[serde(tag = "type")]
#[derive(Default)] #[derive(Default)]
pub enum ContextConfig { pub enum ContextConfig {
/// Local context: actions executed locally
#[serde(rename = "local")] #[serde(rename = "local")]
#[default] #[default]
Local, Local,
/// SSH context: actions over an SSH connection
#[serde(rename = "ssh")] #[serde(rename = "ssh")]
Ssh { Ssh {
/// Host for the SSH connection
host: String, host: String,
/// User for the SSH connection
user: Option<String>, user: Option<String>,
/// TCP port for the SSH connection
port: Option<u16>, port: Option<u16>,
}, },
/// Schroot context: using `schroot`
#[serde(rename = "schroot")] #[serde(rename = "schroot")]
Schroot { Schroot {
/// Name of the schroot
name: String, name: String,
/// Optional parent context for the Schroot context
parent: Option<String>, parent: Option<String>,
}, },
/// Unshare context: chroot with dropped permissions (using `unshare`)
#[serde(rename = "unshare")] #[serde(rename = "unshare")]
Unshare { Unshare {
/// Path to use for chrooting
path: String, path: String,
/// Optional parent context for the Unshare context
parent: Option<String>, parent: Option<String>,
}, },
} }
/// A context, allowing to run commands, read and write files, etc
pub struct Context { pub struct Context {
/// Configuration for the context
pub config: ContextConfig, pub config: ContextConfig,
/// Parent context for the context
///
/// For example, you could have a chroot context over an ssh connection
pub parent: Option<Arc<Context>>, pub parent: Option<Arc<Context>>,
/// ContextDriver for the context, implementing the logic for actions
driver: Mutex<Option<Box<dyn ContextDriver + Send + Sync>>>, driver: Mutex<Option<Box<dyn ContextDriver + Send + Sync>>>,
} }
impl Context { impl Context {
/// Create a context from configuration
pub fn new(config: ContextConfig) -> Self { pub fn new(config: ContextConfig) -> Self {
let parent = match &config { let parent = match &config {
ContextConfig::Schroot { ContextConfig::Schroot {
@@ -97,6 +116,7 @@ impl Context {
} }
} }
/// Create a context with an explicit parent context
pub fn with_parent(config: ContextConfig, parent: Arc<Context>) -> Self { pub fn with_parent(config: ContextConfig, parent: Arc<Context>) -> Self {
Self { Self {
config, config,
@@ -105,6 +125,7 @@ impl Context {
} }
} }
/// Make a command inside context
pub fn command<S: AsRef<OsStr>>(&self, program: S) -> ContextCommand<'_> { pub fn command<S: AsRef<OsStr>>(&self, program: S) -> ContextCommand<'_> {
ContextCommand { ContextCommand {
context: self, context: self,
@@ -126,6 +147,7 @@ impl Context {
.ensure_available(src, dest_root) .ensure_available(src, dest_root)
} }
/// Create a temp directory inside context
pub fn create_temp_dir(&self) -> io::Result<String> { pub fn create_temp_dir(&self) -> io::Result<String> {
self.driver().as_ref().unwrap().create_temp_dir() self.driver().as_ref().unwrap().create_temp_dir()
} }
@@ -143,18 +165,22 @@ impl Context {
self.driver().as_ref().unwrap().list_files(path) self.driver().as_ref().unwrap().list_files(path)
} }
/// Copy a path inside context
pub fn copy_path(&self, src: &Path, dest: &Path) -> io::Result<()> { pub fn copy_path(&self, src: &Path, dest: &Path) -> io::Result<()> {
self.driver().as_ref().unwrap().copy_path(src, dest) self.driver().as_ref().unwrap().copy_path(src, dest)
} }
/// Read a file inside context
pub fn read_file(&self, path: &Path) -> io::Result<String> { pub fn read_file(&self, path: &Path) -> io::Result<String> {
self.driver().as_ref().unwrap().read_file(path) self.driver().as_ref().unwrap().read_file(path)
} }
/// Write a file inside context
pub fn write_file(&self, path: &Path, content: &str) -> io::Result<()> { pub fn write_file(&self, path: &Path, content: &str) -> io::Result<()> {
self.driver().as_ref().unwrap().write_file(path, content) self.driver().as_ref().unwrap().write_file(path, content)
} }
/// Create and obtain a specific driver for the context
pub fn driver( pub fn driver(
&self, &self,
) -> std::sync::MutexGuard<'_, Option<Box<dyn ContextDriver + Send + Sync>>> { ) -> std::sync::MutexGuard<'_, Option<Box<dyn ContextDriver + Send + Sync>>> {
@@ -182,6 +208,7 @@ impl Context {
driver_lock driver_lock
} }
/// Clone a context
pub fn clone_raw(&self) -> Self { pub fn clone_raw(&self) -> Self {
Self { Self {
config: self.config.clone(), config: self.config.clone(),
@@ -207,12 +234,13 @@ pub struct ContextCommand<'a> {
} }
impl<'a> ContextCommand<'a> { impl<'a> ContextCommand<'a> {
/// Add an argument to current command
pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self { pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
self.args.push(arg.as_ref().to_string_lossy().to_string()); self.args.push(arg.as_ref().to_string_lossy().to_string());
self self
} }
// Support chaining args /// Add multiple command arguments
pub fn args<I, S>(&mut self, args: I) -> &mut Self pub fn args<I, S>(&mut self, args: I) -> &mut Self
where where
I: IntoIterator<Item = S>, I: IntoIterator<Item = S>,
@@ -224,6 +252,7 @@ impl<'a> ContextCommand<'a> {
self self
} }
/// Set environment variable for command
pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Self pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Self
where where
K: AsRef<OsStr>, K: AsRef<OsStr>,
@@ -236,6 +265,7 @@ impl<'a> ContextCommand<'a> {
self self
} }
/// Set multiple environment variables for command
pub fn envs<I, K, V>(&mut self, vars: I) -> &mut Self pub fn envs<I, K, V>(&mut self, vars: I) -> &mut Self
where where
I: IntoIterator<Item = (K, V)>, I: IntoIterator<Item = (K, V)>,
@@ -248,11 +278,13 @@ impl<'a> ContextCommand<'a> {
self self
} }
/// Set current working directory for command
pub fn current_dir<P: AsRef<OsStr>>(&mut self, dir: P) -> &mut Self { pub fn current_dir<P: AsRef<OsStr>>(&mut self, dir: P) -> &mut Self {
self.cwd = Some(dir.as_ref().to_string_lossy().to_string()); self.cwd = Some(dir.as_ref().to_string_lossy().to_string());
self self
} }
/// Run command and obtain exit status
pub fn status(&mut self) -> io::Result<std::process::ExitStatus> { pub fn status(&mut self) -> io::Result<std::process::ExitStatus> {
self.context.driver().as_ref().unwrap().run( self.context.driver().as_ref().unwrap().run(
&self.program, &self.program,
@@ -262,7 +294,7 @@ impl<'a> ContextCommand<'a> {
) )
} }
// Capture output /// Run command, capturing output
pub fn output(&mut self) -> io::Result<std::process::Output> { pub fn output(&mut self) -> io::Result<std::process::Output> {
self.context.driver().as_ref().unwrap().run_output( self.context.driver().as_ref().unwrap().run_output(
&self.program, &self.program,

View File

@@ -26,6 +26,7 @@ impl Default for Config {
} }
} }
/// Helper managing contexts
pub struct ContextManager { pub struct ContextManager {
context: RwLock<Arc<Context>>, context: RwLock<Arc<Context>>,
config_path: PathBuf, config_path: PathBuf,
@@ -67,10 +68,12 @@ impl ContextManager {
}) })
} }
/// Obtain current ContextManager configuration
pub fn get_config(&self) -> std::sync::RwLockReadGuard<'_, Config> { pub fn get_config(&self) -> std::sync::RwLockReadGuard<'_, Config> {
self.config.read().unwrap() self.config.read().unwrap()
} }
/// Make a ContextManager using a specific configuration path
pub fn with_path(path: PathBuf) -> Self { pub fn with_path(path: PathBuf) -> Self {
let config = Config::default(); let config = Config::default();
Self { Self {
@@ -80,6 +83,7 @@ impl ContextManager {
} }
} }
/// Save current context configuration to disk
pub fn save(&self) -> io::Result<()> { pub fn save(&self) -> io::Result<()> {
let config = self.config.read().unwrap(); let config = self.config.read().unwrap();
let content = serde_json::to_string_pretty(&*config) let content = serde_json::to_string_pretty(&*config)
@@ -97,6 +101,7 @@ impl ContextManager {
Context::new(context_config) Context::new(context_config)
} }
/// List contexts from configuration
pub fn list_contexts(&self) -> Vec<String> { pub fn list_contexts(&self) -> Vec<String> {
self.config self.config
.read() .read()
@@ -107,6 +112,7 @@ impl ContextManager {
.collect() .collect()
} }
/// Add a context to configuration
pub fn add_context(&self, name: &str, config: ContextConfig) -> io::Result<()> { pub fn add_context(&self, name: &str, config: ContextConfig) -> io::Result<()> {
self.config self.config
.write() .write()
@@ -116,6 +122,7 @@ impl ContextManager {
self.save() self.save()
} }
/// Remove context from configuration
pub fn remove_context(&self, name: &str) -> io::Result<()> { pub fn remove_context(&self, name: &str) -> io::Result<()> {
let mut config = self.config.write().unwrap(); let mut config = self.config.write().unwrap();
if name == "local" { if name == "local" {
@@ -137,6 +144,7 @@ impl ContextManager {
Ok(()) Ok(())
} }
/// Set current context from name (modifying configuration)
pub fn set_current(&self, name: &str) -> io::Result<()> { pub fn set_current(&self, name: &str) -> io::Result<()> {
let mut config = self.config.write().unwrap(); let mut config = self.config.write().unwrap();
if config.contexts.contains_key(name) { if config.contexts.contains_key(name) {
@@ -153,14 +161,18 @@ impl ContextManager {
} }
} }
/// Set current context, without modifying configuration
pub fn set_current_ephemeral(&self, context: Context) { pub fn set_current_ephemeral(&self, context: Context) {
*self.context.write().unwrap() = context.into(); *self.context.write().unwrap() = context.into();
} }
/// Obtain current context handle
pub fn current(&self) -> Arc<Context> { pub fn current(&self) -> Arc<Context> {
self.context.read().unwrap().clone() self.context.read().unwrap().clone()
} }
/// Obtain current context name
/// Will not work for ephemeral context (obtained from config)
pub fn current_name(&self) -> String { pub fn current_name(&self) -> String {
self.config.read().unwrap().context.clone() self.config.read().unwrap().context.clone()
} }

View File

@@ -9,10 +9,12 @@ pub use api::{Context, ContextCommand, ContextConfig};
pub use manager::ContextManager; pub use manager::ContextManager;
use std::sync::Arc; use std::sync::Arc;
/// Obtain global context manager
pub fn manager() -> &'static ContextManager { pub fn manager() -> &'static ContextManager {
&manager::MANAGER &manager::MANAGER
} }
/// Obtain current context
pub fn current() -> Arc<Context> { pub fn current() -> Arc<Context> {
manager::MANAGER.current() manager::MANAGER.current()
} }

View File

@@ -6,12 +6,16 @@ use crate::context;
use std::error::Error; use std::error::Error;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
/// Build mode for the binary build
#[derive(PartialEq)] #[derive(PartialEq)]
pub enum BuildMode { pub enum BuildMode {
/// Use `sbuild` for the build, configured in unshare mode
Sbuild, Sbuild,
/// Local build, directly on the context
Local, Local,
} }
/// Build package in 'cwd' to a .deb
pub fn build_binary_package( pub fn build_binary_package(
arch: Option<&str>, arch: Option<&str>,
series: Option<&str>, series: Option<&str>,

View File

@@ -1,10 +1,27 @@
//! pkh: Debian packaging helper
//!
//! pkh allows working with Debian packages, with multiple actions/submodules
#![deny(missing_docs)]
/// Build a Debian source package (into a .dsc)
pub mod build; pub mod build;
/// Parse or edit a Debian changelog of a source package
pub mod changelog; pub mod changelog;
pub mod context; /// Build a Debian package into a binary (.deb)
pub mod deb; pub mod deb;
/// Obtain information about one or multiple packages
pub mod package_info; pub mod package_info;
/// Download a source package locally
pub mod pull; pub mod pull;
/// Handle context for .deb building: locally, over ssh, in a chroot...
pub mod context;
/// Optional callback function (taking 4 arguments)
/// - Name of the current main operation (e.g. pulling package)
/// - Name of the current nested operation (e.g. cloning git repo)
/// - Progress, position, index of current operation (e.g. amount of data downloaded)
/// - Total amount for current operation (e.g. size of the file to download)
pub type ProgressCallback<'a> = Option<&'a dyn Fn(&str, &str, usize, usize)>; pub type ProgressCallback<'a> = Option<&'a dyn Fn(&str, &str, usize, usize)>;
/// Returns the architecture of current CPU, debian-compatible /// Returns the architecture of current CPU, debian-compatible

View File

@@ -103,9 +103,9 @@ fn main() {
.map(|s| s.as_str()) .map(|s| s.as_str())
.unwrap_or(""); .unwrap_or("");
// Since pull is async, we need to block on it
let (pb, progress_callback) = ui::create_progress_bar(&multi); let (pb, progress_callback) = ui::create_progress_bar(&multi);
// Since pull is async, we need to block on it
if let Err(e) = rt.block_on(pull( if let Err(e) = rt.block_on(pull(
package, package,
version, version,

View File

@@ -93,6 +93,7 @@ fn get_series_from_file(path: &str) -> Result<Vec<String>, Box<dyn Error>> {
parse_series_csv(&content) parse_series_csv(&content)
} }
/// Obtain a list of series from a distribution
pub async fn get_dist_series(dist: &str) -> Result<Vec<String>, Box<dyn Error>> { pub async fn get_dist_series(dist: &str) -> Result<Vec<String>, Box<dyn Error>> {
if Path::new(format!("/usr/share/distro-info/{dist}.csv").as_str()).exists() { if Path::new(format!("/usr/share/distro-info/{dist}.csv").as_str()).exists() {
get_series_from_file(format!("/usr/share/distro-info/{dist}.csv").as_str()) get_series_from_file(format!("/usr/share/distro-info/{dist}.csv").as_str())
@@ -105,6 +106,7 @@ pub async fn get_dist_series(dist: &str) -> Result<Vec<String>, Box<dyn Error>>
} }
} }
/// Obtain the distribution (eg. debian, ubuntu) from a distribution series (eg. noble, bookworm)
pub async fn get_dist_from_series(series: &str) -> Result<String, Box<dyn Error>> { pub async fn get_dist_from_series(series: &str) -> Result<String, Box<dyn Error>> {
let debian_series = get_dist_series("debian").await?; let debian_series = get_dist_series("debian").await?;
if debian_series.contains(&series.to_string()) { if debian_series.contains(&series.to_string()) {
@@ -117,34 +119,55 @@ pub async fn get_dist_from_series(series: &str) -> Result<String, Box<dyn Error>
Err(format!("Unknown series: {}", series).into()) Err(format!("Unknown series: {}", series).into())
} }
/// A File used in a source package
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct FileEntry { pub struct FileEntry {
/// Name of the file
pub name: String, pub name: String,
/// Size of the file
pub size: u64, pub size: u64,
/// SHA256 hash for the file
pub sha256: String, pub sha256: String,
} }
/// A package 'stanza' as found is 'Sources.gz' files, containing basic information about a source package
#[derive(Debug)] #[derive(Debug)]
pub struct PackageStanza { pub struct PackageStanza {
/// Name of the package
pub package: String, pub package: String,
/// Version number for the package
pub version: String, pub version: String,
/// Directory field in the stanza
pub directory: String, pub directory: String,
/// Source package format (e.g. '3.0 (quilt)')
pub format: String, pub format: String,
/// Vcs-Git field in the stanza
pub vcs_git: Option<String>, pub vcs_git: Option<String>,
/// Vcs-Browser field in the stanza
pub vcs_browser: Option<String>, pub vcs_browser: Option<String>,
/// Files present in the source package
pub files: Vec<FileEntry>, pub files: Vec<FileEntry>,
} }
/// Source package information
#[derive(Debug)] #[derive(Debug)]
pub struct PackageInfo { pub struct PackageInfo {
pub dist: String, /// Source 'stanza' for the package, containing basic information
pub series: String,
pub stanza: PackageStanza, pub stanza: PackageStanza,
/// Distribution for the package
pub dist: String,
/// Distribution series for the package
pub series: String,
/// Preferred VCS for the source package
///
/// Should be Launchpad on Ubuntu, and Salsa on Debian
pub preferred_vcs: Option<String>, pub preferred_vcs: Option<String>,
/// URL for the files of the source package
pub archive_url: String, pub archive_url: String,
} }
impl PackageInfo { impl PackageInfo {
/// Returns true if the package is a Debian native package (no orig)
pub fn is_native(&self) -> bool { pub fn is_native(&self) -> bool {
self.stanza.format.contains("(native)") self.stanza.format.contains("(native)")
} }
@@ -175,9 +198,7 @@ fn get_base_url(dist: &str) -> &str {
} }
} }
/* /// Obtain the URL for the 'Release' file of a distribution series
* Obtain the URL for the 'Release' file of a distribution series
*/
fn get_release_url(base_url: &str, series: &str, pocket: &str) -> String { fn get_release_url(base_url: &str, series: &str, pocket: &str) -> String {
let pocket_full = if pocket.is_empty() { let pocket_full = if pocket.is_empty() {
String::new() String::new()
@@ -187,9 +208,7 @@ fn get_release_url(base_url: &str, series: &str, pocket: &str) -> String {
format!("{base_url}/dists/{series}{pocket_full}/Release") format!("{base_url}/dists/{series}{pocket_full}/Release")
} }
/* /// Obtain the components of a distribution series by parsing the 'Release' file
* Obtain the components of a distribution series by parsing the 'Release' file
*/
async fn get_components( async fn get_components(
base_url: &str, base_url: &str,
series: &str, series: &str,
@@ -296,10 +315,8 @@ impl Iterator for DebianSources {
} }
} }
/* /// Parse a 'Sources.gz' debian package file data, to look for a target package and
* Parse a 'Sources.gz' debian package file data, to look for a target package and /// return the data for that package stanza
* return the data for that package stanza
*/
fn parse_sources( fn parse_sources(
data: &[u8], data: &[u8],
target_package: &str, target_package: &str,
@@ -314,6 +331,7 @@ fn parse_sources(
})) }))
} }
/// Get package information from a package, distribution series, and pocket
pub async fn get( pub async fn get(
package_name: &str, package_name: &str,
series: &str, series: &str,
@@ -387,6 +405,7 @@ pub async fn get(
.into()) .into())
} }
/// Try to find package information in a distribution, trying all series and pockets
pub async fn find_package( pub async fn find_package(
package_name: &str, package_name: &str,
dist: &str, dist: &str,

View File

@@ -333,6 +333,11 @@ async fn fetch_archive_sources(
Ok(()) Ok(())
} }
/// Pull a source package locally
///
/// Will try to find the package information, and use it to download it over prefered way
/// (either git or direct archive download), as well as orig tarball, inside 'package' directory
/// The source will be extracted under 'package/package'
pub async fn pull( pub async fn pull(
package: &str, package: &str,
_version: &str, _version: &str,