deb: cross-compilation, ephemeral contexts, local builds
All checks were successful
CI / build (push) Successful in 7m18s
All checks were successful
CI / build (push) Successful in 7m18s
Multiple changes: - New contexts (schroot, unshare) - Cross-building quirks, with ephemeral contexts and repositories management - Contexts with parents, global context manager, better lifetime handling - Local building of binary packages - Pull: pulling dsc files by default - Many small bugfixes and changes Co-authored-by: Valentin Haudiquet <valentin.haudiquet@canonical.com> Co-committed-by: Valentin Haudiquet <valentin.haudiquet@canonical.com>
This commit was merged in pull request #1.
This commit is contained in:
19
.github/workflows/ci.yml
vendored
19
.github/workflows/ci.yml
vendored
@@ -12,7 +12,14 @@ env:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ubuntu:24.04
|
||||
options: --privileged --cap-add SYS_ADMIN --security-opt apparmor:unconfined
|
||||
steps:
|
||||
- name: Set up container image
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y nodejs sudo curl wget ca-certificates build-essential
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
@@ -28,7 +35,11 @@ jobs:
|
||||
- name: Install runtime system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y pristine-tar sbuild mmdebstrap dpkg-dev
|
||||
- name: Run tests
|
||||
run: cargo test
|
||||
|
||||
sudo apt-get install -y git pristine-tar sbuild mmdebstrap util-linux dpkg-dev
|
||||
- name: Setup subuid/subgid
|
||||
run: |
|
||||
usermod --add-subuids 100000-200000 --add-subgids 100000-200000 ${USER:-root}
|
||||
- name: Run tests with verbose logging (timeout 30min)
|
||||
env:
|
||||
RUST_LOG: debug
|
||||
run: timeout 30m cargo test -- --nocapture
|
||||
|
||||
@@ -27,6 +27,7 @@ xz2 = "0.1"
|
||||
serde_json = "1.0.145"
|
||||
directories = "6.0.0"
|
||||
ssh2 = "0.9.5"
|
||||
tempfile = "3.10.1"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.10.1"
|
||||
test-log = "0.2.19"
|
||||
|
||||
@@ -19,7 +19,6 @@ pub fn build_source_package(cwd: Option<&Path>) -> Result<(), Box<dyn Error>> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// We are not testing the build part, as for now this is just a wrapper
|
||||
// around dpkg-buildpackage.
|
||||
|
||||
@@ -2,16 +2,36 @@ use serde::{Deserialize, Serialize};
|
||||
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::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]) -> io::Result<std::process::ExitStatus>;
|
||||
fn run_output(&self, program: &str, args: &[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).
|
||||
@@ -20,7 +40,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 +50,68 @@ pub enum Context {
|
||||
user: Option<String>,
|
||||
port: Option<u16>,
|
||||
},
|
||||
#[serde(rename = "schroot")]
|
||||
Schroot {
|
||||
name: String,
|
||||
parent: Option<String>,
|
||||
},
|
||||
#[serde(rename = "unshare")]
|
||||
Unshare {
|
||||
path: String,
|
||||
parent: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct Context {
|
||||
pub config: ContextConfig,
|
||||
pub parent: Option<Arc<Context>>,
|
||||
driver: Mutex<Option<Box<dyn ContextDriver + Send + Sync>>>,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn command<S: AsRef<OsStr>>(&self, program: S) -> ContextCommand {
|
||||
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,
|
||||
parent,
|
||||
driver: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_parent(config: ContextConfig, parent: Arc<Context>) -> Self {
|
||||
Self {
|
||||
config,
|
||||
parent: Some(parent),
|
||||
driver: Mutex::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(),
|
||||
cwd: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,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.
|
||||
@@ -58,22 +135,58 @@ 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) -> 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,
|
||||
}),
|
||||
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 {
|
||||
name: name.clone(),
|
||||
session: std::sync::Mutex::new(None),
|
||||
parent: self.parent.clone(),
|
||||
}),
|
||||
ContextConfig::Unshare { path, .. } => Box::new(UnshareDriver {
|
||||
path: path.clone(),
|
||||
parent: self.parent.clone(),
|
||||
}),
|
||||
};
|
||||
*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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,13 +198,15 @@ 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)>,
|
||||
cwd: Option<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 +224,51 @@ 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 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.driver.run(&self.program, &self.args)
|
||||
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.driver.run_output(&self.program, &self.args)
|
||||
self.context.driver().as_ref().unwrap().run_output(
|
||||
&self.program,
|
||||
&self.args,
|
||||
&self.env,
|
||||
self.cwd.as_deref(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,17 +8,24 @@ use std::process::Command;
|
||||
pub struct LocalDriver;
|
||||
|
||||
impl ContextDriver for LocalDriver {
|
||||
fn ensure_available(&self, src: &Path, _dest_root: &str) -> io::Result<PathBuf> {
|
||||
src.canonicalize()
|
||||
fn ensure_available(&self, src: &Path, dest_root: &str) -> io::Result<PathBuf> {
|
||||
let dest_root_path = Path::new(dest_root);
|
||||
let dest = dest_root_path.join(src.file_name().unwrap_or(src.as_os_str()));
|
||||
|
||||
if src != dest {
|
||||
// Copy src inside dest_root
|
||||
self.copy_path(src, &dest)?;
|
||||
}
|
||||
dest.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<()> {
|
||||
Ok(())
|
||||
fn retrieve_path(&self, src: &Path, dest: &Path) -> io::Result<()> {
|
||||
self.copy_path(src, dest)
|
||||
}
|
||||
|
||||
fn list_files(&self, path: &Path) -> io::Result<Vec<PathBuf>> {
|
||||
@@ -30,11 +37,60 @@ 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)],
|
||||
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]) -> io::Result<std::process::Output> {
|
||||
Command::new(program).args(args).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(())
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
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 contexts: HashMap<String, Context>,
|
||||
pub context: String,
|
||||
pub contexts: HashMap<String, ContextConfig>,
|
||||
}
|
||||
|
||||
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: Config,
|
||||
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(), Context::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)
|
||||
}
|
||||
|
||||
pub fn add_context(&mut self, name: &str, context: Context) -> io::Result<()> {
|
||||
self.config.contexts.insert(name.to_string(), context);
|
||||
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(), Context::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))
|
||||
.cloned()
|
||||
.unwrap_or(Context::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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
mod api;
|
||||
mod local;
|
||||
mod manager;
|
||||
mod schroot;
|
||||
mod ssh;
|
||||
mod unshare;
|
||||
|
||||
pub use api::{Context, ContextCommand};
|
||||
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::Local,
|
||||
}
|
||||
pub fn manager() -> &'static ContextManager {
|
||||
&manager::MANAGER
|
||||
}
|
||||
|
||||
pub fn current() -> Arc<Context> {
|
||||
manager::MANAGER.current()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -25,11 +29,16 @@ 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
|
||||
assert_eq!(dest, src_file.canonicalize().unwrap());
|
||||
// Should return a path that exists and has the same content
|
||||
assert!(dest.exists());
|
||||
let content = fs::read_to_string(&dest).unwrap();
|
||||
assert_eq!(content, "local");
|
||||
|
||||
// The dest should be in the /tmp directory
|
||||
assert!(dest.starts_with("/tmp"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -37,18 +46,17 @@ 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_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);
|
||||
assert!(mgr.list_contexts().contains(&"myserver".to_string()));
|
||||
|
||||
// List
|
||||
let list = mgr.list_contexts();
|
||||
@@ -56,13 +64,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()));
|
||||
assert_eq!(mgr.current_name(), "myserver".to_string());
|
||||
|
||||
// Remove
|
||||
mgr.remove_context("myserver").unwrap();
|
||||
assert!(mgr.get_context("myserver").is_none());
|
||||
assert_eq!(mgr.current(), Context::Local);
|
||||
assert!(!mgr.list_contexts().contains(&"myserver".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -71,18 +77,72 @@ mod tests {
|
||||
let config_path = temp_dir.path().join("contexts.json");
|
||||
|
||||
{
|
||||
let mut mgr = ContextManager::with_path(config_path.clone());
|
||||
mgr.add_context("persistent", Context::Local).unwrap();
|
||||
let mgr = ContextManager::with_path(config_path.clone());
|
||||
mgr.add_context("persistent", ContextConfig::Local).unwrap();
|
||||
mgr.set_current("persistent").unwrap();
|
||||
}
|
||||
|
||||
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 = Context::new(ContextConfig::Local);
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
265
src/context/schroot.rs
Normal file
265
src/context/schroot.rs
Normal file
@@ -0,0 +1,265 @@
|
||||
/// Schroot context: execute commands in a schroot session
|
||||
/// Not tested, will need more work!
|
||||
use super::api::ContextDriver;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct SchrootDriver {
|
||||
pub name: String,
|
||||
pub session: std::sync::Mutex<Option<String>>,
|
||||
pub parent: Option<Arc<super::api::Context>>,
|
||||
}
|
||||
|
||||
use super::api::{Context, ContextConfig};
|
||||
|
||||
impl SchrootDriver {
|
||||
fn parent(&self) -> Arc<Context> {
|
||||
self.parent
|
||||
.clone()
|
||||
.unwrap_or_else(|| Arc::new(Context::new(ContextConfig::Local)))
|
||||
}
|
||||
|
||||
fn ensure_session(&self) -> io::Result<String> {
|
||||
let mut session_lock = self.session.lock().unwrap();
|
||||
if let Some(id) = session_lock.as_ref() {
|
||||
return Ok(id.clone());
|
||||
}
|
||||
|
||||
// Create new session
|
||||
let output = self
|
||||
.parent()
|
||||
.command("schroot")
|
||||
.arg("-b")
|
||||
.arg("-c")
|
||||
.arg(&self.name)
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(io::Error::other(format!(
|
||||
"Failed to create schroot session: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)));
|
||||
}
|
||||
|
||||
let session_id = String::from_utf8(output.stdout)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
*session_lock = Some(session_id.clone());
|
||||
Ok(session_id)
|
||||
}
|
||||
|
||||
fn get_session_location(&self, session_id: &str) -> io::Result<String> {
|
||||
let output = self
|
||||
.parent()
|
||||
.command("schroot")
|
||||
.arg("--location")
|
||||
.arg("-c")
|
||||
.arg(session_id)
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(io::Error::other(format!(
|
||||
"Failed to get schroot location: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8(output.stdout)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
|
||||
.trim()
|
||||
.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextDriver for SchrootDriver {
|
||||
fn ensure_available(&self, src: &Path, _dest_root: &str) -> io::Result<PathBuf> {
|
||||
src.canonicalize()
|
||||
}
|
||||
|
||||
fn retrieve_path(&self, src: &Path, dest: &Path) -> io::Result<()> {
|
||||
let session_id = self.ensure_session()?;
|
||||
let location = self.get_session_location(&session_id)?;
|
||||
|
||||
let path_in_chroot = src.strip_prefix("/").unwrap_or(src);
|
||||
let host_src = Path::new(&location).join(path_in_chroot);
|
||||
|
||||
self.parent().retrieve_path(&host_src, dest)
|
||||
}
|
||||
|
||||
fn list_files(&self, path: &Path) -> io::Result<Vec<PathBuf>> {
|
||||
let session_id = self.ensure_session()?;
|
||||
let location = self.get_session_location(&session_id)?;
|
||||
|
||||
let path_in_chroot = path.strip_prefix("/").unwrap_or(path);
|
||||
let host_path = Path::new(&location).join(path_in_chroot);
|
||||
|
||||
let files = self.parent().list_files(&host_path)?;
|
||||
let mut chroot_files = Vec::new();
|
||||
|
||||
// TODO: Check if we *need* to strip the prefix.
|
||||
// If we don't, we can just return `files`.
|
||||
for file in files {
|
||||
if let Ok(rel) = file.strip_prefix(&location) {
|
||||
chroot_files.push(Path::new("/").join(rel));
|
||||
} else {
|
||||
chroot_files.push(file);
|
||||
}
|
||||
}
|
||||
Ok(chroot_files)
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
program: &str,
|
||||
args: &[String],
|
||||
env: &[(String, String)],
|
||||
cwd: Option<&str>,
|
||||
) -> io::Result<std::process::ExitStatus> {
|
||||
let session_id = self.ensure_session()?;
|
||||
|
||||
// Construct the schroot command
|
||||
// schroot -p -r -c session_id -- program args...
|
||||
// If cwd is specified, we wrap in sh -c "cd cwd && ..."
|
||||
|
||||
let mut command_args = vec![
|
||||
"-p".to_string(),
|
||||
"-r".to_string(),
|
||||
"-c".to_string(),
|
||||
session_id,
|
||||
"--".to_string(),
|
||||
];
|
||||
|
||||
let mut actual_program = program.to_string();
|
||||
let mut actual_args = args.to_vec();
|
||||
|
||||
// Simplest: Wrap everything in `sh -c` if CWD or ENV is needed.
|
||||
if cwd.is_some() || !env.is_empty() {
|
||||
let mut shell_cmd = String::new();
|
||||
|
||||
if let Some(dir) = cwd {
|
||||
shell_cmd.push_str(&format!("cd {} && ", dir));
|
||||
}
|
||||
|
||||
if !env.is_empty() {
|
||||
shell_cmd.push_str("env ");
|
||||
for (k, v) in env {
|
||||
shell_cmd.push_str(&format!("{}={} ", k, v));
|
||||
}
|
||||
}
|
||||
|
||||
shell_cmd.push_str(&format!("{} {}", program, args.join(" ")));
|
||||
|
||||
actual_program = "sh".to_string();
|
||||
actual_args = vec!["-c".to_string(), shell_cmd];
|
||||
}
|
||||
|
||||
command_args.push(actual_program);
|
||||
command_args.extend(actual_args);
|
||||
|
||||
self.parent().command("schroot").args(command_args).status()
|
||||
}
|
||||
|
||||
fn run_output(
|
||||
&self,
|
||||
program: &str,
|
||||
args: &[String],
|
||||
env: &[(String, String)],
|
||||
cwd: Option<&str>,
|
||||
) -> io::Result<std::process::Output> {
|
||||
let session_id = self.ensure_session()?;
|
||||
|
||||
let mut command_args = vec![
|
||||
"-r".to_string(),
|
||||
"-c".to_string(),
|
||||
session_id,
|
||||
"--".to_string(),
|
||||
];
|
||||
|
||||
let mut actual_program = program.to_string();
|
||||
let mut actual_args = args.to_vec();
|
||||
|
||||
if cwd.is_some() || !env.is_empty() {
|
||||
let mut shell_cmd = String::new();
|
||||
|
||||
if let Some(dir) = cwd {
|
||||
shell_cmd.push_str(&format!("cd {} && ", dir));
|
||||
}
|
||||
|
||||
if !env.is_empty() {
|
||||
shell_cmd.push_str("env ");
|
||||
for (k, v) in env {
|
||||
shell_cmd.push_str(&format!("{}={} ", k, v));
|
||||
}
|
||||
}
|
||||
|
||||
shell_cmd.push_str(&format!("{} {}", program, args.join(" ")));
|
||||
|
||||
actual_program = "sh".to_string();
|
||||
actual_args = vec!["-c".to_string(), shell_cmd];
|
||||
}
|
||||
|
||||
command_args.push(actual_program);
|
||||
command_args.extend(actual_args);
|
||||
|
||||
self.parent().command("schroot").args(command_args).output()
|
||||
}
|
||||
|
||||
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<()> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -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,12 +92,30 @@ 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)],
|
||||
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)?;
|
||||
|
||||
// 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("'", "'\\''")
|
||||
));
|
||||
}
|
||||
if let Some(dir) = cwd {
|
||||
cmd_line.push_str(&format!("cd {} && ", dir));
|
||||
}
|
||||
cmd_line.push_str(program);
|
||||
for arg in args {
|
||||
cmd_line.push(' ');
|
||||
cmd_line.push_str(arg); // TODO: escape
|
||||
@@ -119,12 +139,29 @@ 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)],
|
||||
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
|
||||
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("'", "'\\''")
|
||||
));
|
||||
}
|
||||
if let Some(dir) = cwd {
|
||||
cmd_line.push_str(&format!("cd {} && ", dir));
|
||||
}
|
||||
cmd_line.push_str(program);
|
||||
for arg in args {
|
||||
cmd_line.push(' ');
|
||||
cmd_line.push_str(arg); // TODO: escape
|
||||
@@ -155,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)?;
|
||||
|
||||
@@ -173,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 {
|
||||
|
||||
193
src/context/unshare.rs
Normal file
193
src/context/unshare.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
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<()> {
|
||||
let host_src = Path::new(&self.path).join(src.to_string_lossy().trim_start_matches('/'));
|
||||
self.parent().retrieve_path(&host_src, dest)
|
||||
}
|
||||
|
||||
fn list_files(&self, path: &Path) -> io::Result<Vec<PathBuf>> {
|
||||
let host_path = Path::new(&self.path).join(path.to_string_lossy().trim_start_matches('/'));
|
||||
let host_entries = self.parent().list_files(&host_path)?;
|
||||
|
||||
let mut entries = Vec::new();
|
||||
let prefix = Path::new(&self.path);
|
||||
for entry in host_entries {
|
||||
if let Ok(rel_path) = entry.strip_prefix(prefix) {
|
||||
entries.push(Path::new("/").join(rel_path));
|
||||
} else {
|
||||
entries.push(entry);
|
||||
}
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
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("unshare");
|
||||
|
||||
cmd.envs(env.iter().cloned());
|
||||
|
||||
cmd.arg("--map-user=65536")
|
||||
.arg("--map-group=65536")
|
||||
.arg("--pid")
|
||||
.arg("--ipc")
|
||||
.arg("--uts")
|
||||
.arg("--map-auto")
|
||||
.arg("-r")
|
||||
.arg("--mount")
|
||||
.arg("--fork")
|
||||
.arg("-R")
|
||||
.arg(&self.path);
|
||||
|
||||
if let Some(dir) = cwd {
|
||||
cmd.arg("-w").arg(dir);
|
||||
}
|
||||
|
||||
cmd.arg("--").arg(program).args(args);
|
||||
|
||||
cmd
|
||||
}
|
||||
}
|
||||
157
src/deb.rs
157
src/deb.rs
@@ -1,157 +0,0 @@
|
||||
use crate::context::{Context, current_context};
|
||||
use std::error::Error;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub fn build_binary_package(
|
||||
arch: Option<&str>,
|
||||
series: Option<&str>,
|
||||
cwd: Option<&Path>,
|
||||
) -> 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
|
||||
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")?;
|
||||
let dsc_name = format!("{}_{}.dsc", package, version);
|
||||
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(())
|
||||
}
|
||||
338
src/deb/cross.rs
Normal file
338
src/deb/cross.rs
Normal file
@@ -0,0 +1,338 @@
|
||||
use crate::context;
|
||||
use crate::context::{Context, ContextConfig};
|
||||
use directories::ProjectDirs;
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tar::Archive;
|
||||
use xz2::read::XzDecoder;
|
||||
|
||||
pub struct EphemeralContextGuard {
|
||||
previous_context: String,
|
||||
chroot_path: PathBuf,
|
||||
}
|
||||
|
||||
impl EphemeralContextGuard {
|
||||
pub fn new(series: &str) -> Result<Self, Box<dyn Error>> {
|
||||
let current_context_name = context::manager().current_name();
|
||||
|
||||
// Create a temporary directory for the chroot
|
||||
let chroot_path_str = context::current().create_temp_dir()?;
|
||||
let chroot_path = PathBuf::from(chroot_path_str);
|
||||
|
||||
log::debug!(
|
||||
"Creating new chroot for {} at {}...",
|
||||
series,
|
||||
chroot_path.display()
|
||||
);
|
||||
|
||||
// Download and extract the chroot tarball
|
||||
Self::download_and_extract_chroot(series, &chroot_path)?;
|
||||
|
||||
// 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(current_context_name.clone()),
|
||||
}));
|
||||
|
||||
Ok(Self {
|
||||
previous_context: current_context_name,
|
||||
chroot_path,
|
||||
})
|
||||
}
|
||||
|
||||
fn download_and_extract_chroot(
|
||||
series: &str,
|
||||
chroot_path: &PathBuf,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
// Get project directories for caching
|
||||
let proj_dirs = ProjectDirs::from("com", "pkh", "pkh")
|
||||
.ok_or("Could not determine project directories")?;
|
||||
let cache_dir = proj_dirs.cache_dir();
|
||||
fs::create_dir_all(cache_dir)?;
|
||||
|
||||
// Create tarball filename based on series
|
||||
let tarball_filename = format!("{}-buildd.tar.xz", series);
|
||||
let tarball_path = cache_dir.join(&tarball_filename);
|
||||
|
||||
// Download tarball if it doesn't exist
|
||||
if !tarball_path.exists() {
|
||||
log::debug!("Downloading chroot tarball for {}...", series);
|
||||
Self::download_chroot_tarball(series, &tarball_path)?;
|
||||
} else {
|
||||
log::debug!("Using cached chroot tarball for {}", series);
|
||||
}
|
||||
|
||||
// Extract tarball to chroot directory
|
||||
log::debug!("Extracting chroot tarball to {}...", chroot_path.display());
|
||||
Self::extract_tarball(&tarball_path, chroot_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn download_chroot_tarball(series: &str, tarball_path: &Path) -> Result<(), Box<dyn Error>> {
|
||||
// Use mmdebstrap to download the tarball to the cache directory
|
||||
let status = context::current()
|
||||
.command("mmdebstrap")
|
||||
.arg("--variant=buildd")
|
||||
.arg("--mode=unshare")
|
||||
.arg("--format=tar")
|
||||
.arg(series)
|
||||
.arg(tarball_path.to_string_lossy().to_string())
|
||||
.status()?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(format!("Failed to download chroot tarball for series {}", series).into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_tarball(
|
||||
tarball_path: &PathBuf,
|
||||
chroot_path: &PathBuf,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
// Create the chroot directory
|
||||
fs::create_dir_all(chroot_path)?;
|
||||
|
||||
// Open the tarball file
|
||||
let tarball_file = std::fs::File::open(tarball_path)?;
|
||||
let xz_decoder = XzDecoder::new(tarball_file);
|
||||
let mut archive = Archive::new(xz_decoder);
|
||||
|
||||
// Extract all files to the chroot directory
|
||||
archive.unpack(chroot_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EphemeralContextGuard {
|
||||
fn drop(&mut self) {
|
||||
log::debug!("Cleaning up ephemeral context...");
|
||||
// Reset to normal context
|
||||
if let Err(e) = context::manager().set_current(&self.previous_context) {
|
||||
log::error!("Failed to restore context {}: {}", self.previous_context, e);
|
||||
}
|
||||
|
||||
// Remove chroot directory
|
||||
// We use the restored context to execute the cleanup command
|
||||
let result = context::current()
|
||||
.command("sudo")
|
||||
.arg("rm")
|
||||
.arg("-rf")
|
||||
.arg(&self.chroot_path)
|
||||
.status();
|
||||
|
||||
match result {
|
||||
Ok(status) => {
|
||||
if !status.success() {
|
||||
log::error!(
|
||||
"Failed to remove chroot directory {}",
|
||||
self.chroot_path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to execute cleanup command for {}: {}",
|
||||
self.chroot_path.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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("-a")
|
||||
.arg(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();
|
||||
let local_arch = crate::get_current_arch();
|
||||
|
||||
// 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 {
|
||||
ensure_repositories_deb822(&ctx, arch, &local_arch, series, deb822_path)?;
|
||||
} else {
|
||||
ensure_repositories_legacy(&ctx, arch, &local_arch, series, "/etc/apt/sources.list")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_repositories_deb822(
|
||||
ctx: &context::Context,
|
||||
arch: &str,
|
||||
local_arch: &str,
|
||||
series: &str,
|
||||
deb822_path: &str,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
// Scope existing to local_arch if not already scoped
|
||||
ctx.command("sed")
|
||||
.arg("-i")
|
||||
.arg(format!("/URIs:.*\\(archive\\|security\\)\\.ubuntu\\.com/ {{ n; /^Architectures:/ ! i Architectures: {} }}", local_arch))
|
||||
.arg(deb822_path)
|
||||
.status()?;
|
||||
|
||||
// Ensure all components are enabled for the primary architecture
|
||||
ctx.command("sed")
|
||||
.arg("-i")
|
||||
.arg("/URIs:.*\\(archive\\|security\\)\\.ubuntu\\.com/,/Components:/ s/^Components:.*/Components: main restricted universe multiverse/")
|
||||
.arg(deb822_path)
|
||||
.status()?;
|
||||
|
||||
// Ensure all suites (pockets) are enabled for the primary architecture
|
||||
// Excluding 'proposed' as it contains unstable software
|
||||
let suites = format!("{series} {series}-updates {series}-backports {series}-security");
|
||||
ctx.command("sed")
|
||||
.arg("-i")
|
||||
.arg(format!(
|
||||
"/URIs:.*\\(archive\\|security\\)\\.ubuntu\\.com/,/Suites:/ s/^Suites:.*/Suites: {}/",
|
||||
suites
|
||||
))
|
||||
.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\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()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_repositories_legacy(
|
||||
ctx: &context::Context,
|
||||
arch: &str,
|
||||
local_arch: &str,
|
||||
series: &str,
|
||||
sources_path: &str,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
// Scope archive.ubuntu.com and security.ubuntu.com to local_arch if not already scoped
|
||||
ctx.command("sed")
|
||||
.arg("-i")
|
||||
.arg(format!(
|
||||
r"/archive.ubuntu.com\|security.ubuntu.com/ {{ /arch=/ ! {{ /^deb \[/ ! s/^deb /deb [arch={}] /; /^deb \[/ s/^deb \[\([^]]*\)\]/deb [arch={} \1]/ }} }}",
|
||||
local_arch, local_arch
|
||||
))
|
||||
.arg(sources_path)
|
||||
.status()?;
|
||||
|
||||
// Ensure all components (main restricted universe multiverse) are present for all archive/security lines
|
||||
ctx.command("sed")
|
||||
.arg("-i")
|
||||
.arg(r"/archive.ubuntu.com\|security.ubuntu.com/ s/\( main\)\?\([ ]\+restricted\)\?\([ ]\+universe\)\?\([ ]\+multiverse\)\?$/ main restricted universe multiverse/")
|
||||
.arg(sources_path)
|
||||
.status()?;
|
||||
|
||||
// Ensure all pockets exist. If not, we append them.
|
||||
for pocket in ["", "-updates", "-backports", "-security"] {
|
||||
let suite = format!("{}{}", series, pocket);
|
||||
let has_suite = ctx
|
||||
.command("grep")
|
||||
.arg("-q")
|
||||
.arg(format!(" {}", suite))
|
||||
.arg(sources_path)
|
||||
.status()?
|
||||
.success();
|
||||
|
||||
if !has_suite {
|
||||
let line = format!(
|
||||
"deb [arch={}] http://archive.ubuntu.com/ubuntu/ {} main restricted universe multiverse",
|
||||
local_arch, suite
|
||||
);
|
||||
ctx.command("sh")
|
||||
.arg("-c")
|
||||
.arg(format!("echo '{}' >> {}", line, 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"
|
||||
);
|
||||
ctx.command("sh")
|
||||
.arg("-c")
|
||||
.arg(format!("echo '{}' >> {}", ports_lines, sources_path))
|
||||
.status()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
171
src/deb/local.rs
Normal file
171
src/deb/local.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
/// Local binary package building
|
||||
/// Directly calling 'debian/rules' in current context
|
||||
use crate::context;
|
||||
use crate::deb::find_dsc_file;
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::deb::cross;
|
||||
|
||||
pub fn build(
|
||||
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());
|
||||
env.insert("DEBIAN_FRONTEND".to_string(), "noninteractive".to_string());
|
||||
|
||||
let ctx = context::current();
|
||||
|
||||
if cross {
|
||||
log::debug!("Setting up environment for local cross build...");
|
||||
cross::setup_environment(&mut env, arch)?;
|
||||
cross::ensure_repositories(arch, series)?;
|
||||
}
|
||||
|
||||
// Update package lists
|
||||
log::debug!("Updating package lists for local build...");
|
||||
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
|
||||
log::debug!("Installing essential packages for local build...");
|
||||
let mut cmd = ctx.command("apt-get");
|
||||
|
||||
cmd.envs(env.clone())
|
||||
.arg("-y")
|
||||
.arg("install")
|
||||
.arg("build-essential")
|
||||
.arg("dose-builddebcheck")
|
||||
.arg("fakeroot");
|
||||
if cross {
|
||||
cmd.arg(format!("crossbuild-essential-{arch}"));
|
||||
cmd.arg(format!("libc6-{arch}-cross"));
|
||||
cmd.arg(format!("libc6-dev-{arch}-cross"));
|
||||
cmd.arg("dpkg-cross");
|
||||
cmd.arg(format!("libc6:{arch}"));
|
||||
cmd.arg(format!("libc6-dev:{arch}"));
|
||||
}
|
||||
let status = cmd.status()?;
|
||||
if !status.success() {
|
||||
return Err("Could not install essential packages for the build".into());
|
||||
}
|
||||
|
||||
// Install build dependencies
|
||||
log::debug!("Installing 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 build-dep fails, we try to explain the failure using dose-debcheck
|
||||
if !status.success() {
|
||||
dose3_explain_dependencies(package, version, arch, build_root, cross)?;
|
||||
return Err("Could not install build-dependencies for the build".into());
|
||||
}
|
||||
|
||||
// Run the build step
|
||||
log::debug!("Building (debian/rules build) package...");
|
||||
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(())
|
||||
}
|
||||
|
||||
fn dose3_explain_dependencies(
|
||||
package: &str,
|
||||
version: &str,
|
||||
arch: &str,
|
||||
build_root: &str,
|
||||
cross: bool,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let ctx = context::current();
|
||||
|
||||
// Construct the list of Packages files
|
||||
let mut bg_args = Vec::new();
|
||||
let mut cmd = ctx.command("apt-get");
|
||||
cmd.arg("indextargets")
|
||||
.arg("--format")
|
||||
.arg("$(FILENAME)")
|
||||
.arg("Created-By: Packages");
|
||||
|
||||
let output = cmd.output()?;
|
||||
if output.status.success() {
|
||||
let filenames = String::from_utf8_lossy(&output.stdout);
|
||||
for file in filenames.lines() {
|
||||
let file = file.trim();
|
||||
if !file.is_empty() {
|
||||
bg_args.push(file.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Transform the dsc file into a 'Source' stanza (replacing 'Source' with 'Package')
|
||||
let dsc_path = find_dsc_file(build_root, package, version)?;
|
||||
let mut dsc_content = ctx.read_file(&dsc_path)?;
|
||||
dsc_content = dsc_content.replace("Source", "Package");
|
||||
ctx.write_file(
|
||||
Path::new(&format!("{build_root}/dsc-processed")),
|
||||
&dsc_content,
|
||||
)?;
|
||||
|
||||
// Call dose-builddebcheck
|
||||
let local_arch = crate::get_current_arch();
|
||||
let mut cmd = ctx.command("dose-builddebcheck");
|
||||
cmd.arg("--verbose")
|
||||
.arg("--failures")
|
||||
.arg("--explain")
|
||||
.arg("--summary")
|
||||
.arg(format!("--deb-native-arch={}", local_arch));
|
||||
|
||||
if cross {
|
||||
cmd.arg(format!("--deb-host-arch={}", arch))
|
||||
.arg("--deb-profiles=cross")
|
||||
.arg(format!("--deb-foreign-archs={}", arch));
|
||||
}
|
||||
|
||||
cmd.args(bg_args).arg(format!("{build_root}/dsc-processed"));
|
||||
cmd.status()?;
|
||||
Ok(())
|
||||
}
|
||||
178
src/deb/mod.rs
Normal file
178
src/deb/mod.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
mod cross;
|
||||
mod local;
|
||||
mod sbuild;
|
||||
|
||||
use crate::context;
|
||||
use std::error::Error;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum BuildMode {
|
||||
Sbuild,
|
||||
Local,
|
||||
}
|
||||
|
||||
pub fn build_binary_package(
|
||||
arch: Option<&str>,
|
||||
series: Option<&str>,
|
||||
cwd: Option<&Path>,
|
||||
cross: bool,
|
||||
mode: Option<BuildMode>,
|
||||
) -> 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(¤t_arch);
|
||||
|
||||
// Make sure we select a specific mode, either using user-requested
|
||||
// or by using default for user-supplied parameters
|
||||
let mode = if let Some(m) = mode {
|
||||
m
|
||||
} else {
|
||||
// For cross-compilation, we use local with an ephemeral context
|
||||
// created by the cross-compilation handler (see below)
|
||||
if cross {
|
||||
BuildMode::Local
|
||||
} else {
|
||||
// By default, we use sbuild
|
||||
BuildMode::Sbuild
|
||||
}
|
||||
};
|
||||
|
||||
// Specific case: native cross-compilation, we don't allow that
|
||||
// instead this wraps to an automatic unshare chroot
|
||||
// using an ephemeral context
|
||||
let _guard = if cross && mode == BuildMode::Local {
|
||||
Some(cross::EphemeralContextGuard::new(series)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Prepare build directory
|
||||
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 the build using target build mode
|
||||
match mode {
|
||||
BuildMode::Local => local::build(&package, &version, arch, series, &build_root, cross)?,
|
||||
BuildMode::Sbuild => sbuild::build(&package, &version, arch, series, &build_root, cross)?,
|
||||
};
|
||||
|
||||
// Retrieve produced .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(())
|
||||
}
|
||||
|
||||
fn find_dsc_file(
|
||||
build_root: &str,
|
||||
package: &str,
|
||||
version: &str,
|
||||
) -> Result<PathBuf, Box<dyn Error>> {
|
||||
// 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 = PathBuf::from(build_root).join(&dsc_name);
|
||||
|
||||
if !dsc_path.exists() {
|
||||
return Err(format!("Could not find .dsc file at {}", dsc_path.display()).into());
|
||||
}
|
||||
Ok(dsc_path)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
async fn test_build_end_to_end(package: &str, series: &str, arch: Option<&str>, cross: bool) {
|
||||
log::info!(
|
||||
"Starting end-to-end test for package: {} (series: {}, arch: {:?}, cross: {})",
|
||||
package,
|
||||
series,
|
||||
arch,
|
||||
cross
|
||||
);
|
||||
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let cwd = temp_dir.path();
|
||||
log::debug!("Created temporary directory: {}", cwd.display());
|
||||
|
||||
log::info!("Pulling package {} from Ubuntu {}...", package, series);
|
||||
crate::pull::pull(
|
||||
package,
|
||||
"",
|
||||
Some(series),
|
||||
"",
|
||||
"",
|
||||
Some("ubuntu"),
|
||||
Some(cwd),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Cannot pull package");
|
||||
log::info!("Successfully pulled package {}", package);
|
||||
|
||||
// Change directory to the package directory
|
||||
let cwd = cwd.join(package).join(package);
|
||||
log::debug!("Package directory: {}", cwd.display());
|
||||
|
||||
log::info!("Starting binary package build...");
|
||||
crate::deb::build_binary_package(arch, Some(series), Some(&cwd), cross, None)
|
||||
.expect("Cannot build binary package (deb)");
|
||||
log::info!("Successfully built binary package");
|
||||
|
||||
// Check that the .deb files are present
|
||||
let parent_dir = cwd.parent().expect("Cannot find parent directory");
|
||||
let deb_files = std::fs::read_dir(parent_dir)
|
||||
.expect("Cannot read build directory")
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "deb"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
log::info!("Found {} .deb files after build", deb_files.len());
|
||||
for file in &deb_files {
|
||||
log::debug!(" - {}", file.path().display());
|
||||
}
|
||||
|
||||
assert!(!deb_files.is_empty(), "No .deb files found after build");
|
||||
log::info!(
|
||||
"End-to-end test completed successfully for package: {}",
|
||||
package
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE: We don't end-to-end test for a non-cross build for now.
|
||||
// This is an issue that we need to solve.
|
||||
// It seems that sbuild cannot by default be used inside of a (docker) container,
|
||||
// but our tests are currently running in one in CI.
|
||||
// TODO: Investigate on how to fix that.
|
||||
|
||||
#[tokio::test]
|
||||
#[test_log::test]
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
async fn test_deb_hello_ubuntu_cross_end_to_end() {
|
||||
test_build_end_to_end("hello", "noble", Some("riscv64"), true).await;
|
||||
}
|
||||
}
|
||||
35
src/deb/sbuild.rs
Normal file
35
src/deb/sbuild.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
/// Sbuild binary package building
|
||||
/// Call 'sbuild' with the dsc file to build the package with unshare
|
||||
use crate::context;
|
||||
use std::error::Error;
|
||||
|
||||
pub fn build(
|
||||
package: &str,
|
||||
_version: &str,
|
||||
arch: &str,
|
||||
series: &str,
|
||||
build_root: &str,
|
||||
cross: bool,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let ctx = context::current();
|
||||
let mut cmd = ctx.command("sbuild");
|
||||
cmd.current_dir(format!("{}/{}", build_root, package));
|
||||
cmd.arg("--chroot-mode=unshare");
|
||||
cmd.arg("--no-clean-source");
|
||||
|
||||
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.status()?;
|
||||
if !status.success() {
|
||||
return Err(format!("sbuild failed with status: {}", status).into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
12
src/lib.rs
12
src/lib.rs
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
47
src/main.rs
47
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,11 @@ 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 qemu-binfmt)")
|
||||
.long_help("Cross-compile for target architecture (instead of using qemu-binfmt)\nNote that most packages cannot be cross-compiled").required(false))
|
||||
.arg(arg!(--mode <mode> "Change build mode [sbuild, local]").required(false)
|
||||
.long_help("Change build mode [sbuild, local]\nDefault will chose depending on other parameters, don't provide if unsure")),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("context")
|
||||
@@ -71,7 +76,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")
|
||||
@@ -142,22 +150,23 @@ 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);
|
||||
let mode: Option<&str> = sub_matches.get_one::<String>("mode").map(|s| s.as_str());
|
||||
let mode: Option<pkh::deb::BuildMode> = match mode {
|
||||
Some("sbuild") => Some(pkh::deb::BuildMode::Sbuild),
|
||||
Some("local") => Some(pkh::deb::BuildMode::Local),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
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, mode)
|
||||
{
|
||||
error!("{}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
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)) => {
|
||||
@@ -168,7 +177,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 +200,7 @@ fn main() {
|
||||
})
|
||||
});
|
||||
|
||||
Context::Ssh { host, user, port }
|
||||
ContextConfig::Ssh { host, user, port }
|
||||
}
|
||||
_ => {
|
||||
error!("Unknown context type: {}", type_str);
|
||||
@@ -217,20 +226,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) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
59
src/pull.rs
59
src/pull.rs
@@ -262,22 +262,51 @@ async fn fetch_orig_tarball(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_dsc_file(
|
||||
info: &PackageInfo,
|
||||
cwd: Option<&Path>,
|
||||
progress: ProgressCallback<'_>,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let target_dir = cwd.unwrap_or_else(|| Path::new("."));
|
||||
|
||||
// Find the dsc file in the file list
|
||||
let dsc_file = info
|
||||
.stanza
|
||||
.files
|
||||
.iter()
|
||||
.find(|f| f.name.ends_with(".dsc"))
|
||||
.ok_or("Could not find .dsc file in package info")?;
|
||||
let filename = &dsc_file.name;
|
||||
|
||||
debug!("Fetching dsc file: {}", filename);
|
||||
|
||||
download_file_checksum(
|
||||
format!("{}/{}", &info.archive_url, filename).as_str(),
|
||||
&dsc_file.sha256,
|
||||
target_dir,
|
||||
progress,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_archive_sources(
|
||||
info: &PackageInfo,
|
||||
cwd: Option<&Path>,
|
||||
progress: ProgressCallback<'_>,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let package_dir = if let Some(path) = cwd {
|
||||
path.join(&info.stanza.package)
|
||||
path
|
||||
} else {
|
||||
Path::new(&info.stanza.package).to_path_buf()
|
||||
&Path::new(".").to_path_buf()
|
||||
};
|
||||
|
||||
std::fs::create_dir_all(&package_dir)?;
|
||||
std::fs::create_dir_all(package_dir)?;
|
||||
|
||||
for file in &info.stanza.files {
|
||||
let url = format!("{}/{}", info.archive_url, file.name);
|
||||
download_file_checksum(&url, &file.sha256, &package_dir, progress).await?;
|
||||
download_file_checksum(&url, &file.sha256, package_dir, progress).await?;
|
||||
}
|
||||
|
||||
// Extract the debian tarball or diff
|
||||
@@ -399,6 +428,7 @@ pub async fn pull(
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
clone_repo(
|
||||
url.as_str(),
|
||||
package,
|
||||
@@ -406,6 +436,7 @@ pub async fn pull(
|
||||
Some(&package_dir),
|
||||
progress,
|
||||
)?;
|
||||
|
||||
if !package_info.is_native() {
|
||||
if let Some(cb) = progress {
|
||||
cb("Fetching orig tarball...", "", 0, 0);
|
||||
@@ -414,12 +445,17 @@ pub async fn pull(
|
||||
} else {
|
||||
debug!("Native package, skipping orig tarball fetch.");
|
||||
}
|
||||
|
||||
if let Some(cb) = progress {
|
||||
cb("Fetching dsc file...", "", 0, 0);
|
||||
}
|
||||
fetch_dsc_file(&package_info, Some(&package_dir), progress).await?;
|
||||
} else {
|
||||
// Fallback to archive fetching
|
||||
if let Some(cb) = progress {
|
||||
cb("Downloading from archive...", "", 0, 0);
|
||||
}
|
||||
fetch_archive_sources(&package_info, Some(cwd.unwrap_or(Path::new("."))), progress).await?;
|
||||
fetch_archive_sources(&package_info, Some(&package_dir), progress).await?;
|
||||
}
|
||||
|
||||
Ok(package_info)
|
||||
@@ -480,18 +516,25 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for orig tarball in package dir
|
||||
// Check for orig tarball in package dir (only for non-native packages)
|
||||
let mut found_tarball = false;
|
||||
let mut found_dsc = false;
|
||||
for entry in std::fs::read_dir(package_dir).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if name.contains(".orig.tar.") {
|
||||
found_tarball = true;
|
||||
break;
|
||||
}
|
||||
if name.ends_with(".dsc") {
|
||||
found_dsc = true;
|
||||
}
|
||||
}
|
||||
|
||||
assert!(found_tarball, "Orig tarball not found in package dir");
|
||||
// Only check for orig tarball if the package is not native
|
||||
if !info.is_native() {
|
||||
assert!(found_tarball, "Orig tarball not found in package dir");
|
||||
}
|
||||
assert!(found_dsc, "DSC file not found in package dir");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user