From 8e9e19a6ca228531cd4c15c8b9b4de7a702ab2e6 Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Wed, 17 Dec 2025 17:27:27 +0100 Subject: [PATCH] exp: cross --- src/context/api.rs | 88 +++++++++++++++++++++++++------- src/context/local.rs | 8 +-- src/context/manager.rs | 20 ++++---- src/context/mod.rs | 16 +++--- src/context/schroot.rs | 111 +++++++++++++++++++++++++++++++++++++++++ src/context/ssh.rs | 21 +++++--- src/deb.rs | 107 +++++++++++++++++++++++++++++++++++++-- src/main.rs | 13 +++-- 8 files changed, 330 insertions(+), 54 deletions(-) create mode 100644 src/context/schroot.rs diff --git a/src/context/api.rs b/src/context/api.rs index 842e884..2faaa46 100644 --- a/src/context/api.rs +++ b/src/context/api.rs @@ -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; fn retrieve_path(&self, src: &Path, dest: &Path) -> io::Result<()>; fn list_files(&self, path: &Path) -> io::Result>; - fn run(&self, program: &str, args: &[String]) -> io::Result; - fn run_output(&self, program: &str, args: &[String]) -> io::Result; + fn run(&self, program: &str, args: &[String], env: &[(String, String)]) -> io::Result; + fn run_output(&self, program: &str, args: &[String], env: &[(String, String)]) -> io::Result; fn prepare_work_dir(&self) -> io::Result; } @@ -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, port: Option, }, + #[serde(rename = "schroot")] + Schroot { + name: String, + } +} + +pub struct Context { + config: ContextConfig, + driver: RefCell>>, } impl Context { - pub fn command>(&self, program: S) -> ContextCommand { + pub fn new(config: ContextConfig) -> Self { + Self { + config, + driver: RefCell::new(None), + } + } + + pub fn command>(&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 { - 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> { + if self.driver.borrow().is_none() { + let driver: Box = 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, +pub struct ContextCommand<'a> { + context: &'a Context, program: String, args: Vec, + env: Vec<(String, String)>, } -impl ContextCommand { +impl<'a> ContextCommand<'a> { pub fn arg>(&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(&mut self, key: K, val: V) -> &mut Self + where + K: AsRef, + V: AsRef, + { + self.env.push(( + key.as_ref().to_string_lossy().to_string(), + val.as_ref().to_string_lossy().to_string(), + )); + self + } + + pub fn envs(&mut self, vars: I) -> &mut Self + where + I: IntoIterator, + K: AsRef, + V: AsRef, + { + for (key, val) in vars { + self.env(key, val); + } + self + } + pub fn status(&mut self) -> io::Result { - 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 { - self.driver.run_output(&self.program, &self.args) + self.context.driver().run_output(&self.program, &self.args, &self.env) } } diff --git a/src/context/local.rs b/src/context/local.rs index 591fbb9..2caf6e0 100644 --- a/src/context/local.rs +++ b/src/context/local.rs @@ -30,11 +30,11 @@ impl ContextDriver for LocalDriver { Ok(entries) } - fn run(&self, program: &str, args: &[String]) -> io::Result { - Command::new(program).args(args).status() + fn run(&self, program: &str, args: &[String], env: &[(String, String)]) -> io::Result { + Command::new(program).args(args).envs(env.iter().map(|(k, v)| (k, v))).status() } - fn run_output(&self, program: &str, args: &[String]) -> io::Result { - Command::new(program).args(args).output() + fn run_output(&self, program: &str, args: &[String], env: &[(String, String)]) -> io::Result { + Command::new(program).args(args).envs(env.iter().map(|(k, v)| (k, v))).output() } } diff --git a/src/context/manager.rs b/src/context/manager.rs index 7dd3623..07892ce 100644 --- a/src/context/manager.rs +++ b/src/context/manager.rs @@ -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, - pub contexts: HashMap, + pub contexts: HashMap, } 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 { + 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 { diff --git a/src/context/mod.rs b/src/context/mod.rs index 874addd..f578a08 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -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(); } diff --git a/src/context/schroot.rs b/src/context/schroot.rs new file mode 100644 index 0000000..b40f1b8 --- /dev/null +++ b/src/context/schroot.rs @@ -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>, +} + +impl ContextDriver for SchrootDriver { + fn ensure_available(&self, _src: &Path, _dest_root: &str) -> io::Result { + // 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> { + // 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 { + // 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 { + 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 { + // TODO: Implement schroot work dir creation + Err(io::Error::new( + io::ErrorKind::Unsupported, + "prepare_work_dir not yet implemented for schroot", + )) + } +} \ No newline at end of file diff --git a/src/context/ssh.rs b/src/context/ssh.rs index 5356e19..9fe1976 100644 --- a/src/context/ssh.rs +++ b/src/context/ssh.rs @@ -90,12 +90,17 @@ impl ContextDriver for SshDriver { Ok(files) } - fn run(&self, program: &str, args: &[String]) -> io::Result { + fn run(&self, program: &str, args: &[String], env: &[(String, String)]) -> io::Result { 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 { + fn run_output(&self, program: &str, args: &[String], env: &[(String, String)]) -> io::Result { 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 diff --git a/src/deb.rs b/src/deb.rs index 08a9727..6b826fc 100644 --- a/src/deb.rs +++ b/src/deb.rs @@ -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> { 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> { 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> { + // 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"(?.*)=(?.*)").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(()) +} diff --git a/src/main.rs b/src/main.rs index 47734a7..0176d74 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 "Target distribution series").required(false)) - .arg(arg!(-a --arch "Target architecture").required(false)), + .arg(arg!(-a --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::("series").map(|s| s.as_str()); let arch = sub_matches.get_one::("arch").map(|s| s.as_str()); + let cross = sub_matches.get_one::("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::("endpoint") @@ -191,7 +196,7 @@ fn main() { }) }); - Context::Ssh { host, user, port } + ContextConfig::Ssh { host, user, port } } _ => { error!("Unknown context type: {}", type_str);