deb: cross-compilation, ephemeral contexts, local builds
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:
2025-12-25 17:10:44 +00:00
committed by vhaudiquet
parent 88313b0c51
commit 1538e9ee19
19 changed files with 1784 additions and 301 deletions
+91 -41
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;
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()
}
}