context: add context
All checks were successful
CI / build (push) Successful in 1m50s

pkh context allows to manage contexts (local, ssh) and run contextualized commands (deb)

in other words, this allows building binary packages over ssh
This commit is contained in:
2025-12-15 20:48:44 +01:00
parent ad98d9c1ab
commit 1d65d1ce31
9 changed files with 789 additions and 12 deletions

113
src/context/api.rs Normal file
View File

@@ -0,0 +1,113 @@
use serde::{Deserialize, Serialize};
use std::ffi::OsStr;
use std::io;
use std::path::{Path, PathBuf};
use super::local::LocalDriver;
use super::ssh::SshDriver;
/// Internal trait defining the strategy for executing a command.
///
/// This allows to have different execution behaviors (Local, SSH, ...) while keeping the
/// `ContextCommand` API consistent.
pub trait CommandRunner {
fn add_arg(&mut self, arg: String);
fn status(&mut self) -> io::Result<std::process::ExitStatus>;
fn output(&mut self) -> io::Result<std::process::Output>;
}
pub trait ContextDriver {
fn ensure_available(&self, src: &Path, dest_root: &str) -> io::Result<PathBuf>;
fn create_runner(&self, program: String) -> Box<dyn CommandRunner>;
fn prepare_work_dir(&self) -> io::Result<String>;
}
/// Represents an execution environment (Local or via SSH).
///
/// This is the data model that configuration files store. It defines *where* operations happen.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type")]
#[derive(Default)]
pub enum Context {
#[serde(rename = "local")]
#[default]
Local,
#[serde(rename = "ssh")]
Ssh {
host: String,
user: Option<String>,
port: Option<u16>,
},
}
impl Context {
pub fn command<S: AsRef<OsStr>>(&self, program: S) -> ContextCommand {
let runner = self
.driver()
.create_runner(program.as_ref().to_string_lossy().to_string());
ContextCommand { runner }
}
/// Ensures that the source file/directory exists at the destination context.
///
/// If Local: Returns the absolute path of `src`.
/// If Remote: Copies `src` to `dest_root` on the remote and returns the path to the copied entity.
pub fn ensure_available(&self, src: &Path, dest_root: &str) -> io::Result<PathBuf> {
self.driver().ensure_available(src, dest_root)
}
pub fn prepare_work_dir(&self) -> io::Result<String> {
self.driver().prepare_work_dir()
}
fn driver(&self) -> Box<dyn ContextDriver> {
match self {
Context::Local => Box::new(LocalDriver),
Context::Ssh { host, user, port } => Box::new(SshDriver {
host: host.clone(),
user: user.clone(),
port: *port,
}),
}
}
}
/// A builder and executor for commands within a specific `Context`.
///
/// This struct acts as a high-level facade (API) for command creation. It hides the implementation
/// details of *how* the command is actually executed, allowing the user to simple chain arguments
/// and call `status()` or `output()`.
///
/// It delegates the actual work to a `CommandRunner`.
pub struct ContextCommand {
runner: Box<dyn CommandRunner>,
}
impl ContextCommand {
pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
self.runner
.add_arg(arg.as_ref().to_string_lossy().to_string());
self
}
// Support chaining args
pub fn args<I, S>(&mut self, args: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
for arg in args {
self.arg(arg);
}
self
}
pub fn status(&mut self) -> io::Result<std::process::ExitStatus> {
self.runner.status()
}
// Capture output
pub fn output(&mut self) -> io::Result<std::process::Output> {
self.runner.output()
}
}