Compare commits

1 Commits

Author SHA1 Message Date
8e9e19a6ca exp: cross 2025-12-17 17:27:27 +01:00
8 changed files with 330 additions and 54 deletions

View File

@@ -1,16 +1,18 @@
use serde::{Deserialize, Serialize};
use std::cell::{Ref, RefCell};
use std::ffi::OsStr;
use std::io;
use std::path::{Path, PathBuf};
use super::local::LocalDriver;
use super::ssh::SshDriver;
use super::schroot::SchrootDriver;
pub trait ContextDriver {
fn ensure_available(&self, src: &Path, dest_root: &str) -> io::Result<PathBuf>;
fn retrieve_path(&self, src: &Path, dest: &Path) -> io::Result<()>;
fn list_files(&self, path: &Path) -> io::Result<Vec<PathBuf>>;
fn run(&self, program: &str, args: &[String]) -> io::Result<std::process::ExitStatus>;
fn run_output(&self, program: &str, args: &[String]) -> io::Result<std::process::Output>;
fn run(&self, program: &str, args: &[String], env: &[(String, String)]) -> io::Result<std::process::ExitStatus>;
fn run_output(&self, program: &str, args: &[String], env: &[(String, String)]) -> io::Result<std::process::Output>;
fn prepare_work_dir(&self) -> io::Result<String>;
}
@@ -20,7 +22,7 @@ pub trait ContextDriver {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type")]
#[derive(Default)]
pub enum Context {
pub enum ContextConfig {
#[serde(rename = "local")]
#[default]
Local,
@@ -30,14 +32,31 @@ pub enum Context {
user: Option<String>,
port: Option<u16>,
},
#[serde(rename = "schroot")]
Schroot {
name: String,
}
}
pub struct Context {
config: ContextConfig,
driver: RefCell<Option<Box<dyn ContextDriver>>>,
}
impl Context {
pub fn command<S: AsRef<OsStr>>(&self, program: S) -> ContextCommand {
pub fn new(config: ContextConfig) -> Self {
Self {
config,
driver: RefCell::new(None),
}
}
pub fn command<S: AsRef<OsStr>>(&self, program: S) -> ContextCommand<'_> {
ContextCommand {
driver: self.driver(),
context: self,
program: program.as_ref().to_string_lossy().to_string(),
args: Vec::new(),
env: Vec::new(),
}
}
@@ -66,15 +85,23 @@ impl Context {
self.driver().list_files(path)
}
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,
}),
fn driver(&self) -> Ref<Box<dyn ContextDriver>> {
if self.driver.borrow().is_none() {
let driver: Box<dyn ContextDriver> = match &self.config {
ContextConfig::Local => Box::new(LocalDriver),
ContextConfig::Ssh { host, user, port } => Box::new(SshDriver {
host: host.clone(),
user: user.clone(),
port: *port,
}),
ContextConfig::Schroot { name } => Box::new(SchrootDriver {
name: name.clone(),
session: RefCell::new(None),
}),
};
*self.driver.borrow_mut() = Some(driver);
}
Ref::map(self.driver.borrow(), |opt| opt.as_ref().unwrap())
}
}
@@ -85,13 +112,14 @@ impl Context {
/// and call `status()` or `output()`.
///
/// It delegates the actual work to a `ContextDriver`.
pub struct ContextCommand {
driver: Box<dyn ContextDriver>,
pub struct ContextCommand<'a> {
context: &'a Context,
program: String,
args: Vec<String>,
env: Vec<(String, String)>,
}
impl ContextCommand {
impl<'a> ContextCommand<'a> {
pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
self.args.push(arg.as_ref().to_string_lossy().to_string());
self
@@ -109,12 +137,36 @@ impl ContextCommand {
self
}
pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Self
where
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.env.push((
key.as_ref().to_string_lossy().to_string(),
val.as_ref().to_string_lossy().to_string(),
));
self
}
pub fn envs<I, K, V>(&mut self, vars: I) -> &mut Self
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
for (key, val) in vars {
self.env(key, val);
}
self
}
pub fn status(&mut self) -> io::Result<std::process::ExitStatus> {
self.driver.run(&self.program, &self.args)
self.context.driver().run(&self.program, &self.args, &self.env)
}
// Capture output
pub fn output(&mut self) -> io::Result<std::process::Output> {
self.driver.run_output(&self.program, &self.args)
self.context.driver().run_output(&self.program, &self.args, &self.env)
}
}

View File

@@ -30,11 +30,11 @@ impl ContextDriver for LocalDriver {
Ok(entries)
}
fn run(&self, program: &str, args: &[String]) -> io::Result<std::process::ExitStatus> {
Command::new(program).args(args).status()
fn run(&self, program: &str, args: &[String], env: &[(String, String)]) -> io::Result<std::process::ExitStatus> {
Command::new(program).args(args).envs(env.iter().map(|(k, v)| (k, v))).status()
}
fn run_output(&self, program: &str, args: &[String]) -> io::Result<std::process::Output> {
Command::new(program).args(args).output()
fn run_output(&self, program: &str, args: &[String], env: &[(String, String)]) -> io::Result<std::process::Output> {
Command::new(program).args(args).envs(env.iter().map(|(k, v)| (k, v))).output()
}
}

View File

@@ -5,12 +5,12 @@ use std::fs;
use std::io;
use std::path::PathBuf;
use super::api::Context;
use super::api::{Context, ContextConfig};
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct Config {
pub current_context: Option<String>,
pub contexts: HashMap<String, Context>,
pub contexts: HashMap<String, ContextConfig>,
}
pub struct ContextManager {
@@ -36,7 +36,7 @@ impl ContextManager {
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
} else {
let mut cfg = Config::default();
cfg.contexts.insert("local".to_string(), Context::Local);
cfg.contexts.insert("local".to_string(), ContextConfig::Local);
cfg.current_context = Some("local".to_string());
cfg
};
@@ -65,12 +65,12 @@ impl ContextManager {
self.config.contexts.keys().cloned().collect()
}
pub fn get_context(&self, name: &str) -> Option<&Context> {
self.config.contexts.get(name)
pub fn get_context(&self, name: &str) -> Option<Context> {
self.config.contexts.get(name).map(|cfg| Context::new(cfg.clone()))
}
pub fn add_context(&mut self, name: &str, context: Context) -> io::Result<()> {
self.config.contexts.insert(name.to_string(), context);
pub fn add_context(&mut self, name: &str, config: ContextConfig) -> io::Result<()> {
self.config.contexts.insert(name.to_string(), config);
self.save()
}
@@ -81,7 +81,7 @@ impl ContextManager {
if !self.config.contexts.contains_key("local") {
self.config
.contexts
.insert("local".to_string(), Context::Local);
.insert("local".to_string(), ContextConfig::Local);
}
}
self.save()?;
@@ -107,8 +107,8 @@ impl ContextManager {
.current_context
.as_deref()
.and_then(|name| self.config.contexts.get(name))
.cloned()
.unwrap_or(Context::Local)
.map(|cfg| Context::new(cfg.clone()))
.unwrap_or_else(|| Context::new(ContextConfig::Local))
}
pub fn current_name(&self) -> Option<String> {

View File

@@ -2,14 +2,15 @@ mod api;
mod local;
mod manager;
mod ssh;
mod schroot;
pub use api::{Context, ContextCommand};
pub use api::{Context, ContextCommand, ContextConfig};
pub use manager::ContextManager;
pub fn current_context() -> Context {
match ContextManager::new() {
Ok(mgr) => mgr.current(),
Err(_) => Context::Local,
Err(_) => Context::new(ContextConfig::Local),
}
}
@@ -25,7 +26,7 @@ mod tests {
let src_file = temp_dir.path().join("src.txt");
fs::write(&src_file, "local").unwrap();
let ctx = Context::Local;
let ctx = Context::new(ContextConfig::Local);
let dest = ctx.ensure_available(&src_file, "/tmp").unwrap();
// Should return canonical path
@@ -40,15 +41,14 @@ mod tests {
let mut mgr = ContextManager::with_path(path.clone());
// Add
let ssh_ctx = Context::Ssh {
let ssh_cfg = ContextConfig::Ssh {
host: "10.0.0.1".into(),
user: Some("admin".into()),
port: Some(2222),
};
mgr.add_context("myserver", ssh_ctx.clone()).unwrap();
mgr.add_context("myserver", ssh_cfg.clone()).unwrap();
assert!(mgr.get_context("myserver").is_some());
assert_eq!(mgr.get_context("myserver").unwrap(), &ssh_ctx);
// List
let list = mgr.list_contexts();
@@ -56,13 +56,11 @@ mod tests {
// Set Current
mgr.set_current("myserver").unwrap();
assert_eq!(mgr.current(), ssh_ctx);
assert_eq!(mgr.current_name(), Some("myserver".to_string()));
// Remove
mgr.remove_context("myserver").unwrap();
assert!(mgr.get_context("myserver").is_none());
assert_eq!(mgr.current(), Context::Local);
}
#[test]
@@ -72,7 +70,7 @@ mod tests {
{
let mut mgr = ContextManager::with_path(config_path.clone());
mgr.add_context("persistent", Context::Local).unwrap();
mgr.add_context("persistent", ContextConfig::Local).unwrap();
mgr.set_current("persistent").unwrap();
}

111
src/context/schroot.rs Normal file
View File

@@ -0,0 +1,111 @@
use super::api::ContextDriver;
use std::cell::RefCell;
use std::io;
use std::path::{Path, PathBuf};
use log::debug;
pub struct SchrootDriver {
pub name: String,
pub session: RefCell<Option<String>>,
}
impl ContextDriver for SchrootDriver {
fn ensure_available(&self, _src: &Path, _dest_root: &str) -> io::Result<PathBuf> {
// TODO: Implement schroot file transfer logic
Err(io::Error::new(
io::ErrorKind::Unsupported,
"ensure_available not yet implemented for schroot",
))
}
fn retrieve_path(&self, _src: &Path, _dest: &Path) -> io::Result<()> {
// TODO: Implement schroot file retrieval logic
Err(io::Error::new(
io::ErrorKind::Unsupported,
"retrieve_path not yet implemented for schroot",
))
}
fn list_files(&self, _path: &Path) -> io::Result<Vec<PathBuf>> {
// TODO: Implement schroot file listing logic
Err(io::Error::new(
io::ErrorKind::Unsupported,
"list_files not yet implemented for schroot",
))
}
fn run(&self, program: &str, args: &[String], env: &[(String, String)]) -> io::Result<std::process::ExitStatus> {
// Initialize session on first run
if self.session.borrow().is_none() {
let session_output = std::process::Command::new("schroot")
.arg("-b")
.arg("-c")
.arg(&self.name)
.output()?;
if !session_output.status.success() {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("Failed to create schroot session: {}",
String::from_utf8_lossy(&session_output.stderr))
));
}
let session_id = String::from_utf8(session_output.stdout)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
.trim()
.to_string();
*self.session.borrow_mut() = Some(session_id);
}
let session_id = self.session.borrow();
let session_id = session_id.as_ref().unwrap();
let mut cmd = std::process::Command::new("sudo");
cmd
.args(env.iter().map(|(k, v)| format!("{k}={v}")))
.arg("schroot")
.arg("-p") // Preserve environment
.arg("-r")
.arg("-c")
.arg(session_id)
.arg("--")
.arg(program)
.args(args);
debug!("Executing: {:?}", cmd);
cmd.status()
}
fn run_output(&self, program: &str, args: &[String], env: &[(String, String)]) -> io::Result<std::process::Output> {
let mut cmd = std::process::Command::new("sudo");
cmd
.arg("schroot")
.arg("-r")
.arg("-c")
.arg(&self.name)
.arg("--");
// Handle env variables for schroot by wrapping in env command
if !env.is_empty() {
cmd.arg("env");
for (k, v) in env {
cmd.arg(format!("{}={}", k, v));
}
}
cmd.arg(program).args(args);
cmd.output()
}
fn prepare_work_dir(&self) -> io::Result<String> {
// TODO: Implement schroot work dir creation
Err(io::Error::new(
io::ErrorKind::Unsupported,
"prepare_work_dir not yet implemented for schroot",
))
}
}

View File

@@ -90,12 +90,17 @@ impl ContextDriver for SshDriver {
Ok(files)
}
fn run(&self, program: &str, args: &[String]) -> io::Result<std::process::ExitStatus> {
fn run(&self, program: &str, args: &[String], env: &[(String, String)]) -> io::Result<std::process::ExitStatus> {
let sess = connect_ssh(&self.host, self.user.as_deref(), self.port)?;
let mut channel = sess.channel_session().map_err(io::Error::other)?;
// Construct command line
let mut cmd_line = program.to_string();
// Construct command line with env vars
// TODO: No, use ssh2 channel.set_env
let mut cmd_line = String::new();
for (key, value) in env {
cmd_line.push_str(&format!("export {}='{}'; ", key, value.replace("'", "'\\''")));
}
cmd_line.push_str(program);
for arg in args {
cmd_line.push(' ');
cmd_line.push_str(arg); // TODO: escape
@@ -119,12 +124,16 @@ impl ContextDriver for SshDriver {
Ok(ExitStatus::from_raw(code))
}
fn run_output(&self, program: &str, args: &[String]) -> io::Result<std::process::Output> {
fn run_output(&self, program: &str, args: &[String], env: &[(String, String)]) -> io::Result<std::process::Output> {
let sess = connect_ssh(&self.host, self.user.as_deref(), self.port)?;
let mut channel = sess.channel_session().map_err(io::Error::other)?;
// Construct command line
let mut cmd_line = program.to_string();
// Construct command line with env vars
let mut cmd_line = String::new();
for (key, value) in env {
cmd_line.push_str(&format!("export {}='{}'; ", key, value.replace("'", "'\\''")));
}
cmd_line.push_str(program);
for arg in args {
cmd_line.push(' ');
cmd_line.push_str(arg); // TODO: escape

View File

@@ -1,11 +1,14 @@
use crate::context::{Context, current_context};
use crate::context::{current_context, Context, ContextConfig};
use std::error::Error;
use std::fs;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
pub fn build_binary_package(
arch: Option<&str>,
series: Option<&str>,
cwd: Option<&Path>,
cross: bool,
) -> Result<(), Box<dyn Error>> {
let cwd = cwd.unwrap_or_else(|| Path::new("."));
@@ -37,7 +40,11 @@ pub fn build_binary_package(
);
// Run sbuild
run_sbuild(&ctx, &remote_dsc_path, arch, series, &build_root)?;
if cross {
run_cross_build(&ctx, &remote_dsc_path, arch, series, &build_root)?;
} else {
run_sbuild(&ctx, &remote_dsc_path, arch, series, &build_root)?;
}
// Retrieve artifacts
// Always retrieve to the directory containing the .dsc file
@@ -61,7 +68,9 @@ pub fn build_binary_package(
fn find_dsc_file(cwd: &Path, package: &str, version: &str) -> Result<PathBuf, Box<dyn Error>> {
let parent = cwd.parent().ok_or("Cannot find parent directory")?;
let dsc_name = format!("{}_{}.dsc", package, version);
// Strip epoch from version (e.g., "1:2.3.4-5" -> "2.3.4-5")
let version_without_epoch = version.split_once(':').map(|(_, v)| v).unwrap_or(version);
let dsc_name = format!("{}_{}.dsc", package, version_without_epoch);
let dsc_path = parent.join(&dsc_name);
if !dsc_path.exists() {
@@ -155,3 +164,95 @@ fn run_sbuild(
}
Ok(())
}
fn run_cross_build(
ctx: &Context,
dsc_path: &Path,
arch: Option<&str>,
series: Option<&str>,
output_dir: &str,
) -> Result<(), Box<dyn Error>> {
// TODO: Setup the schroot for cross-build?
let arch = arch.unwrap();
let series = series.unwrap();
let localarch = "amd64";
// let context = Context::new(ContextConfig::Schroot { name: format!("{series}-{localarch}-{arch}") });
let context = Context::new(ContextConfig::Schroot { name: format!("{series}-{localarch}") });
// Set environment variables for cross-compilation: dpkg-architecture variables
let dpkg_architecture = String::from_utf8(ctx.command("dpkg-architecture").arg(format!("-a{}", arch)).output()?.stdout)?;
let env_var_regex = regex::Regex::new(r"(?<key>.*)=(?<value>.*)").unwrap();
let mut env = HashMap::new();
for l in dpkg_architecture.lines() {
let capture = env_var_regex.captures(l).unwrap();
let key = capture.name("key").unwrap().as_str().to_string();
let value = capture.name("value").unwrap().as_str().to_string();
env.insert(key.clone(), value.clone());
if key == "DEB_HOST_GNU_TYPE" {
env.insert("CROSS_COMPILE".to_string(), format!("{value}-"));
}
}
env.insert("DEB_BUILD_PROFILES".to_string(), "cross".to_string());
env.insert("DEB_BUILD_OPTIONS".to_string(), "nocheck".to_string());
env.insert("LANG".to_string(), "C".to_string());
// Add target ('host') architecture
context.command("dpkg")
.envs(env.clone())
.arg("--add-architecture")
.arg(format!("{arch}"))
.status()?;
// Add missing repositories inside the schroot
// let ports_repo = if is_ubuntu {
let ports_repo = format!("deb [arch={arch}] http://ports.ubuntu.com/ubuntu-ports {series} main restricted universe multiverse");
// } else {
// format!("deb [arch={arch}] http://ftp.ports.debian.org/debian-ports {series} main")
// };
// Add ports repository to sources.list
context.command("sh")
.envs(env.clone())
.arg("-c")
.arg(format!("echo '{}' >> /etc/apt/sources.list", ports_repo))
.status()?;
// Update package lists
context.command("apt-get")
.envs(env.clone())
.arg("update")
.status()?;
context.command("apt-get")
.envs(env.clone())
.arg("-y")
.arg("install")
.arg("build-essential")
.arg(format!("crossbuild-essential-{arch}"))
.status()?;
// Install build dependencies
context.command("apt-get")
.envs(env.clone())
.arg("-y")
.arg("build-dep")
.arg(format!("--host-architecture={arch}"))
.arg("./")
.status()?;
context.command("debian/rules")
.envs(env.clone())
.arg("build")
.status()?;
context.command("fakeroot")
.envs(env.clone())
.arg("debian/rules")
.arg("binary")
.status()?;
Ok(())
}

View File

@@ -3,6 +3,7 @@ use std::io::Write;
extern crate clap;
use clap::{Command, arg, command};
use pkh::context::ContextConfig;
extern crate flate2;
@@ -53,7 +54,8 @@ fn main() {
Command::new("deb")
.about("Build the binary package")
.arg(arg!(-s --series <series> "Target distribution series").required(false))
.arg(arg!(-a --arch <arch> "Target architecture").required(false)),
.arg(arg!(-a --arch <arch> "Target architecture").required(false))
.arg(arg!(--cross "Cross-compile for target architecture (instead of using qemu-binfmt)").required(false)),
)
.subcommand(
Command::new("context")
@@ -142,8 +144,11 @@ fn main() {
let cwd = std::env::current_dir().unwrap();
let series = sub_matches.get_one::<String>("series").map(|s| s.as_str());
let arch = sub_matches.get_one::<String>("arch").map(|s| s.as_str());
let cross = sub_matches.get_one::<bool>("cross").unwrap_or(&false);
if let Err(e) = pkh::deb::build_binary_package(arch, series, Some(cwd.as_path())) {
if let Err(e) =
pkh::deb::build_binary_package(arch, series, Some(cwd.as_path()), *cross)
{
error!("{}", e);
std::process::exit(1);
}
@@ -168,7 +173,7 @@ fn main() {
.unwrap_or("local");
let context = match type_str {
"local" => Context::Local,
"local" => ContextConfig::Local,
"ssh" => {
let endpoint = args
.get_one::<String>("endpoint")
@@ -191,7 +196,7 @@ fn main() {
})
});
Context::Ssh { host, user, port }
ContextConfig::Ssh { host, user, port }
}
_ => {
error!("Unknown context type: {}", type_str);