exp: cross #2

This commit is contained in:
2025-12-20 00:06:07 +01:00
parent 8e9e19a6ca
commit 31bcd28c72
16 changed files with 1209 additions and 438 deletions

View File

@@ -27,6 +27,4 @@ xz2 = "0.1"
serde_json = "1.0.145"
directories = "6.0.0"
ssh2 = "0.9.5"
[dev-dependencies]
tempfile = "3.10.1"

View File

@@ -1,19 +1,37 @@
use serde::{Deserialize, Serialize};
use std::cell::{Ref, RefCell};
use std::ffi::OsStr;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::Mutex;
use super::local::LocalDriver;
use super::ssh::SshDriver;
use super::schroot::SchrootDriver;
use super::ssh::SshDriver;
use super::unshare::UnshareDriver;
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], 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>;
fn run(
&self,
program: &str,
args: &[String],
env: &[(String, String)],
cwd: Option<&str>,
) -> io::Result<std::process::ExitStatus>;
fn run_output(
&self,
program: &str,
args: &[String],
env: &[(String, String)],
cwd: Option<&str>,
) -> io::Result<std::process::Output>;
fn create_temp_dir(&self) -> io::Result<String>;
fn copy_path(&self, src: &Path, dest: &Path) -> io::Result<()>;
fn read_file(&self, path: &Path) -> io::Result<String>;
fn write_file(&self, path: &Path, content: &str) -> io::Result<()>;
}
/// Represents an execution environment (Local or via SSH).
@@ -35,19 +53,55 @@ pub enum ContextConfig {
#[serde(rename = "schroot")]
Schroot {
name: String,
}
parent: Option<String>,
},
#[serde(rename = "unshare")]
Unshare {
path: String,
parent: Option<String>,
},
}
pub struct Context {
config: ContextConfig,
driver: RefCell<Option<Box<dyn ContextDriver>>>,
pub config: ContextConfig,
pub parent: Option<Arc<Context>>,
driver: Mutex<Option<Box<dyn ContextDriver + Send + Sync>>>,
}
impl Context {
pub fn new(config: ContextConfig) -> Self {
let parent = match &config {
ContextConfig::Schroot {
parent: Some(parent_name),
..
}
| ContextConfig::Unshare {
parent: Some(parent_name),
..
} => {
let config_lock = crate::context::manager::MANAGER.get_config();
let parent_config = config_lock
.contexts
.get(parent_name)
.cloned()
.expect("Parent context not found");
Some(Arc::new(Context::new(parent_config)))
}
_ => None,
};
Self {
config,
driver: RefCell::new(None),
parent,
driver: Mutex::new(None),
}
}
pub fn with_parent(config: ContextConfig, parent: Arc<Context>) -> Self {
Self {
config,
parent: Some(parent),
driver: Mutex::new(None),
}
}
@@ -57,6 +111,7 @@ impl Context {
program: program.as_ref().to_string_lossy().to_string(),
args: Vec::new(),
env: Vec::new(),
cwd: None,
}
}
@@ -65,11 +120,14 @@ impl 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)
self.driver()
.as_ref()
.unwrap()
.ensure_available(src, dest_root)
}
pub fn prepare_work_dir(&self) -> io::Result<String> {
self.driver().prepare_work_dir()
pub fn create_temp_dir(&self) -> io::Result<String> {
self.driver().as_ref().unwrap().create_temp_dir()
}
/// Retrieve a file or directory from the context to the local filesystem.
@@ -77,31 +135,59 @@ impl Context {
/// The `src` path is on the context, `dest` is on the local machine.
/// If `src` is a directory, it is copied recursively.
pub fn retrieve_path(&self, src: &Path, dest: &Path) -> io::Result<()> {
self.driver().retrieve_path(src, dest)
self.driver().as_ref().unwrap().retrieve_path(src, dest)
}
/// List files in a directory on the context.
pub fn list_files(&self, path: &Path) -> io::Result<Vec<PathBuf>> {
self.driver().list_files(path)
self.driver().as_ref().unwrap().list_files(path)
}
fn driver(&self) -> Ref<Box<dyn ContextDriver>> {
if self.driver.borrow().is_none() {
let driver: Box<dyn ContextDriver> = match &self.config {
pub fn copy_path(&self, src: &Path, dest: &Path) -> io::Result<()> {
self.driver().as_ref().unwrap().copy_path(src, dest)
}
pub fn read_file(&self, path: &Path) -> io::Result<String> {
self.driver().as_ref().unwrap().read_file(path)
}
pub fn write_file(&self, path: &Path, content: &str) -> io::Result<()> {
self.driver().as_ref().unwrap().write_file(path, content)
}
pub fn driver(
&self,
) -> std::sync::MutexGuard<'_, Option<Box<dyn ContextDriver + Send + Sync>>> {
let mut driver_lock = self.driver.lock().unwrap();
if driver_lock.is_none() {
let driver: Box<dyn ContextDriver + Send + Sync> = 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 {
ContextConfig::Schroot { name, .. } => Box::new(SchrootDriver {
name: name.clone(),
session: RefCell::new(None),
session: std::sync::Mutex::new(None),
parent: self.parent.clone(),
}),
ContextConfig::Unshare { path, .. } => Box::new(UnshareDriver {
path: path.clone(),
parent: self.parent.clone(),
}),
};
*self.driver.borrow_mut() = Some(driver);
*driver_lock = Some(driver);
}
driver_lock
}
pub fn clone_raw(&self) -> Self {
Self {
config: self.config.clone(),
parent: self.parent.clone(),
driver: std::sync::Mutex::new(None),
}
Ref::map(self.driver.borrow(), |opt| opt.as_ref().unwrap())
}
}
@@ -117,6 +203,7 @@ pub struct ContextCommand<'a> {
program: String,
args: Vec<String>,
env: Vec<(String, String)>,
cwd: Option<String>,
}
impl<'a> ContextCommand<'a> {
@@ -161,12 +248,27 @@ impl<'a> ContextCommand<'a> {
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
}
pub fn status(&mut self) -> io::Result<std::process::ExitStatus> {
self.context.driver().run(&self.program, &self.args, &self.env)
self.context.driver().as_ref().unwrap().run(
&self.program,
&self.args,
&self.env,
self.cwd.as_deref(),
)
}
// Capture output
pub fn output(&mut self) -> io::Result<std::process::Output> {
self.context.driver().run_output(&self.program, &self.args, &self.env)
self.context.driver().as_ref().unwrap().run_output(
&self.program,
&self.args,
&self.env,
self.cwd.as_deref(),
)
}
}

View File

@@ -12,9 +12,9 @@ impl ContextDriver for LocalDriver {
src.canonicalize()
}
fn prepare_work_dir(&self) -> io::Result<String> {
// TODO: Fix that, we should not always use '..' as work directory locally
Ok("..".to_string())
fn create_temp_dir(&self) -> io::Result<String> {
let temp_dir = tempfile::Builder::new().prefix("pkh-").tempdir()?;
Ok(temp_dir.keep().to_string_lossy().to_string())
}
fn retrieve_path(&self, _src: &Path, _dest: &Path) -> io::Result<()> {
@@ -30,11 +30,60 @@ impl ContextDriver for LocalDriver {
Ok(entries)
}
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(
&self,
program: &str,
args: &[String],
env: &[(String, String)],
cwd: Option<&str>,
) -> io::Result<std::process::ExitStatus> {
let mut cmd = Command::new(program);
cmd.args(args).envs(env.iter().map(|(k, v)| (k, v)));
if let Some(dir) = cwd {
cmd.current_dir(dir);
}
cmd.status()
}
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()
fn run_output(
&self,
program: &str,
args: &[String],
env: &[(String, String)],
cwd: Option<&str>,
) -> io::Result<std::process::Output> {
let mut cmd = Command::new(program);
cmd.args(args).envs(env.iter().map(|(k, v)| (k, v)));
if let Some(dir) = cwd {
cmd.current_dir(dir);
}
cmd.output()
}
fn copy_path(&self, src: &Path, dest: &Path) -> io::Result<()> {
copy_dir_recursive(src, dest)
}
fn read_file(&self, path: &Path) -> io::Result<String> {
std::fs::read_to_string(path)
}
fn write_file(&self, path: &Path, content: &str) -> io::Result<()> {
std::fs::write(path, content)
}
}
fn copy_dir_recursive(src: &Path, dest: &Path) -> io::Result<()> {
if src.is_dir() {
std::fs::create_dir_all(dest)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let path = entry.path();
let dest_path = dest.join(entry.file_name());
copy_dir_recursive(&path, &dest_path)?;
}
} else {
std::fs::copy(src, dest)?;
}
Ok(())
}

View File

@@ -4,22 +4,39 @@ use std::collections::HashMap;
use std::fs;
use std::io;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::RwLock;
use super::api::{Context, ContextConfig};
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Config {
pub current_context: Option<String>,
pub context: String,
pub contexts: HashMap<String, ContextConfig>,
}
pub struct ContextManager {
config_path: PathBuf,
config: Config,
impl Default for Config {
fn default() -> Self {
let mut contexts = HashMap::new();
contexts.insert("local".to_string(), ContextConfig::Local);
Self {
context: "local".to_string(),
contexts,
}
}
}
pub struct ContextManager {
context: RwLock<Arc<Context>>,
config_path: PathBuf,
config: RwLock<Config>,
}
pub static MANAGER: std::sync::LazyLock<ContextManager> =
std::sync::LazyLock::new(|| ContextManager::new().expect("Cannot setup context manager"));
impl ContextManager {
pub fn new() -> io::Result<Self> {
fn new() -> io::Result<Self> {
let proj_dirs = ProjectDirs::from("com", "pkh", "pkh").ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
@@ -31,67 +48,101 @@ impl ContextManager {
let config_path = config_dir.join("contexts.json");
let config = if config_path.exists() {
// Load existing configuration file
let content = fs::read_to_string(&config_path)?;
serde_json::from_str(&content)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
} else {
let mut cfg = Config::default();
cfg.contexts.insert("local".to_string(), ContextConfig::Local);
cfg.current_context = Some("local".to_string());
cfg
// Create a new configuration file
Config::default()
};
Ok(Self {
context: RwLock::new(Arc::new(Self::make_context(
config.context.as_str(),
&config,
))),
config_path,
config,
config: RwLock::new(config),
})
}
pub fn get_config(&self) -> std::sync::RwLockReadGuard<'_, Config> {
self.config.read().unwrap()
}
pub fn with_path(path: PathBuf) -> Self {
let config = Config::default();
Self {
context: RwLock::new(Arc::new(Self::make_context("local", &config))),
config_path: path,
config: Config::default(),
config: RwLock::new(config),
}
}
pub fn save(&self) -> io::Result<()> {
let content = serde_json::to_string_pretty(&self.config)
let config = self.config.read().unwrap();
let content = serde_json::to_string_pretty(&*config)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
fs::write(&self.config_path, content)?;
Ok(())
}
fn make_context(name: &str, config: &Config) -> Context {
let context_config = config
.contexts
.get(name)
.cloned()
.expect("Context not found in config");
Context::new(context_config)
}
pub fn list_contexts(&self) -> Vec<String> {
self.config.contexts.keys().cloned().collect()
self.config
.read()
.unwrap()
.contexts
.keys()
.cloned()
.collect()
}
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, config: ContextConfig) -> io::Result<()> {
self.config.contexts.insert(name.to_string(), config);
pub fn add_context(&self, name: &str, config: ContextConfig) -> io::Result<()> {
self.config
.write()
.unwrap()
.contexts
.insert(name.to_string(), config);
self.save()
}
pub fn remove_context(&mut self, name: &str) -> io::Result<()> {
if self.config.contexts.remove(name).is_some() {
if self.config.current_context.as_deref() == Some(name) {
self.config.current_context = Some("local".to_string());
if !self.config.contexts.contains_key("local") {
self.config
.contexts
.insert("local".to_string(), ContextConfig::Local);
pub fn remove_context(&self, name: &str) -> io::Result<()> {
let mut config = self.config.write().unwrap();
if name == "local" {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Cannot remove local context",
));
}
if config.contexts.remove(name).is_some() {
// If we are removing the current context, fallback to local
if name == config.context {
config.context = "local".to_string();
self.set_current_ephemeral(Self::make_context("local", &config));
}
drop(config); // Drop write lock before saving
self.save()?;
}
Ok(())
}
pub fn set_current(&mut self, name: &str) -> io::Result<()> {
if self.config.contexts.contains_key(name) {
self.config.current_context = Some(name.to_string());
pub fn set_current(&self, name: &str) -> io::Result<()> {
let mut config = self.config.write().unwrap();
if config.contexts.contains_key(name) {
config.context = name.to_string();
self.set_current_ephemeral(Self::make_context(name, &config));
drop(config); // Drop write lock before saving
self.save()?;
Ok(())
} else {
@@ -102,16 +153,15 @@ impl ContextManager {
}
}
pub fn current(&self) -> Context {
self.config
.current_context
.as_deref()
.and_then(|name| self.config.contexts.get(name))
.map(|cfg| Context::new(cfg.clone()))
.unwrap_or_else(|| Context::new(ContextConfig::Local))
pub fn set_current_ephemeral(&self, context: Context) {
*self.context.write().unwrap() = context.into();
}
pub fn current_name(&self) -> Option<String> {
self.config.current_context.clone()
pub fn current(&self) -> Arc<Context> {
self.context.read().unwrap().clone()
}
pub fn current_name(&self) -> String {
self.config.read().unwrap().context.clone()
}
}

View File

@@ -1,17 +1,20 @@
mod api;
mod local;
mod manager;
mod ssh;
mod schroot;
mod ssh;
mod unshare;
pub use api::{Context, ContextCommand, ContextConfig};
pub use manager::ContextManager;
use std::sync::Arc;
pub fn current_context() -> Context {
match ContextManager::new() {
Ok(mgr) => mgr.current(),
Err(_) => Context::new(ContextConfig::Local),
}
pub fn manager() -> &'static ContextManager {
&manager::MANAGER
}
pub fn current() -> Arc<Context> {
manager::MANAGER.current()
}
#[cfg(test)]
@@ -38,7 +41,7 @@ mod tests {
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path().to_path_buf();
let mut mgr = ContextManager::with_path(path.clone());
let mgr = ContextManager::with_path(path.clone());
// Add
let ssh_cfg = ContextConfig::Ssh {
@@ -48,7 +51,7 @@ mod tests {
};
mgr.add_context("myserver", ssh_cfg.clone()).unwrap();
assert!(mgr.get_context("myserver").is_some());
assert!(mgr.list_contexts().contains(&"myserver".to_string()));
// List
let list = mgr.list_contexts();
@@ -56,11 +59,11 @@ mod tests {
// Set Current
mgr.set_current("myserver").unwrap();
assert_eq!(mgr.current_name(), Some("myserver".to_string()));
assert_eq!(mgr.current_name(), "myserver".to_string());
// Remove
mgr.remove_context("myserver").unwrap();
assert!(mgr.get_context("myserver").is_none());
assert!(!mgr.list_contexts().contains(&"myserver".to_string()));
}
#[test]
@@ -69,7 +72,7 @@ mod tests {
let config_path = temp_dir.path().join("contexts.json");
{
let mut mgr = ContextManager::with_path(config_path.clone());
let mgr = ContextManager::with_path(config_path.clone());
mgr.add_context("persistent", ContextConfig::Local).unwrap();
mgr.set_current("persistent").unwrap();
}
@@ -77,10 +80,64 @@ mod tests {
let content = fs::read_to_string(&config_path).unwrap();
let loaded_config: super::manager::Config = serde_json::from_str(&content).unwrap();
assert_eq!(
loaded_config.current_context,
Some("persistent".to_string())
);
assert_eq!(loaded_config.context, "persistent".to_string());
assert!(loaded_config.contexts.contains_key("persistent"));
}
#[test]
fn test_context_fallback_on_removal() {
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path().to_path_buf();
let mgr = ContextManager::with_path(path);
// 1. Add and set a context
mgr.add_context("temp", ContextConfig::Local).unwrap();
mgr.set_current("temp").unwrap();
assert_eq!(mgr.current_name(), "temp");
// 2. Remove it
mgr.remove_context("temp").unwrap();
// 3. Should have fallen back to local
assert_eq!(mgr.current_name(), "local");
assert!(mgr.list_contexts().contains(&"local".to_string()));
}
#[test]
fn test_context_file_ops() {
let temp_dir = tempfile::tempdir().unwrap();
let ctx = super::manager().current();
let file_path = temp_dir.path().join("test.txt");
let content = "hello world";
// 1. Write file
ctx.write_file(&file_path, content).unwrap();
// 2. Read file
let read_content = ctx.read_file(&file_path).unwrap();
assert_eq!(read_content, content);
// 3. Copy path
let dest_path = temp_dir.path().join("test_copy.txt");
ctx.copy_path(&file_path, &dest_path).unwrap();
let copied_content = ctx.read_file(&dest_path).unwrap();
assert_eq!(copied_content, content);
// 4. Recursive copy
let subdir = temp_dir.path().join("subdir");
std::fs::create_dir_all(&subdir).unwrap();
let subfile = subdir.join("subfile.txt");
ctx.write_file(&subfile, "subcontent").unwrap();
let subdir_copy = temp_dir.path().join("subdir_copy");
ctx.copy_path(&subdir, &subdir_copy).unwrap();
assert!(subdir_copy.exists());
assert!(subdir_copy.join("subfile.txt").exists());
assert_eq!(
ctx.read_file(&subdir_copy.join("subfile.txt")).unwrap(),
"subcontent"
);
}
}

View File

@@ -1,21 +1,17 @@
use super::api::ContextDriver;
use std::cell::RefCell;
use std::io;
use std::path::{Path, PathBuf};
use log::debug;
use std::sync::Arc;
pub struct SchrootDriver {
pub name: String,
pub session: RefCell<Option<String>>,
pub session: std::sync::Mutex<Option<String>>,
pub parent: Option<Arc<super::api::Context>>,
}
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 ensure_available(&self, src: &Path, _dest_root: &str) -> io::Result<PathBuf> {
src.canonicalize()
}
fn retrieve_path(&self, _src: &Path, _dest: &Path) -> io::Result<()> {
@@ -34,21 +30,36 @@ impl ContextDriver for SchrootDriver {
))
}
fn run(&self, program: &str, args: &[String], env: &[(String, String)]) -> io::Result<std::process::ExitStatus> {
fn run(
&self,
program: &str,
args: &[String],
env: &[(String, String)],
cwd: Option<&str>,
) -> io::Result<std::process::ExitStatus> {
// Initialize session on first run
if self.session.borrow().is_none() {
let session_output = std::process::Command::new("schroot")
let mut session_lock = self.session.lock().unwrap();
if session_lock.is_none() {
let session_output = if let Some(parent) = &self.parent {
parent
.command("schroot")
.arg("-b")
.arg("-c")
.arg(&self.name)
.output()?;
.output()?
} else {
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))
));
return Err(io::Error::other(format!(
"Failed to create schroot session: {}",
String::from_utf8_lossy(&session_output.stderr)
)));
}
let session_id = String::from_utf8(session_output.stdout)
@@ -56,56 +67,189 @@ impl ContextDriver for SchrootDriver {
.trim()
.to_string();
*self.session.borrow_mut() = Some(session_id);
*session_lock = Some(session_id);
}
drop(session_lock);
let session_id = self.session.borrow();
let session_id = session_id.as_ref().unwrap();
let session_lock = self.session.lock().unwrap();
let session_id = session_lock.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
if let Some(parent) = &self.parent {
let mut cmd = parent.command("sudo");
cmd.envs(env.iter().cloned());
if let Some(dir) = cwd {
cmd.arg("schroot")
.arg("-p")
.arg("-r")
.arg("-c")
.arg(session_id)
.arg("--")
.arg("sh")
.arg("-c")
.arg(format!("cd {} && {} {}", dir, program, args.join(" ")));
} else {
cmd.arg("schroot")
.arg("-p")
.arg("-r")
.arg("-c")
.arg(session_id)
.arg("--")
.arg(program)
.args(args);
}
cmd.status()
} else {
let mut cmd = std::process::Command::new("sudo");
cmd.args(env.iter().map(|(k, v)| format!("{k}={v}")));
debug!("Executing: {:?}", cmd);
if let Some(dir) = cwd {
cmd.arg("schroot")
.arg("-p")
.arg("-r")
.arg("-c")
.arg(session_id)
.arg("--")
.arg("sh")
.arg("-c")
.arg(format!("cd {} && {} {}", dir, program, args.join(" ")));
} else {
cmd.arg("schroot")
.arg("-p")
.arg("-r")
.arg("-c")
.arg(session_id)
.arg("--")
.arg(program)
.args(args);
}
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")
fn run_output(
&self,
program: &str,
args: &[String],
env: &[(String, String)],
cwd: Option<&str>,
) -> io::Result<std::process::Output> {
if let Some(parent) = &self.parent {
let mut cmd = parent.command("sudo");
cmd.envs(env.iter().cloned());
if let Some(dir) = cwd {
cmd.arg("schroot")
.arg("-r")
.arg("-c")
.arg(&self.name)
.arg("--")
.arg("sh")
.arg("-c");
let mut cmd_str = String::new();
cmd_str.push_str(&format!("cd {} && {} {}", dir, program, args.join(" ")));
cmd.arg(cmd_str);
} else {
cmd.arg("schroot")
.arg("-r")
.arg("-c")
.arg(&self.name)
.arg("--");
cmd.arg(program).args(args);
}
cmd.output()
} else {
let mut cmd = std::process::Command::new("sudo");
if let Some(dir) = cwd {
cmd.arg("schroot")
.arg("-r")
.arg("-c")
.arg(&self.name)
.arg("--")
.arg("sh")
.arg("-c");
let mut cmd_str = String::new();
for (k, v) in env {
cmd_str.push_str(&format!("{}={} ", k, v));
}
cmd_str.push_str(&format!("cd {} && {} {}", dir, program, args.join(" ")));
cmd.arg(cmd_str);
} else {
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",
))
fn create_temp_dir(&self) -> io::Result<String> {
let output = self.run_output("mktemp", &["-d".to_string()], &[], None)?;
if !output.status.success() {
return Err(io::Error::other("schroot mktemp failed"));
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
fn copy_path(&self, src: &Path, dest: &Path) -> io::Result<()> {
let status = self.run(
"cp",
&[
"-a".to_string(),
src.to_string_lossy().to_string(),
dest.to_string_lossy().to_string(),
],
&[],
None,
)?;
if !status.success() {
return Err(io::Error::other("schroot copy failed"));
}
Ok(())
}
fn read_file(&self, path: &Path) -> io::Result<String> {
let output = self.run_output("cat", &[path.to_string_lossy().to_string()], &[], None)?;
if !output.status.success() {
return Err(io::Error::other(format!(
"schroot read failed: {}",
String::from_utf8_lossy(&output.stderr)
)));
}
String::from_utf8(output.stdout).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
fn write_file(&self, path: &Path, content: &str) -> io::Result<()> {
// TODO: change that command, it's not safe
let status = self.run(
"sh",
&[
"-c".to_string(),
format!(
"echo -ne '{}' > '{}'",
content.replace("'", "'\\''"),
path.to_string_lossy()
),
],
&[],
None,
)?;
if !status.success() {
return Err(io::Error::other("schroot write failed"));
}
Ok(())
}
}

View File

@@ -2,7 +2,9 @@
/// Context driver: Copies over SFTP with ssh2, executes commands over ssh2 channels
use super::api::ContextDriver;
use log::debug;
use ssh2;
use std::fs;
use std::io::Write;
use std::io::{self, Read};
use std::net::TcpStream;
#[cfg(unix)]
@@ -90,7 +92,13 @@ impl ContextDriver for SshDriver {
Ok(files)
}
fn run(&self, program: &str, args: &[String], env: &[(String, String)]) -> io::Result<std::process::ExitStatus> {
fn run(
&self,
program: &str,
args: &[String],
env: &[(String, String)],
cwd: Option<&str>,
) -> 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)?;
@@ -98,7 +106,14 @@ impl ContextDriver for SshDriver {
// 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(&format!(
"export {}='{}'; ",
key,
value.replace("'", "'\\''")
));
}
if let Some(dir) = cwd {
cmd_line.push_str(&format!("cd {} && ", dir));
}
cmd_line.push_str(program);
for arg in args {
@@ -124,14 +139,27 @@ impl ContextDriver for SshDriver {
Ok(ExitStatus::from_raw(code))
}
fn run_output(&self, program: &str, args: &[String], env: &[(String, String)]) -> io::Result<std::process::Output> {
fn run_output(
&self,
program: &str,
args: &[String],
env: &[(String, String)],
cwd: Option<&str>,
) -> 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 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(&format!(
"export {}='{}'; ",
key,
value.replace("'", "'\\''")
));
}
if let Some(dir) = cwd {
cmd_line.push_str(&format!("cd {} && ", dir));
}
cmd_line.push_str(program);
for arg in args {
@@ -164,7 +192,7 @@ impl ContextDriver for SshDriver {
})
}
fn prepare_work_dir(&self) -> io::Result<String> {
fn create_temp_dir(&self) -> io::Result<String> {
let sess = connect_ssh(&self.host, self.user.as_deref(), self.port)?;
let mut channel = sess.channel_session().map_err(io::Error::other)?;
@@ -182,6 +210,40 @@ impl ContextDriver for SshDriver {
Ok(stdout.trim().to_string())
}
fn copy_path(&self, src: &Path, dest: &Path) -> 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)?;
// TODO: use sftp
let cmd = format!("cp -a {:?} {:?}", src, dest);
debug!("Executing remote copy: {}", cmd);
channel.exec(&cmd).map_err(io::Error::other)?;
channel.wait_close().map_err(io::Error::other)?;
if channel.exit_status().unwrap_or(-1) != 0 {
return Err(io::Error::other(format!("Remote copy failed: {}", cmd)));
}
Ok(())
}
fn read_file(&self, path: &Path) -> io::Result<String> {
let sess = connect_ssh(&self.host, self.user.as_deref(), self.port)?;
let sftp = sess.sftp().map_err(io::Error::other)?;
let mut remote_file = sftp.open(path).map_err(io::Error::other)?;
let mut content = String::new();
remote_file.read_to_string(&mut content)?;
Ok(content)
}
fn write_file(&self, path: &Path, content: &str) -> io::Result<()> {
let sess = connect_ssh(&self.host, self.user.as_deref(), self.port)?;
let sftp = sess.sftp().map_err(io::Error::other)?;
if let Some(parent) = path.parent() {
let _ = sftp.mkdir(parent, 0o755);
}
let mut remote_file = sftp.create(path).map_err(io::Error::other)?;
remote_file.write_all(content.as_bytes())?;
Ok(())
}
}
impl SshDriver {

177
src/context/unshare.rs Normal file
View File

@@ -0,0 +1,177 @@
use super::api::{Context, ContextCommand, ContextDriver};
use log::debug;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
pub struct UnshareDriver {
pub path: String,
pub parent: Option<Arc<super::api::Context>>,
}
/// Recursively copy a directory and all its contents
fn copy_dir_recursive(src: &Path, dest: &Path) -> io::Result<()> {
// Create the destination directory
std::fs::create_dir_all(dest)?;
// Iterate through the source directory
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let src_path = entry.path();
let dest_path = dest.join(entry.file_name());
if src_path.is_dir() {
// Recursively copy subdirectories
copy_dir_recursive(&src_path, &dest_path)?;
} else {
// Copy files
std::fs::copy(&src_path, &dest_path)?;
}
}
Ok(())
}
impl ContextDriver for UnshareDriver {
fn ensure_available(&self, src: &Path, dest_root: &str) -> io::Result<PathBuf> {
// Construct the destination path inside the chroot
let dest_dir = Path::new(&self.path).join(dest_root.trim_start_matches('/'));
debug!(
"unshare/ensure_available: copy '{}' to '{}'",
src.display(),
dest_dir.display()
);
// Ensure the destination directory exists
std::fs::create_dir_all(&dest_dir)?;
// Get the filename from the source path
let filename = src
.file_name()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Invalid source path"))?;
// Construct the full destination path
let dest_path = dest_dir.join(filename);
// Copy the file or directory into the chroot
if src.is_dir() {
copy_dir_recursive(src, &dest_path)?;
debug!(
"Copied directory {} to {}",
src.display(),
dest_path.display()
);
} else {
std::fs::copy(src, &dest_path)?;
debug!("Copied file {} to {}", src.display(), dest_path.display());
}
// Return the path as it appears inside the chroot (without the chroot prefix)
Ok(Path::new(dest_root).join(filename))
}
fn retrieve_path(&self, _src: &Path, _dest: &Path) -> io::Result<()> {
// TODO: Implement chroot file retrieval logic
Err(io::Error::new(
io::ErrorKind::Unsupported,
"retrieve_path not yet implemented for chroot",
))
}
fn list_files(&self, _path: &Path) -> io::Result<Vec<PathBuf>> {
// TODO: Implement chroot file listing logic
Err(io::Error::new(
io::ErrorKind::Unsupported,
"list_files not yet implemented for chroot",
))
}
fn run(
&self,
program: &str,
args: &[String],
env: &[(String, String)],
cwd: Option<&str>,
) -> io::Result<std::process::ExitStatus> {
self.command(program, args, env, cwd).status()
}
fn run_output(
&self,
program: &str,
args: &[String],
env: &[(String, String)],
cwd: Option<&str>,
) -> io::Result<std::process::Output> {
self.command(program, args, env, cwd).output()
}
fn create_temp_dir(&self) -> io::Result<String> {
// Create a temporary directory inside the chroot
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let work_dir_name = format!("pkh-build-{}", timestamp);
let work_dir_inside_chroot = format!("/tmp/{}", work_dir_name);
// Create the directory on the host filesystem
let host_path = Path::new(&self.path).join("tmp").join(&work_dir_name);
std::fs::create_dir_all(&host_path)?;
debug!(
"Created work directory: {} (host: {})",
work_dir_inside_chroot,
host_path.display()
);
// Return the path as it appears inside the chroot
Ok(work_dir_inside_chroot)
}
fn copy_path(&self, src: &Path, dest: &Path) -> io::Result<()> {
let host_src = Path::new(&self.path).join(src.to_string_lossy().trim_start_matches('/'));
let host_dest = Path::new(&self.path).join(dest.to_string_lossy().trim_start_matches('/'));
self.parent().copy_path(&host_src, &host_dest)
}
fn read_file(&self, path: &Path) -> io::Result<String> {
let host_path = Path::new(&self.path).join(path.to_string_lossy().trim_start_matches('/'));
self.parent().read_file(&host_path)
}
fn write_file(&self, path: &Path, content: &str) -> io::Result<()> {
let host_path = Path::new(&self.path).join(path.to_string_lossy().trim_start_matches('/'));
self.parent().write_file(&host_path, content)
}
}
impl UnshareDriver {
fn parent(&self) -> &Context {
self.parent
.as_ref()
.expect("UnshareDriver requires a parent context")
}
fn command(
&self,
program: &str,
args: &[String],
env: &[(String, String)],
cwd: Option<&str>,
) -> ContextCommand<'_> {
let mut cmd = self.parent().command("sudo");
cmd.args(env.iter().map(|(k, v)| format!("{k}={v}")));
cmd.arg("unshare").arg("-R").arg(&self.path);
if let Some(dir) = cwd {
cmd.arg("-w").arg(dir);
}
cmd.arg(program).args(args);
cmd
}
}

View File

@@ -1,258 +0,0 @@
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("."));
// Parse changelog to get package name and version
let changelog_path = cwd.join("debian/changelog");
let (package, version, _series) = crate::changelog::parse_changelog_header(&changelog_path)?;
// Find .dsc file
let dsc_path = find_dsc_file(cwd, &package, &version)?;
println!("Building {} using sbuild...", dsc_path.display());
// Identify all related files from .dsc
let mut files_to_ensure = get_dsc_related_files(&dsc_path)?;
// Ensure dsc itself is included (usually first)
if !files_to_ensure.contains(&dsc_path) {
files_to_ensure.insert(0, dsc_path.clone());
}
// Prepare Environment
let ctx = current_context();
let build_root = ctx.prepare_work_dir()?;
// Ensure availability of all needed files for the build
let remote_dsc_path = upload_package_files(&ctx, &files_to_ensure, &build_root, &dsc_path)?;
println!(
"Building {} on {}...",
remote_dsc_path.display(),
build_root
);
// Run sbuild
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
let local_output_dir = dsc_path
.parent()
.ok_or("Could not determine parent directory of dsc file")?;
println!("Retrieving artifacts to {}...", local_output_dir.display());
// Only retrieve .deb files
let remote_files = ctx.list_files(Path::new(&build_root))?;
for remote_file in remote_files {
if remote_file.extension().is_some_and(|ext| ext == "deb") {
let file_name = remote_file.file_name().ok_or("Invalid remote filename")?;
let local_dest = local_output_dir.join(file_name);
ctx.retrieve_path(&remote_file, &local_dest)?;
}
}
Ok(())
}
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")?;
// 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() {
return Err(format!("Could not find .dsc file at {}", dsc_path.display()).into());
}
Ok(dsc_path)
}
fn get_dsc_related_files(dsc_path: &Path) -> Result<Vec<PathBuf>, Box<dyn Error>> {
let content = std::fs::read_to_string(dsc_path)?;
let parent = dsc_path.parent().unwrap(); // dsc_path exists so parent exists
let mut files = Vec::new();
let mut in_files = false;
for line in content.lines() {
if line.starts_with("Files:") {
in_files = true;
continue;
}
if in_files {
if line.starts_with(' ') {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
let filename = parts[2];
let filepath = parent.join(filename);
if filepath.exists() {
files.push(filepath);
} else {
return Err(
format!("Referenced file {} not found", filepath.display()).into()
);
}
}
} else {
in_files = false;
}
}
}
Ok(files)
}
fn upload_package_files(
ctx: &Context,
files: &[PathBuf],
dest_root: &str,
local_dsc_path: &Path,
) -> Result<PathBuf, Box<dyn Error>> {
let mut remote_dsc_path = PathBuf::new();
for file in files {
let remote_path = ctx.ensure_available(file, dest_root)?;
// Check if this is the dsc file by comparing file names
if let (Some(f_name), Some(dsc_name)) = (file.file_name(), local_dsc_path.file_name())
&& f_name == dsc_name
{
remote_dsc_path = remote_path;
}
}
if remote_dsc_path.as_os_str().is_empty() {
return Err("Failed to determine remote path for .dsc file".into());
}
Ok(remote_dsc_path)
}
fn run_sbuild(
ctx: &Context,
dsc_path: &Path,
arch: Option<&str>,
series: Option<&str>,
output_dir: &str,
) -> Result<(), Box<dyn Error>> {
let mut cmd = ctx.command("sbuild");
cmd.arg("--chroot-mode=unshare");
if let Some(a) = arch {
cmd.arg(format!("--arch={}", a));
}
if let Some(s) = series {
cmd.arg(format!("--dist={}", s));
}
// Add output directory argument
cmd.arg(format!("--build-dir={}", output_dir));
let status = cmd.arg(dsc_path).status()?;
if !status.success() {
return Err(format!("sbuild failed with status: {}", status).into());
}
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(())
}

169
src/deb/cross.rs Normal file
View File

@@ -0,0 +1,169 @@
use crate::context;
use crate::context::{Context, ContextConfig};
use std::collections::HashMap;
use std::error::Error;
/// Setup a specific chroot context for native cross builds
pub fn setup_native_context(series: &str) -> Result<(), Box<dyn Error>> {
// Use the system cache directory to store the chroot
let proj_dirs = directories::ProjectDirs::from("com", "pkh", "pkh")
.expect("Could not determine cache directory");
let cache_dir = proj_dirs.cache_dir();
let chroot_path = cache_dir.join(format!("pkh-cross-{series}"));
// Check if the chroot already exists
if !chroot_path.exists() {
log::debug!(
"Creating new chroot for {} at {}...",
series,
chroot_path.display()
);
std::fs::create_dir_all(&chroot_path)?;
let status = context::current()
.command("sudo")
.arg("mmdebstrap")
.arg("--variant=buildd")
.arg(series)
.arg(chroot_path.to_string_lossy().to_string())
.status()?;
if !status.success() {
// Clean up on failure
let _ = std::fs::remove_dir_all(&chroot_path);
return Err(format!("mmdebstrap failed for series {}", series).into());
}
}
// Switch to an ephemeral context to build the package in the chroot
context::manager().set_current_ephemeral(Context::new(ContextConfig::Unshare {
path: chroot_path.to_string_lossy().to_string(),
parent: Some(context::manager().current_name()),
}));
Ok(())
}
/// Set environment variables for cross-compilation
pub fn setup_environment(
env: &mut HashMap<String, String>,
arch: &str,
) -> Result<(), Box<dyn Error>> {
let dpkg_architecture = String::from_utf8(
context::current()
.command("dpkg-architecture")
.arg(format!("-a{}", arch))
.output()?
.stdout,
)?;
let env_var_regex = regex::Regex::new(r"(?<key>.*)=(?<value>.*)").unwrap();
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());
Ok(())
}
/// Ensure that repositories for target architecture are available
/// This also handles the 'ports.ubuntu.com' vs 'archive.ubuntu.com' on Ubuntu
pub fn ensure_repositories(arch: &str, series: &str) -> Result<(), Box<dyn Error>> {
let ctx = context::current();
// Add target ('host') architecture
ctx.command("dpkg")
.arg("--add-architecture")
.arg(arch)
.status()?;
// Check if we are on Ubuntu
let os_release = String::from_utf8(ctx.command("cat").arg("/etc/os-release").output()?.stdout)?;
if !os_release.contains("ID=ubuntu") {
return Ok(());
}
// Handle DEB822 format (Ubuntu 24.04+)
let deb822_path = "/etc/apt/sources.list.d/ubuntu.sources";
let has_deb822 = ctx
.command("test")
.arg("-f")
.arg(deb822_path)
.status()?
.success();
if has_deb822 {
// Scope existing to amd64 if not already scoped
// This looks for URIs lines for archive/security and adds Architectures: amd64 on the next line if missing
ctx.command("sed")
.arg("-i")
.arg("/URIs:.*\\(archive\\|security\\)\\.ubuntu\\.com/ { n; /^Architectures:/ ! i Architectures: amd64 }")
.arg(deb822_path)
.status()?;
// Add ports if not already present
let has_ports = ctx
.command("grep")
.arg("-q")
.arg("ports.ubuntu.com")
.arg(deb822_path)
.status()?
.success();
if !has_ports {
let ports_block = format!(
"\nTypes: deb\nURIs: http://ports.ubuntu.com/ubuntu-ports\nSuites: {series} {series}-updates {series}-backports {series}-security {series}-proposed\nComponents: main restricted universe multiverse\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\nArchitectures: {arch}\n"
);
ctx.command("sh")
.arg("-c")
.arg(format!("echo '{}' >> {}", ports_block, deb822_path))
.status()?;
}
} else {
// Traditional sources.list
let sources_path = "/etc/apt/sources.list";
// Scope archive.ubuntu.com and security.ubuntu.com to amd64 if not already scoped
// 1. For lines without [], insert [arch=amd64]
// 2. For lines with [], insert arch=amd64 inside the brackets
// Both only if arch= is not already present on the line
ctx.command("sed")
.arg("-i")
.arg(r"/archive.ubuntu.com\|security.ubuntu.com/ { /arch=/ ! { /^deb \[/ ! s/^deb /deb [arch=amd64] /; /^deb \[/ s/^deb \[\([^]]*\)\]/deb [arch=amd64 \1]/ } }")
.arg(sources_path)
.status()?;
// Add ports repository to sources.list if not already present
let has_ports = ctx
.command("grep")
.arg("-q")
.arg("ports.ubuntu.com")
.arg(sources_path)
.status()?
.success();
if !has_ports {
let ports_lines = format!(
"deb [arch={arch}] http://ports.ubuntu.com/ubuntu-ports {series} main restricted universe multiverse\n\
deb [arch={arch}] http://ports.ubuntu.com/ubuntu-ports {series}-updates main restricted universe multiverse\n\
deb [arch={arch}] http://ports.ubuntu.com/ubuntu-ports {series}-backports main restricted universe multiverse\n\
deb [arch={arch}] http://ports.ubuntu.com/ubuntu-ports {series}-security main restricted universe multiverse\n\
deb [arch={arch}] http://ports.ubuntu.com/ubuntu-ports {series}-proposed main restricted universe multiverse"
);
ctx.command("sh")
.arg("-c")
.arg(format!("echo '{}' >> {}", ports_lines, sources_path))
.status()?;
}
}
Ok(())
}

99
src/deb/local.rs Normal file
View File

@@ -0,0 +1,99 @@
/// Local binary package building
/// Directly calling 'debian/rules' in current context
use crate::context;
use std::collections::HashMap;
use std::error::Error;
use std::path::Path;
use crate::deb::cross;
pub fn build(
_cwd: &Path,
package: &str,
_version: &str,
arch: &str,
series: &str,
build_root: &str,
cross: bool,
) -> Result<(), Box<dyn Error>> {
// Environment
let mut env = HashMap::<String, String>::new();
env.insert("LANG".to_string(), "C".to_string());
let ctx = context::current();
if cross {
cross::setup_environment(&mut env, arch)?;
cross::ensure_repositories(arch, series)?;
}
// Update package lists
let status = ctx
.command("apt-get")
.envs(env.clone())
.arg("update")
.status()?;
if !status.success() {
return Err(
"Could not execute apt-get update. If this is a local build, try executing with sudo."
.into(),
);
}
// Install essential packages
let mut cmd = ctx.command("apt-get");
cmd.envs(env.clone())
.arg("-y")
.arg("install")
.arg("build-essential")
.arg("fakeroot");
if cross {
cmd.arg(format!("crossbuild-essential-{arch}"));
}
let status = cmd.status()?;
if !status.success() {
return Err("Could not install essential packages for the build".into());
}
// Install build dependencies
let mut cmd = ctx.command("apt-get");
cmd.current_dir(format!("{build_root}/{package}"))
.envs(env.clone())
.arg("-y")
.arg("build-dep");
if cross {
cmd.arg(format!("--host-architecture={arch}"));
}
let status = cmd.arg("./").status()?;
if !status.success() {
return Err("Could not install build-dependencies for the build".into());
}
// Run the build step
let status = ctx
.command("debian/rules")
.current_dir(format!("{build_root}/{package}"))
.envs(env.clone())
.arg("build")
.status()?;
if !status.success() {
return Err("Error while building the package".into());
}
// Run the 'binary' step to produce deb
let status = ctx
.command("fakeroot")
.current_dir(format!("{build_root}/{package}"))
.envs(env.clone())
.arg("debian/rules")
.arg("binary")
.status()?;
if !status.success() {
return Err(
"Error while building the binary artifacts (.deb) from the built package".into(),
);
}
Ok(())
}

70
src/deb/mod.rs Normal file
View File

@@ -0,0 +1,70 @@
mod cross;
mod local;
mod sbuild;
use crate::context;
use std::error::Error;
use std::path::Path;
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("."));
// Parse changelog to get package name, version and series
let changelog_path = cwd.join("debian/changelog");
let (package, version, package_series) =
crate::changelog::parse_changelog_header(&changelog_path)?;
let series = if let Some(s) = series {
s
} else {
&package_series
};
let current_arch = crate::get_current_arch();
let arch = arch.unwrap_or(&current_arch);
// Specific case: native cross-compilation, we don't allow that
// instead this wraps to an automatic unshare chroot
// using an ephemeral context
if cross {
cross::setup_native_context(series)?;
}
// Prepare Environment
let ctx = context::current();
let build_root = ctx.create_temp_dir()?;
// Ensure availability of all needed files for the build
let parent_dir = cwd.parent().ok_or("Cannot find parent directory")?;
ctx.ensure_available(parent_dir, &build_root)?;
let parent_dir_name = parent_dir
.file_name()
.ok_or("Cannot find parent directory name")?;
let build_root = format!("{}/{}", build_root, parent_dir_name.to_str().unwrap());
// Run sbuild
if cross {
local::build(cwd, &package, &version, arch, series, &build_root, cross)?;
} else {
sbuild::build(cwd, &package, &version, arch, series, &build_root, cross)?;
}
// Retrieve artifacts
// Always retrieve to the directory containing the .dsc file
println!("Retrieving artifacts to {}...", parent_dir.display());
// Only retrieve .deb files
let remote_files = ctx.list_files(Path::new(&build_root))?;
for remote_file in remote_files {
if remote_file.extension().is_some_and(|ext| ext == "deb") {
let file_name = remote_file.file_name().ok_or("Invalid remote filename")?;
let local_dest = parent_dir.join(file_name);
ctx.retrieve_path(&remote_file, &local_dest)?;
}
}
Ok(())
}

51
src/deb/sbuild.rs Normal file
View File

@@ -0,0 +1,51 @@
/// Sbuild binary package building
/// Call 'sbuild' with the dsc file to build the package with unshare
use crate::context;
use std::error::Error;
use std::path::{Path, PathBuf};
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")?;
// 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() {
return Err(format!("Could not find .dsc file at {}", dsc_path.display()).into());
}
Ok(dsc_path)
}
pub fn build(
cwd: &Path,
package: &str,
version: &str,
arch: &str,
series: &str,
build_root: &str,
cross: bool,
) -> Result<(), Box<dyn Error>> {
let dsc_path = find_dsc_file(cwd, package, version)?;
let ctx = context::current();
let mut cmd = ctx.command("sbuild");
cmd.arg("--chroot-mode=unshare");
if cross {
cmd.arg(format!("--host={}", arch));
} else {
cmd.arg(format!("--arch={}", arch));
}
cmd.arg(format!("--dist={}", series));
// Add output directory argument
cmd.arg(format!("--build-dir={}", build_root));
let status = cmd.arg(dsc_path).status()?;
if !status.success() {
return Err(format!("sbuild failed with status: {}", status).into());
}
Ok(())
}

View File

@@ -6,3 +6,15 @@ pub mod package_info;
pub mod pull;
pub type ProgressCallback<'a> = Option<&'a dyn Fn(&str, &str, usize, usize)>;
/// Returns the architecture of current CPU, debian-compatible
pub fn get_current_arch() -> String {
match std::env::consts::ARCH {
"x86" => "i386".to_string(),
"x86_64" => "amd64".to_string(),
"arm" => "armhf".to_string(),
"aarch64" => "arm64".to_string(),
"powerpc64" => "ppc64el".to_string(),
x => x.to_string(),
}
}

View File

@@ -73,7 +73,10 @@ fn main() {
.about("Remove a context")
.arg(arg!(<name> "Context name"))
)
.subcommand(Command::new("ls").about("List contexts"))
.subcommand(
Command::new("ls")
.about("List contexts")
)
.subcommand(Command::new("show").about("Show current context"))
.subcommand(
Command::new("use")
@@ -154,15 +157,7 @@ fn main() {
}
}
Some(("context", sub_matches)) => {
use pkh::context::{Context, ContextManager};
let mut mgr = match ContextManager::new() {
Ok(mgr) => mgr,
Err(e) => {
error!("Failed to initialize context manager: {}", e);
std::process::exit(1);
}
};
let mgr = pkh::context::manager();
match sub_matches.subcommand() {
Some(("create", args)) => {
@@ -222,20 +217,14 @@ fn main() {
let contexts = mgr.list_contexts();
let current = mgr.current_name();
for ctx in contexts {
if Some(&ctx) == current.as_ref() {
if ctx == current {
println!("* {}", ctx);
} else {
println!(" {}", ctx);
}
}
}
Some(("show", _)) => {
if let Some(name) = mgr.current_name() {
println!("{}", name);
} else {
println!("No context set (defaulting to local)");
}
}
Some(("show", _)) => {}
Some(("use", args)) => {
let name = args.get_one::<String>("name").unwrap();
if let Err(e) = mgr.set_current(name) {

View File

@@ -105,7 +105,7 @@ pub async fn get_dist_series(dist: &str) -> Result<Vec<String>, Box<dyn Error>>
}
}
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?;
if debian_series.contains(&series.to_string()) {
return Ok("debian".to_string());