use breezyshim::workingtree::PyWorkingTree;
use buildlog_consultant::Problem;
use debian_changelog::{ChangeLog, Urgency};
use debversion::Version;
use std::io::Seek;
use std::path::{Path, PathBuf};
pub fn get_build_architecture() -> String {
std::process::Command::new("dpkg-architecture")
.arg("-qDEB_BUILD_ARCH")
.output()
.map(|output| String::from_utf8(output.stdout).unwrap().trim().to_string())
.unwrap()
}
pub const DEFAULT_BUILDER: &str = "sbuild --no-clean-source";
fn python_command() -> String {
pyo3::Python::attach(|py| {
use pyo3::types::PyAnyMethods;
let sys_module = py.import("sys").unwrap();
sys_module
.getattr("executable")
.unwrap()
.extract::<String>()
.unwrap()
})
}
pub fn builddeb_command(
build_command: Option<&str>,
result_dir: Option<&std::path::Path>,
apt_repository: Option<&str>,
apt_repository_key: Option<&str>,
extra_repositories: Option<&Vec<&str>>,
) -> Vec<String> {
let mut build_command = build_command.unwrap_or(DEFAULT_BUILDER).to_string();
if let Some(extra_repositories) = extra_repositories {
for repo in extra_repositories {
build_command.push_str(&format!(
" --extra-repository={}",
shlex::try_quote(repo).unwrap()
));
}
}
let mut args = vec![
python_command(),
"-m".to_string(),
"breezy".to_string(),
"builddeb".to_string(),
"--guess-upstream-branch-url".to_string(),
format!("--builder={}", build_command),
];
if let Some(apt_repository) = apt_repository {
args.push(format!("--apt-repository={}", apt_repository));
}
if let Some(apt_repository_key) = apt_repository_key {
args.push(format!("--apt-repository-key={}", apt_repository_key));
}
if let Some(result_dir) = result_dir {
args.push(format!("--result-dir={}", result_dir.to_string_lossy()));
}
args
}
#[derive(Debug)]
pub struct BuildFailedError(pub i32);
impl std::fmt::Display for BuildFailedError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Build failed: {}", self.0)
}
}
impl std::error::Error for BuildFailedError {}
pub fn build(
local_tree: &dyn PyWorkingTree,
outf: std::fs::File,
build_command: &str,
result_dir: Option<&std::path::Path>,
distribution: Option<&str>,
subpath: &std::path::Path,
source_date_epoch: Option<chrono::DateTime<chrono::Utc>>,
apt_repository: Option<&str>,
apt_repository_key: Option<&str>,
extra_repositories: Option<&Vec<&str>>,
) -> Result<(), BuildFailedError> {
let args = builddeb_command(
Some(build_command),
result_dir,
apt_repository,
apt_repository_key,
extra_repositories,
);
let mut env = std::env::vars().collect::<std::collections::HashMap<_, _>>();
if let Some(distribution) = distribution {
env.insert("DISTRIBUTION".to_owned(), distribution.to_owned());
}
if let Some(source_date_epoch) = source_date_epoch {
env.insert(
"SOURCE_DATE_EPOCH".to_owned(),
format!("{}", source_date_epoch.timestamp()),
);
}
log::info!("Building debian packages, running {}.", build_command);
match std::process::Command::new(&args[0])
.args(&args[1..])
.current_dir(local_tree.abspath(subpath).unwrap())
.stdout(outf.try_clone().unwrap())
.stderr(outf)
.envs(env)
.status()
{
Ok(status) => {
if status.success() {
log::info!("Build succeeded.");
Ok(())
} else {
Err(BuildFailedError(status.code().unwrap_or(1)))
}
}
Err(e) => {
log::error!("Failed to run build command: {}", e);
Err(BuildFailedError(1))
}
}
}
pub const BUILD_LOG_FILENAME: &str = "build.log";
#[derive(Debug)]
pub enum BuildOnceError {
Detailed {
stage: Option<String>,
phase: Option<buildlog_consultant::sbuild::Phase>,
retcode: i32,
command: Vec<String>,
error: Box<dyn Problem>,
description: String,
},
Unidentified {
stage: Option<String>,
phase: Option<buildlog_consultant::sbuild::Phase>,
retcode: i32,
command: Vec<String>,
description: String,
},
}
pub struct BuildOnceResult {
pub source_package: String,
pub version: debversion::Version,
pub changes_names: Vec<PathBuf>,
}
pub fn build_once(
local_tree: &dyn PyWorkingTree,
build_suite: Option<&str>,
output_directory: &Path,
build_command: &str,
subpath: &Path,
source_date_epoch: Option<chrono::DateTime<chrono::Utc>>,
apt_repository: Option<&str>,
apt_repository_key: Option<&str>,
extra_repositories: Option<&Vec<&str>>,
) -> Result<BuildOnceResult, BuildOnceError> {
use buildlog_consultant::problems::debian::DpkgSourceLocalChanges;
use buildlog_consultant::sbuild::{worker_failure_from_sbuild_log, SbuildLog};
let build_log_path = output_directory.join(BUILD_LOG_FILENAME);
log::debug!("Writing build log to {}", build_log_path.display());
let mut logf = std::fs::File::create(&build_log_path).unwrap();
match build(
local_tree,
logf.try_clone().unwrap(),
build_command,
Some(output_directory),
build_suite,
subpath,
source_date_epoch,
apt_repository,
apt_repository_key,
extra_repositories,
) {
Ok(()) => (),
Err(e) => {
logf.sync_all().unwrap();
logf.seek(std::io::SeekFrom::Start(0)).unwrap();
let sbuildlog =
SbuildLog::try_from(std::fs::File::open(&build_log_path).unwrap()).unwrap();
let sbuild_failure = worker_failure_from_sbuild_log(&sbuildlog);
if let Some(error) = sbuild_failure
.error
.as_ref()
.and_then(|e| e.as_any().downcast_ref::<DpkgSourceLocalChanges>())
{
if let Some(diff_file) = error.diff_file.as_ref() {
let diff_file_name =
output_directory.join(Path::new(&diff_file).file_name().unwrap());
std::fs::copy(diff_file, &diff_file_name).unwrap();
}
}
let retcode = e.0;
if let Some(error) = sbuild_failure.error {
return Err(BuildOnceError::Detailed {
stage: sbuild_failure.stage,
phase: sbuild_failure.phase,
retcode,
command: shlex::split(build_command).unwrap(),
error,
description: sbuild_failure.description.unwrap_or_default(),
});
} else {
return Err(BuildOnceError::Unidentified {
stage: sbuild_failure.stage,
phase: sbuild_failure.phase,
retcode,
command: shlex::split(build_command).unwrap(),
description: sbuild_failure
.description
.unwrap_or_else(|| format!("Build failed with exit code {}", retcode)),
});
}
}
}
let (package, version) = get_last_changelog_entry(local_tree, subpath);
let mut changes_names = vec![];
for (_kind, entry) in find_changes_files(output_directory, &package, &version) {
changes_names.push(entry.path());
}
Ok(BuildOnceResult {
source_package: package,
version,
changes_names,
})
}
fn control_files_in_root(tree: &dyn PyWorkingTree, subpath: &std::path::Path) -> bool {
let debian_path = subpath.join("debian");
if tree.has_filename(&debian_path) {
return false;
}
let control_path = subpath.join("control");
if tree.has_filename(&control_path) {
return true;
}
tree.has_filename(std::path::Path::new(&format!(
"{}.in",
control_path.to_string_lossy()
)))
}
fn get_last_changelog_entry(
local_tree: &dyn PyWorkingTree,
subpath: &std::path::Path,
) -> (String, debversion::Version) {
let path = if control_files_in_root(local_tree, subpath) {
subpath.join("changelog")
} else {
subpath.join("debian/changelog")
};
let f = local_tree.get_file(&path).unwrap();
let cl = ChangeLog::read_relaxed(f).unwrap();
let e = cl.iter().next().unwrap();
(e.package().unwrap(), e.version().unwrap())
}
pub fn gbp_dch(path: &std::path::Path) -> Result<(), std::io::Error> {
let cmd = std::process::Command::new("gbp-dch")
.arg("--ignore-branch")
.current_dir(path)
.output()?;
if !cmd.status.success() {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"gbp-dch failed",
));
}
Ok(())
}
pub fn find_changes_files(
path: &std::path::Path,
package: &str,
version: &debversion::Version,
) -> impl Iterator<Item = (String, std::fs::DirEntry)> {
let mut non_epoch_version = version.upstream_version.to_string();
if let Some(debian_version) = version.debian_revision.as_ref() {
non_epoch_version.push_str(&format!("-{}", debian_version));
}
let regex = format!(
"{}_{}_(.*)",
regex::escape(package),
regex::escape(&non_epoch_version)
);
let c = regex::Regex::new(®ex).unwrap();
std::fs::read_dir(path).unwrap().filter_map(move |entry| {
let entry = entry.unwrap();
c.captures(entry.file_name().to_str().unwrap())
.map(|m| (m.get(1).unwrap().as_str().to_owned(), entry))
})
}
pub fn attempt_build(
local_tree: &dyn PyWorkingTree,
suffix: Option<&str>,
build_suite: Option<&str>,
output_directory: &std::path::Path,
build_command: &str,
build_changelog_entry: Option<&str>,
subpath: &std::path::Path,
source_date_epoch: Option<chrono::DateTime<chrono::Utc>>,
run_gbp_dch: bool,
apt_repository: Option<&str>,
apt_repository_key: Option<&str>,
extra_repositories: Option<&Vec<&str>>,
) -> Result<BuildOnceResult, BuildOnceError> {
if run_gbp_dch
&& subpath == std::path::Path::new("")
&& local_tree
.abspath(std::path::Path::new(".git"))
.map_or(false, |p| std::path::Path::new(&p).exists())
{
gbp_dch(&local_tree.abspath(subpath).unwrap()).unwrap();
}
if let Some(build_changelog_entry) = build_changelog_entry {
assert!(
suffix.is_some(),
"build_changelog_entry specified, but suffix is None"
);
assert!(
build_suite.is_some(),
"build_changelog_entry specified, but build_suite is None"
);
add_dummy_changelog_entry(
local_tree,
subpath,
suffix.unwrap(),
build_suite.unwrap(),
build_changelog_entry,
None,
None,
);
}
build_once(
local_tree,
build_suite,
output_directory,
build_command,
subpath,
source_date_epoch,
apt_repository,
apt_repository_key,
extra_repositories,
)
}
pub fn version_add_suffix(version: &Version, suffix: &str) -> Version {
fn add_suffix(v: &str, suffix: &str) -> String {
if let Some(m) = regex::Regex::new(&format!("(.*)({})([0-9]+)", regex::escape(suffix)))
.unwrap()
.captures(v)
{
let main = m.get(1).unwrap().as_str();
let suffix = m.get(2).unwrap().as_str();
let revision = m.get(3).unwrap().as_str();
format!("{}{}{}", main, suffix, revision.parse::<u64>().unwrap() + 1)
} else {
format!("{}{}1", v, suffix)
}
}
let mut version = version.clone();
if let Some(r) = version.debian_revision {
version.debian_revision = Some(add_suffix(&r, suffix));
} else {
version.upstream_version = add_suffix(&version.upstream_version, suffix);
}
version
}
pub fn add_dummy_changelog_entry(
tree: &dyn PyWorkingTree,
subpath: &Path,
suffix: &str,
suite: &str,
message: &str,
timestamp: Option<chrono::DateTime<chrono::Utc>>,
maintainer: Option<(String, String)>,
) -> Version {
let path = if control_files_in_root(tree, subpath) {
subpath.join("changelog")
} else {
subpath.join("debian/changelog")
};
let mut cl = ChangeLog::read_relaxed(tree.get_file(&path).unwrap()).unwrap();
let prev_entry = cl.iter().next().unwrap();
let prev_version = prev_entry.version().unwrap();
let version = version_add_suffix(&prev_version, suffix);
log::debug!("Adding dummy changelog entry {} for build", &version);
let mut entry = cl.auto_add_change(
&[&format!("* {}", message)],
maintainer.unwrap_or_else(|| debian_changelog::get_maintainer().unwrap()),
timestamp.map(|t| t.into()),
Some(Urgency::Low),
);
entry.set_version(&version);
entry.set_distributions(vec![suite.to_string()]);
tree.put_file_bytes_non_atomic(&path, cl.to_string().as_bytes())
.unwrap();
entry.version().unwrap()
}
#[cfg(test)]
mod tests {
use super::*;
use breezyshim::tree::MutableTree;
use std::fs::File;
use tempfile::tempdir;
#[test]
#[cfg(target_os = "linux")]
fn test_get_build_architecture() {
let arch = get_build_architecture();
assert!(!arch.is_empty() && arch.len() < 10);
}
#[test]
#[cfg(not(target_os = "linux"))]
#[should_panic(expected = "No such file or directory")]
fn test_get_build_architecture_panics_on_non_linux() {
let _ = get_build_architecture();
}
#[test]
fn test_build_fails_with_invalid_command() {
let td = tempdir().unwrap();
let tree = breezyshim::controldir::create_standalone_workingtree(
td.path(),
&breezyshim::controldir::ControlDirFormat::default(),
)
.unwrap();
let log_file = File::create(td.path().join("build.log")).unwrap();
let result = build(
&tree,
log_file,
"command_that_does_not_exist",
None,
None,
Path::new(""),
None,
None,
None,
None,
);
assert!(result.is_err());
if let Err(BuildFailedError(code)) = result {
assert!(code > 0);
} else {
panic!("Expected BuildFailedError");
}
}
#[test]
fn test_build_once_error_conversion() {
let td = tempdir().unwrap();
let tree = breezyshim::controldir::create_standalone_workingtree(
td.path(),
&breezyshim::controldir::ControlDirFormat::default(),
)
.unwrap();
std::fs::create_dir_all(td.path().join("debian")).unwrap();
std::fs::write(
td.path().join("debian/changelog"),
r#"test-package (1.0) unstable; urgency=low
* Initial release
-- Test User <test@example.com> Thu, 01 Jan 2023 00:00:00 +0000
"#,
)
.unwrap();
tree.add(&[Path::new("debian"), Path::new("debian/changelog")])
.unwrap();
let output_dir = tempdir().unwrap();
let result = build_once(
&tree,
None,
output_dir.path(),
"command_that_does_not_exist",
Path::new(""),
None,
None,
None,
None,
);
assert!(result.is_err());
match result {
Err(BuildOnceError::Unidentified { .. }) | Err(BuildOnceError::Detailed { .. }) => {
}
_ => panic!("Expected Unidentified or Detailed BuildOnceError"),
}
}
#[test]
fn test_builddeb_command() {
let command = builddeb_command(
Some("sbuild --no-clean-source"),
Some(std::path::Path::new("/tmp")),
Some("ppa:my-ppa/ppa"),
Some("my-ppa-key"),
Some(&vec!["deb http://example.com/debian buster main"]),
);
assert_eq!(command, vec![
python_command(),
"-m".to_string(),
"breezy".to_string(),
"builddeb".to_string(),
"--guess-upstream-branch-url".to_string(),
"--builder=sbuild --no-clean-source --extra-repository='deb http://example.com/debian buster main'".to_string(),
"--apt-repository=ppa:my-ppa/ppa".to_string(),
"--apt-repository-key=my-ppa-key".to_string(),
"--result-dir=/tmp".to_string(),
]);
}
#[test]
fn test_python_command() {
let _ = python_command();
}
#[test]
fn test_control_files_not_in_root() {
let td = tempfile::tempdir().unwrap();
let tree = breezyshim::controldir::create_standalone_workingtree(
td.path(),
&breezyshim::controldir::ControlDirFormat::default(),
)
.unwrap();
let subpath = std::path::Path::new("");
tree.mkdir(&subpath.join("debian")).unwrap();
tree.put_file_bytes_non_atomic(&subpath.join("debian/control"), b"")
.unwrap();
assert!(!control_files_in_root(&tree, subpath));
}
#[test]
fn test_control_files_in_root() {
let td = tempfile::tempdir().unwrap();
let tree = breezyshim::controldir::create_standalone_workingtree(
td.path(),
&breezyshim::controldir::ControlDirFormat::default(),
)
.unwrap();
let subpath = std::path::Path::new("");
tree.put_file_bytes_non_atomic(&subpath.join("control"), b"")
.unwrap();
assert!(control_files_in_root(&tree, subpath));
}
mod test_version_add_suffix {
use super::*;
#[test]
fn test_native() {
assert_eq!(
"1.0~jan+lint4".parse::<Version>().unwrap(),
version_add_suffix(&"1.0~jan+lint3".parse().unwrap(), "~jan+lint"),
);
assert_eq!(
"1.0~jan+lint1".parse::<Version>().unwrap(),
version_add_suffix(&"1.0".parse().unwrap(), "~jan+lint"),
);
}
#[test]
fn test_normal() {
assert_eq!(
"1.0-1~jan+lint4".parse::<Version>().unwrap(),
version_add_suffix(&"1.0-1~jan+lint3".parse().unwrap(), "~jan+lint"),
);
assert_eq!(
"1.0-1~jan+lint1".parse::<Version>().unwrap(),
version_add_suffix(&"1.0-1".parse().unwrap(), "~jan+lint"),
);
assert_eq!(
"0.0.12-1~jan+lint1".parse::<Version>().unwrap(),
version_add_suffix(&"0.0.12-1".parse().unwrap(), "~jan+lint"),
);
assert_eq!(
"0.0.12-1~jan+unchanged1~jan+lint1"
.parse::<Version>()
.unwrap(),
version_add_suffix(&"0.0.12-1~jan+unchanged1".parse().unwrap(), "~jan+lint"),
);
}
}
mod test_add_dummy_changelog {
use super::*;
use std::path::Path;
#[test]
fn test_simple() {
let td = tempfile::tempdir().unwrap();
let tree = breezyshim::controldir::create_standalone_workingtree(
td.path(),
&breezyshim::controldir::ControlDirFormat::default(),
)
.unwrap();
std::fs::create_dir(td.path().join("debian")).unwrap();
std::fs::write(
td.path().join("debian/changelog"),
r#"janitor (0.1-1) UNRELEASED; urgency=medium
* Initial release. (Closes: #XXXXXX)
-- Jelmer Vernooij <jelmer@debian.org> Sat, 05 Sep 2020 12:35:04 -0000
"#,
)
.unwrap();
tree.add(&[Path::new("debian"), Path::new("debian/changelog")])
.unwrap();
add_dummy_changelog_entry(
&tree,
Path::new(""),
"jan+some",
"some-fixes",
"Dummy build.",
Some(
chrono::DateTime::parse_from_rfc3339("2020-09-05T12:35:04Z")
.unwrap()
.to_utc(),
),
Some(("Jelmer Vernooij".to_owned(), "jelmer@debian.org".to_owned())),
);
let contents = std::fs::read_to_string(td.path().join("debian/changelog")).unwrap();
assert_eq!(
r#"janitor (0.1-1jan+some1) some-fixes; urgency=medium
* Initial release. (Closes: #XXXXXX)
* Dummy build.
-- Jelmer Vernooij <jelmer@debian.org> Sat, 05 Sep 2020 12:35:04 -0000
"#,
contents
);
}
#[test]
fn test_native() {
let td = tempfile::tempdir().unwrap();
let tree = breezyshim::controldir::create_standalone_workingtree(
td.path(),
&breezyshim::controldir::ControlDirFormat::default(),
)
.unwrap();
std::fs::create_dir(td.path().join("debian")).unwrap();
std::fs::write(
td.path().join("debian/changelog"),
r#"janitor (0.1) UNRELEASED; urgency=medium
* Initial release. (Closes: #XXXXXX)
-- Jelmer Vernooij <jelmer@debian.org> Sat, 05 Sep 2020 12:35:04 -0000
"#,
)
.unwrap();
tree.add(&[Path::new("debian"), Path::new("debian/changelog")])
.unwrap();
add_dummy_changelog_entry(
&tree,
Path::new(""),
"jan+some",
"some-fixes",
"Dummy build.",
Some(
chrono::DateTime::parse_from_rfc3339("2020-09-05T12:35:04Z")
.unwrap()
.to_utc(),
),
Some(("Jelmer Vernooij".to_owned(), "jelmer@debian.org".to_owned())),
);
let contents = std::fs::read_to_string(td.path().join("debian/changelog")).unwrap();
assert_eq!(
r#"janitor (0.1jan+some1) some-fixes; urgency=medium
* Initial release. (Closes: #XXXXXX)
* Dummy build.
-- Jelmer Vernooij <jelmer@debian.org> Sat, 05 Sep 2020 12:35:04 -0000
"#,
contents
);
}
#[test]
fn test_exists() {
let td = tempfile::tempdir().unwrap();
let tree = breezyshim::controldir::create_standalone_workingtree(
td.path(),
&breezyshim::controldir::ControlDirFormat::default(),
)
.unwrap();
std::fs::create_dir(td.path().join("debian")).unwrap();
std::fs::write(
td.path().join("debian/changelog"),
r#"janitor (0.1-1jan+some1) UNRELEASED; urgency=medium
* Initial release. (Closes: #XXXXXX)
-- Jelmer Vernooij <jelmer@debian.org> Sat, 05 Sep 2020 12:35:04 -0000
"#,
)
.unwrap();
tree.add(&[Path::new("debian"), Path::new("debian/changelog")])
.unwrap();
add_dummy_changelog_entry(
&tree,
Path::new(""),
"jan+some",
"some-fixes",
"Dummy build.",
Some(
chrono::DateTime::parse_from_rfc3339("2020-09-05T12:35:04Z")
.unwrap()
.to_utc(),
),
Some(("Jelmer Vernooij".to_owned(), "jelmer@debian.org".to_owned())),
);
let contents = std::fs::read_to_string(td.path().join("debian/changelog")).unwrap();
assert_eq!(
r#"janitor (0.1-1jan+some2) some-fixes; urgency=medium
* Initial release. (Closes: #XXXXXX)
* Dummy build.
-- Jelmer Vernooij <jelmer@debian.org> Sat, 05 Sep 2020 12:35:04 -0000
"#,
contents
);
}
}
}