use crate::dependencies::debian::DebianDependency;
use breezyshim::commit::PyCommitReporter;
use breezyshim::debian::debcommit::debcommit;
use breezyshim::error::Error as BrzError;
use breezyshim::tree::{MutableTree, Tree};
use breezyshim::workingtree::{GenericWorkingTree, WorkingTree};
pub use buildlog_consultant::sbuild::Phase;
use debian_analyzer::abstract_control::AbstractControlEditor;
use debian_analyzer::editor::{Editor, EditorError, Marshallable, MutableTreeEdit, TreeEditor};
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub enum Error {
CircularDependency(String),
MissingSource,
BrzError(BrzError),
EditorError(debian_analyzer::editor::EditorError),
IoError(std::io::Error),
InvalidField(String, String),
}
impl From<BrzError> for Error {
fn from(e: BrzError) -> Self {
Error::BrzError(e)
}
}
impl From<debian_analyzer::editor::EditorError> for Error {
fn from(e: debian_analyzer::editor::EditorError) -> Self {
Error::EditorError(e)
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::IoError(e)
}
}
impl From<Error> for crate::fix_build::InterimError<Error> {
fn from(e: Error) -> crate::fix_build::InterimError<Error> {
crate::fix_build::InterimError::Other(e)
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::CircularDependency(pkg) => write!(f, "Circular dependency on {}", pkg),
Error::MissingSource => write!(f, "No source stanza"),
Error::BrzError(e) => write!(f, "{}", e),
Error::EditorError(e) => write!(f, "{}", e),
Error::IoError(e) => write!(f, "{}", e),
Error::InvalidField(field, e) => write!(f, "Invalid field {}: {}", field, e),
}
}
}
impl std::error::Error for Error {}
pub struct DebianPackagingContext {
pub tree: GenericWorkingTree,
pub subpath: PathBuf,
pub committer: (String, String),
pub update_changelog: bool,
pub commit_reporter: Option<Box<dyn PyCommitReporter>>,
}
impl DebianPackagingContext {
pub fn new(
tree: GenericWorkingTree,
subpath: &Path,
committer: Option<(String, String)>,
update_changelog: bool,
commit_reporter: Option<Box<dyn PyCommitReporter>>,
) -> Self {
Self {
tree,
subpath: subpath.to_path_buf(),
committer: committer.unwrap_or_else(|| debian_changelog::get_maintainer().unwrap()),
update_changelog,
commit_reporter,
}
}
pub fn has_filename(&self, path: &Path) -> bool {
self.tree.has_filename(&self.subpath.join(path))
}
pub fn abspath(&self, path: &Path) -> PathBuf {
self.tree.abspath(&self.subpath.join(path)).unwrap()
}
pub fn edit_file<P: Marshallable>(
&self,
path: &std::path::Path,
allow_generated: bool,
) -> Result<TreeEditor<'_, P>, EditorError> {
let path = self.subpath.join(path);
self.tree.edit_file(&path, allow_generated, true)
}
pub fn commit(&self, summary: &str, update_changelog: Option<bool>) -> Result<bool, Error> {
let update_changelog = update_changelog.unwrap_or(self.update_changelog);
let committer = format!("{} <{}>", self.committer.0, self.committer.1);
let lock_write = self.tree.lock_write();
let r = if update_changelog {
let mut cl = self
.edit_file::<debian_changelog::ChangeLog>(Path::new("debian/changelog"), false)?;
cl.auto_add_change(&[summary], self.committer.clone(), Some(chrono::Local::now().into()), None);
cl.commit()?;
debcommit(
&self.tree,
Some(&committer),
&self.subpath,
None,
self.commit_reporter.as_deref(),
None,
)
} else {
let mut builder = self
.tree
.build_commit()
.message(summary)
.committer(&committer);
if !self.subpath.as_os_str().is_empty() {
builder = builder.specific_files(&[&self.subpath]);
}
if let Some(commit_reporter) = self.commit_reporter.as_ref() {
builder = builder.reporter(commit_reporter.as_ref());
}
builder.commit()
};
std::mem::drop(lock_write);
match r {
Ok(_) => Ok(true),
Err(BrzError::PointlessCommit) => Ok(false),
Err(e) => Err(e.into()),
}
}
pub fn add_dependency(
&self,
phase: &Phase,
requirement: &DebianDependency,
) -> Result<bool, Error> {
match phase {
Phase::AutoPkgTest(n) => self.add_test_dependency(n, requirement),
Phase::Build => self.add_build_dependency(requirement),
Phase::BuildEnv => {
log::warn!("Unknown phase {:?}", phase);
Ok(false)
}
Phase::CreateSession => {
log::warn!("Unknown phase {:?}", phase);
Ok(false)
}
}
}
pub fn edit_control<'a>(&'a self) -> Result<Box<dyn AbstractControlEditor + 'a>, Error> {
if self
.tree
.has_filename(&self.subpath.join("debian/debcargo.toml"))
{
Ok(Box::new(
debian_analyzer::debcargo::DebcargoEditor::from_directory(
&self.tree.abspath(&self.subpath).unwrap(),
)?,
))
} else {
let control_path = Path::new("debian/control");
Ok(
Box::new(self.edit_file::<debian_control::Control>(control_path, false)?)
as Box<dyn AbstractControlEditor>,
)
}
}
fn add_build_dependency(&self, requirement: &DebianDependency) -> Result<bool, Error> {
assert!(!requirement.is_empty());
let mut control = self.edit_control()?;
for binary in control.binaries() {
if requirement.touches_package(&binary.name().unwrap()) {
return Err(Error::CircularDependency(binary.name().unwrap()));
}
}
let mut source = if let Some(source) = control.source() {
source
} else {
return Err(Error::MissingSource);
};
for rel in requirement.iter() {
source.ensure_build_dep(rel);
}
std::mem::drop(source);
let desc = requirement.relation_string();
if !control.commit() {
log::info!("Giving up; build dependency {} was already present.", desc);
return Ok(false);
}
log::info!("Adding build dependency: {}", desc);
self.commit(&format!("Add missing build dependency on {}.", desc), None)?;
Ok(true)
}
pub fn edit_tests_control(&self) -> Result<TreeEditor<'_, deb822_lossless::Deb822>, Error> {
Ok(self.edit_file::<deb822_lossless::Deb822>(Path::new("debian/tests/control"), false)?)
}
pub fn edit_rules(&self) -> Result<TreeEditor<'_, makefile_lossless::Makefile>, Error> {
Ok(self.edit_file::<makefile_lossless::Makefile>(Path::new("debian/rules"), false)?)
}
fn add_test_dependency(
&self,
testname: &str,
requirement: &DebianDependency,
) -> Result<bool, Error> {
let editor = self.edit_tests_control()?;
let mut command_counter = 1;
for mut para in editor.paragraphs() {
let name = para.get("Tests").unwrap_or_else(|| {
let name = format!("command{}", command_counter);
command_counter += 1;
name
});
if name != testname {
continue;
}
for rel in requirement.iter() {
let depends = para.get("Depends").unwrap_or_default();
let mut rels: debian_control::lossless::relations::Relations =
depends.parse().map_err(|e| {
Error::InvalidField(format!("Test Depends for {}", testname), e)
})?;
debian_analyzer::relations::ensure_relation(&mut rels, rel);
para.insert("Depends", &rels.to_string());
}
}
let desc = requirement.relation_string();
if editor.commit()?.is_empty() {
log::info!(
"Giving up; dependency {} for test {} was already present.",
desc,
testname,
);
return Ok(false);
}
log::info!("Adding dependency to test {}: {}", testname, desc);
self.commit(
&format!("Add missing dependency for test {} on {}.", testname, desc),
None,
)?;
Ok(true)
}
}
#[cfg(test)]
mod tests {
use super::*;
use breezyshim::controldir::{create_standalone_workingtree, ControlDirFormat};
pub const COMMITTER: &str = "ognibuild <ognibuild@example.com>";
fn setup(path: &Path) -> DebianPackagingContext {
let tree = create_standalone_workingtree(path, &ControlDirFormat::default()).unwrap();
std::fs::create_dir_all(path.join("debian")).unwrap();
std::fs::write(
path.join("debian/control"),
r###"Source: blah
Build-Depends: libc6
Package: python-blah
Depends: ${python3:Depends}
Description: A python package
Foo
"###,
)
.unwrap();
std::fs::write(
path.join("debian/changelog"),
r###"blah (0.1) UNRELEASED; urgency=medium
* Initial release. (Closes: #XXXXXX)
-- Jelmer Vernooij <jelmer@debian.org> Sat, 04 Apr 2020 14:12:13 +0000
"###,
)
.unwrap();
tree.add(&[
Path::new("debian"),
Path::new("debian/control"),
Path::new("debian/changelog"),
])
.unwrap();
tree.build_commit()
.message("Initial commit")
.committer(COMMITTER)
.commit()
.unwrap();
DebianPackagingContext::new(
tree,
Path::new(""),
Some(("ognibuild".to_owned(), "<ognibuild@jelmer.uk>".to_owned())),
false,
Some(Box::new(breezyshim::commit::NullCommitReporter::new())),
)
}
#[test]
fn test_already_present() {
let td = tempfile::tempdir().unwrap();
let context = setup(td.path());
let dep = DebianDependency::simple("libc6");
assert!(!context.add_build_dependency(&dep).unwrap());
}
#[test]
fn test_basic() {
let td = tempfile::tempdir().unwrap();
let context = setup(td.path());
let dep = DebianDependency::simple("foo");
assert!(context.add_build_dependency(&dep).unwrap());
let control = std::fs::read_to_string(td.path().join("debian/control")).unwrap();
assert_eq!(
control,
r###"Source: blah
Build-Depends: foo, libc6
Package: python-blah
Depends: ${python3:Depends}
Description: A python package
Foo
"###
);
}
#[test]
fn test_circular() {
let td = tempfile::tempdir().unwrap();
let context = setup(td.path());
let dep = DebianDependency::simple("python-blah");
assert!(matches!(
context.add_build_dependency(&dep),
Err(Error::CircularDependency(_))
));
}
}