use crate::names::upstream_package_to_debian_source_name;
use crate::Error;
use breezyshim::branch::Branch;
use breezyshim::workingtree::PyWorkingTree;
use debian_analyzer::debhelper::maximum_debhelper_compat_version;
use debian_analyzer::editor::{Editor, TreeEditor};
use debian_analyzer::lintian::latest_standards_version;
use debian_analyzer::relations::{ensure_exact_version, ensure_relation, ensure_some_version};
use debian_control::fields::MultiArch;
use debian_control::lossless::relations::Relations;
use debian_control::lossless::{Control, Source};
use debversion::Version;
use ognibuild::buildsystem::BuildSystem;
use ognibuild::debian::upstream_deps::get_project_wide_deps;
use ognibuild::session::Session;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use upstream_ontologist::UpstreamMetadata;
use url::Url;
struct ProcessorContext<'a> {
session: &'a dyn Session,
wt: &'a dyn PyWorkingTree,
subpath: PathBuf,
debian_path: PathBuf,
upstream_version: String,
metadata: &'a UpstreamMetadata,
compat_release: String,
buildsystem: Box<dyn BuildSystem>,
_buildsystem_subpath: PathBuf,
maintainer: Option<String>,
_kickstart_from_dist: Option<Box<dyn FnOnce(&dyn PyWorkingTree, &Path) -> Result<(), Error>>>,
}
impl<'a> ProcessorContext<'a> {
fn kickstart_tree(&mut self, sourceful: bool) -> Result<(), Error> {
if sourceful {
if let Some(kickstart_fn) = self._kickstart_from_dist.take() {
kickstart_fn(self.wt, &self.subpath)?;
} else {
log::debug!("No kickstart_from_dist function provided, skipping dist import");
}
} else {
prepare_empty_subpath(self.wt, &self.subpath)?;
}
Ok(())
}
fn create_control_file(&self) -> Result<TreeEditor<'_, Control>, Error> {
Ok(TreeEditor::<Control>::new(
self.wt,
&self.debian_path.join("control"),
false,
true,
)?)
}
fn bootstrap_debhelper(
&self,
source: &mut Source,
config: DebhelperConfig,
) -> Result<(), Error> {
bootstrap_debhelper(
self.wt,
&self.debian_path,
source,
&self.compat_release,
config,
)
}
fn get_project_wide_deps(&self) -> (Relations, Relations) {
let (build_deps, test_deps) =
get_project_wide_deps(self.session, self.buildsystem.as_ref());
let mut build_ret = Relations::new();
for dep in build_deps {
let rs: Relations = dep.into();
for rel in rs.entries() {
ensure_relation(&mut build_ret, rel);
}
}
let mut test_ret = Relations::new();
for dep in test_deps {
let rs: Relations = dep.into();
for rel in rs.entries() {
ensure_relation(&mut test_ret, rel);
}
}
(build_ret, test_ret)
}
}
fn prepare_empty_subpath(wt: &dyn PyWorkingTree, subpath: &Path) -> Result<(), Error> {
wt.branch()
.generate_revision_history(&breezyshim::RevisionId::null())
.unwrap();
if !wt.has_filename(subpath) {
wt.mkdir(subpath)?;
}
if !wt.is_versioned(subpath) {
wt.add(&[subpath])?;
}
Ok(())
}
fn enable_dh_addon(source: &mut Source, addon: &str) {
let mut build_depends = source.build_depends().unwrap_or_default();
ensure_some_version(&mut build_depends, &format!("dh-sequence-{}", addon));
source.set_build_depends(&build_depends);
}
fn import_build_deps(source: &mut Source, new_build_deps: &Relations) {
let mut build_depends = source.build_depends().unwrap_or_default();
for build_dep in new_build_deps.entries() {
for rel in build_dep.relations() {
ensure_relation(&mut build_depends, rel.into());
}
}
source.set_build_depends(&build_depends);
}
fn debhelper_rules<F: std::io::Write>(
f: &mut F,
buildsystem: Option<&str>,
build_directory: Option<&str>,
env: HashMap<&str, &str>,
) -> std::io::Result<()> {
f.write_all(b"#!/usr/bin/make -f\n")?;
f.write_all(b"%:\n")?;
f.write_all(b"\tdh $@")?;
if let Some(buildsystem) = buildsystem {
f.write_all(format!(" --buildsystem={}", buildsystem).as_bytes())?;
}
if let Some(build_directory) = build_directory {
f.write_all(format!(" --builddirectory={}", build_directory).as_bytes())?;
}
f.write_all(b"\n")?;
for (key, value) in env {
f.write_all(format!("export {}={}\n", key, value).as_bytes())?;
}
Ok(())
}
#[derive(Debug, Default)]
struct DebhelperConfig<'a> {
addons: Vec<&'a str>,
env: HashMap<&'a str, &'a str>,
buildsystem: Option<&'a str>,
build_directory: Option<&'a str>,
}
fn bootstrap_debhelper(
wt: &dyn PyWorkingTree,
debian_path: &Path,
source: &mut Source,
compat_release: &str,
config: DebhelperConfig,
) -> Result<(), Error> {
let mut build_depends = source.build_depends().unwrap_or_default();
ensure_exact_version(
&mut build_depends,
"debhelper-compat",
&maximum_debhelper_compat_version(compat_release)
.to_string()
.parse::<Version>()
.unwrap(),
None,
);
source.set_build_depends(&build_depends);
for addon in config.addons.iter() {
enable_dh_addon(source, addon);
}
let mut f = Vec::new();
debhelper_rules(
&mut f,
config.buildsystem,
config.build_directory,
config.env,
)?;
wt.put_file_bytes_non_atomic(&debian_path.join("rules"), &f)?;
Ok(())
}
fn process_setup_py(context: &mut ProcessorContext) -> Result<(), Error> {
context.kickstart_tree(true)?;
let mut control = context.create_control_file()?;
let upstream_name = match context.metadata.name() {
Some(name) => name,
None => {
return Err(Error::MissingUpstreamInfo(
"unable to determine name from setup.py for the python3 project".to_string(),
))
}
};
let source_name = crate::names::python_source_package_name(upstream_name);
let mut source = control.add_source(&source_name);
if let Some(ref maintainer) = context.maintainer {
source.set_maintainer(maintainer);
}
source.set_section(Some("python"));
source.set_rules_requires_root(false);
source.set_standards_version(&latest_standards_version().to_string());
context.bootstrap_debhelper(
&mut source,
DebhelperConfig {
buildsystem: Some("pybuild"),
addons: vec!["python3"],
..Default::default()
},
)?;
source.set_testsuite("autopkgtest-pkg-python");
let python3_support = check_python3_support(context.wt, &context.subpath)?;
if !python3_support {
log::warn!("Project may not support Python 3, but proceeding with Python 3 packaging");
}
let mut build_depends = source.build_depends().unwrap_or_default();
ensure_relation(&mut build_depends, "python3-all".parse().unwrap());
if context
.wt
.has_filename(&context.subpath.join("pyproject.toml"))
{
ensure_relation(
&mut build_depends,
"pybuild-plugin-pyproject".parse().unwrap(),
);
}
if context.wt.has_filename(&context.subpath.join("setup.py")) {
ensure_relation(&mut build_depends, "python3-setuptools".parse().unwrap());
}
source.set_build_depends(&build_depends);
let (build_deps, test_deps) = context.get_project_wide_deps();
import_build_deps(&mut source, &build_deps);
import_build_deps(&mut source, &test_deps);
let binary_name = crate::names::python_binary_package_name(upstream_name);
let mut binary = control.add_binary(&binary_name);
binary.set_architecture(Some("all"));
binary
.as_mut_deb822()
.insert("Depends", "${python3:Depends}");
control.commit()?;
Ok(())
}
fn process_maven(context: &mut ProcessorContext) -> Result<(), Error> {
context.kickstart_tree(true)?;
let mut control = context.create_control_file()?;
let upstream_name = match context.metadata.name() {
Some(name) => name,
None => {
return Err(Error::MissingUpstreamInfo(
"unable to determine the artifactId from pom.xml for the java project".to_string(),
))
}
};
let source_name = crate::names::upstream_name_to_debian_source_name(upstream_name)
.unwrap_or_else(|| upstream_name.to_string());
let mut source = control.add_source(&source_name);
if let Some(ref maintainer) = context.maintainer {
source.set_maintainer(maintainer);
}
source.set_section(Some("java"));
source.set_rules_requires_root(false);
source.set_standards_version(&latest_standards_version().to_string());
context.bootstrap_debhelper(
&mut source,
DebhelperConfig {
buildsystem: Some("maven"),
..Default::default()
},
)?;
let (build_deps, _test_deps) = context.get_project_wide_deps();
import_build_deps(&mut source, &build_deps);
let mut binary = control.add_binary(&format!("lib{}-java", source_name));
binary.set_architecture(Some("all"));
binary.as_mut_deb822().insert("Depends", "${java:Depends}");
control.commit()?;
Ok(())
}
fn process_npm(context: &mut ProcessorContext) -> Result<(), Error> {
context.kickstart_tree(true)?;
let mut control = context.create_control_file()?;
let upstream_name = match context.metadata.name() {
Some(name) => name
.trim_end_matches("@")
.replace(['/', '_'], "-")
.replace("@", "")
.to_lowercase(),
None => {
return Err(Error::MissingUpstreamInfo(
"unable to determine the name from package.json for the node project".to_string(),
))
}
};
let mut source = control.add_source(&format!("node-{}", upstream_name));
if let Some(ref maintainer) = context.maintainer {
source.set_maintainer(maintainer);
}
context.bootstrap_debhelper(
&mut source,
DebhelperConfig {
addons: vec!["nodejs"],
..Default::default()
},
)?;
source.set_section(Some("javascript"));
source.set_rules_requires_root(false);
source.set_standards_version(&latest_standards_version().to_string());
let (build_deps, _test_deps) = context.get_project_wide_deps();
import_build_deps(&mut source, &build_deps);
let mut binary = control.add_binary(&format!("node-{}", upstream_name));
binary.set_architecture(Some("all"));
source.set_testsuite("autopkgtest-pkg-nodejs");
control.commit()?;
Ok(())
}
fn process_dist_zilla(context: &mut ProcessorContext) -> Result<(), Error> {
context.kickstart_tree(true)?;
let mut control = context.create_control_file()?;
let upstream_name = match context.metadata.name() {
Some(name) => name,
None => {
return Err(Error::MissingUpstreamInfo(
"unable to determine the name in dist.ini for the perl project".to_string(),
))
}
};
let mut source = control.add_source(&crate::names::perl_package_name(upstream_name));
if let Some(ref maintainer) = context.maintainer {
source.set_maintainer(maintainer);
}
source.set_section(Some("perl"));
source.set_rules_requires_root(false);
source.set_testsuite("autopkgtest-pkg-perl");
source.set_standards_version(&latest_standards_version().to_string());
let (build_deps, _test_deps) = context.get_project_wide_deps();
import_build_deps(&mut source, &build_deps);
context.bootstrap_debhelper(
&mut source,
DebhelperConfig {
addons: vec!["dist-zilla"],
..Default::default()
},
)?;
let binary_name = source.name().unwrap();
let mut binary = control.add_binary(&binary_name);
binary.set_architecture(Some("all"));
binary.as_mut_deb822().insert("Depends", "${perl:Depends}");
control.commit()?;
Ok(())
}
fn process_meson(context: &mut ProcessorContext) -> Result<(), Error> {
context.kickstart_tree(true)?;
let mut control = context.create_control_file()?;
let upstream_name = match context.metadata.name() {
Some(name) => name,
None => {
return Err(Error::MissingUpstreamInfo(
"unable to determine the name in meson.build".to_string(),
))
}
};
let source_name = crate::names::upstream_name_to_debian_source_name(upstream_name)
.unwrap_or_else(|| upstream_name.to_string());
let mut source = control.add_source(&source_name);
if let Some(ref maintainer) = context.maintainer {
source.set_maintainer(maintainer);
}
source.set_section(Some("misc"));
source.set_rules_requires_root(false);
source.set_standards_version(&latest_standards_version().to_string());
source.set_build_depends(&"meson".parse().unwrap());
let (build_deps, _test_deps) = context.get_project_wide_deps();
import_build_deps(&mut source, &build_deps);
context.bootstrap_debhelper(
&mut source,
DebhelperConfig {
buildsystem: Some("meson"),
build_directory: Some("build"),
..Default::default()
},
)?;
let binary_name = source.name().unwrap();
let mut binary = control.add_binary(&binary_name);
binary.set_architecture(Some("any"));
binary
.as_mut_deb822()
.insert("Depends", "${misc:Depends}, ${shlibs:Depends}");
control.commit()?;
Ok(())
}
fn process_perl_build_tiny(context: &mut ProcessorContext) -> Result<(), Error> {
context.kickstart_tree(true)?;
let mut control = context.create_control_file()?;
let upstream_name = match context.metadata.name() {
Some(name) => name,
None => {
return Err(Error::MissingUpstreamInfo(
"unable to determine the name in dist.ini for the perl project".to_string(),
))
}
};
let mut source = control.add_source(&crate::names::perl_package_name(upstream_name));
if let Some(ref maintainer) = context.maintainer {
source.set_maintainer(maintainer);
}
source.set_section(Some("perl"));
source.set_rules_requires_root(false);
source.set_testsuite("autopkgtest-pkg-perl");
source.set_standards_version(&latest_standards_version().to_string());
source.set_build_depends(&"libmodule-build-perl".parse().unwrap());
let (build_deps, _test_deps) = context.get_project_wide_deps();
import_build_deps(&mut source, &build_deps);
context.bootstrap_debhelper(&mut source, DebhelperConfig::default())?;
let binary_name = source.name().unwrap();
let mut binary = control.add_binary(&binary_name);
binary.set_architecture(Some("all"));
binary.as_mut_deb822().insert("Depends", "${perl:Depends}");
control.commit()?;
Ok(())
}
fn process_golang(context: &mut ProcessorContext) -> Result<(), Error> {
context.kickstart_tree(true)?;
let mut control = context.create_control_file()?;
let repository_url = match context.metadata.repository() {
Some(url) => url.parse::<url::Url>().unwrap(),
None => {
return Err(Error::MissingUpstreamInfo(
"no upstream or origin url is associated with the go project".to_string(),
))
}
};
let godebname = crate::names::go_base_name(
&[repository_url.host_str().unwrap(), repository_url.path()].concat(),
);
let mut source = control.add_source(&format!("golang-{}", godebname));
if let Some(ref maintainer) = context.maintainer {
source.set_maintainer(maintainer);
}
source.set_rules_requires_root(false);
source.set_standards_version(&latest_standards_version().to_string());
source.as_mut_deb822().insert(
"XS-Go-Import-Path",
&crate::names::go_import_path_from_repo(&repository_url),
);
if let Some(url) = context.metadata.repository() {
source.set_vcs_browser(Some(url));
}
source.set_section(Some("devel"));
let (build_deps, _test_deps) = context.get_project_wide_deps();
import_build_deps(&mut source, &build_deps);
source.set_testsuite("autopkgtest-pkg-go");
let mut dh_env = HashMap::new();
if context.wt.has_filename(&context.subpath.join("examples")) {
dh_env.insert("DH_GOLANG_EXCLUDES", "examples/");
}
context.bootstrap_debhelper(
&mut source,
DebhelperConfig {
addons: vec!["golang"],
buildsystem: Some("golang"),
build_directory: Some("_build"),
env: dh_env,
},
)?;
let mut binary = control.add_binary(&format!("golang-{}-dev", godebname));
binary.set_architecture(Some("all"));
binary.set_multi_arch(Some(MultiArch::Foreign));
control.commit()?;
Ok(())
}
fn process_r(context: &mut ProcessorContext) -> Result<(), Error> {
context.kickstart_tree(true)?;
let mut control = context.create_control_file()?;
let archive = match context.metadata.archive() {
Some("CRAN") => "cran",
Some("Bioconductor") => "bioc",
_ => "other",
};
let upstream_name = match context.metadata.name() {
Some(name) => name.to_lowercase(),
None => {
return Err(Error::MissingUpstreamInfo(
"unable to determine the name from DESCRIPTION for the R project".to_string(),
))
}
};
let mut source = control.add_source(&format!("r-{}-{}", archive, upstream_name));
if let Some(ref maintainer) = context.maintainer {
source.set_maintainer(maintainer);
}
source.set_section(Some("gnu-r"));
source.set_rules_requires_root(false);
source.set_build_depends(&"dh-r, r-base-dev".parse().unwrap());
source.set_standards_version(&latest_standards_version().to_string());
source.set_testsuite("autopkgtest-pkg-r");
let (build_deps, _test_deps) = context.get_project_wide_deps();
import_build_deps(&mut source, &build_deps);
context.bootstrap_debhelper(
&mut source,
DebhelperConfig {
buildsystem: Some("R"),
..Default::default()
},
)?;
let mut binary = control.add_binary(&format!("r-{}-{}", archive, upstream_name));
binary.set_architecture(Some("any"));
binary.as_mut_deb822().insert(
"Depends",
"${R:Depends}, ${shlibs:Depends}, ${misc:Depends}",
);
binary
.as_mut_deb822()
.insert("Recommends", "${R:Recommends}");
binary.as_mut_deb822().insert("Suggests", "${R:Suggests}");
control.commit()?;
Ok(())
}
fn process_octave(context: &mut ProcessorContext) -> Result<(), Error> {
context.kickstart_tree(true)?;
let mut control = context.create_control_file()?;
let upstream_name = match context.metadata.name() {
Some(name) => name.to_lowercase(),
None => {
return Err(Error::MissingUpstreamInfo(
"unable to determine the name from DESCRIPTION for the octave project".to_string(),
))
}
};
let mut source = control.add_source(&format!("octave-{}", upstream_name));
if let Some(ref maintainer) = context.maintainer {
source.set_maintainer(maintainer);
}
source.set_section(Some("math"));
source.set_rules_requires_root(false);
source.set_build_depends(&"dh-octave".parse().unwrap());
source.set_standards_version(&latest_standards_version().to_string());
let (build_deps, _test_deps) = context.get_project_wide_deps();
import_build_deps(&mut source, &build_deps);
context.bootstrap_debhelper(
&mut source,
DebhelperConfig {
buildsystem: Some("octave"),
addons: vec!["octave"],
..Default::default()
},
)?;
let mut binary = control.add_binary(&format!("octave-{}", upstream_name));
binary.set_architecture(Some("all"));
binary
.as_mut_deb822()
.insert("Depends", "${octave:Depends}, ${misc:Depends}");
binary.set_description(Some("${octave:Upstream-Description}"));
control.commit()?;
Ok(())
}
fn process_default(context: &mut ProcessorContext) -> Result<(), Error> {
context.kickstart_tree(true)?;
let mut control = context.create_control_file()?;
let upstream_name = match context.metadata.name() {
Some(name) => name,
None => {
return Err(Error::MissingUpstreamInfo(
"unable to determine the upstream name".to_string(),
))
}
};
let source_name =
crate::names::upstream_name_to_debian_source_name(upstream_name).ok_or_else(|| {
Error::MissingUpstreamInfo(format!(
"Unable to determine source package name for {}",
upstream_name
))
})?;
let mut source = control.add_source(&source_name);
if let Some(ref maintainer) = context.maintainer {
source.set_maintainer(maintainer);
}
source.set_section(Some("misc"));
source.set_rules_requires_root(false);
source.set_standards_version(&latest_standards_version().to_string());
let (build_deps, _test_deps) = context.get_project_wide_deps();
import_build_deps(&mut source, &build_deps);
context.bootstrap_debhelper(&mut source, DebhelperConfig::default())?;
let binary_name = source.name().unwrap();
let mut binary = control.add_binary(&binary_name);
binary.set_architecture(Some("any"));
binary
.as_mut_deb822()
.insert("Depends", "${misc:Depends}, ${shlibs:Depends}");
control.commit()?;
Ok(())
}
fn process_cmake(context: &mut ProcessorContext) -> Result<(), Error> {
context.kickstart_tree(true)?;
let mut control = context.create_control_file()?;
let upstream_name = match context.metadata.name() {
Some(name) => name,
None => {
return Err(Error::MissingUpstreamInfo(
"unable to determine the name of the cmake project".to_string(),
))
}
};
let source_name = crate::names::upstream_name_to_debian_source_name(upstream_name)
.unwrap_or_else(|| upstream_name.to_string());
let mut source = control.add_source(&source_name);
if let Some(ref maintainer) = context.maintainer {
source.set_maintainer(maintainer);
}
source.set_standards_version(&latest_standards_version().to_string());
source.set_section(Some("misc"));
source.set_rules_requires_root(false);
let mut build_depends = Relations::new();
ensure_some_version(&mut build_depends, "cmake");
source.set_build_depends(&build_depends);
context.bootstrap_debhelper(
&mut source,
DebhelperConfig {
buildsystem: Some("cmake"),
..Default::default()
},
)?;
let (additional_build_deps, _test_deps) = context.get_project_wide_deps();
import_build_deps(&mut source, &additional_build_deps);
let binary_name = source.name().unwrap();
let mut binary = control.add_binary(&binary_name);
binary.set_architecture(Some("any"));
binary
.as_mut_deb822()
.insert("Depends", "${misc:Depends}, ${shlibs:Depends}");
control.commit()?;
Ok(())
}
fn process_make(context: &mut ProcessorContext) -> Result<(), Error> {
context.kickstart_tree(true)?;
if context
.wt
.has_filename(&context.subpath.join("Makefile.PL"))
{
return process_makefile_pl(context);
}
let mut control = context.create_control_file()?;
let upstream_name = match context.metadata.name() {
Some(name) => name,
None => {
return Err(Error::MissingUpstreamInfo(
"unable to determine the name of the make project".to_string(),
))
}
};
let source_name = crate::names::upstream_name_to_debian_source_name(upstream_name)
.unwrap_or_else(|| upstream_name.to_string());
let mut source = control.add_source(&source_name);
if let Some(ref maintainer) = context.maintainer {
source.set_maintainer(maintainer);
}
source.set_standards_version(&latest_standards_version().to_string());
source.set_section(Some("misc"));
source.set_rules_requires_root(false);
let autotools = ["configure.ac", "configure.in", "Makefile.am", "autogen.sh"]
.iter()
.any(|f| context.wt.has_filename(&context.subpath.join(f)));
context.bootstrap_debhelper(
&mut source,
DebhelperConfig {
buildsystem: if autotools { None } else { Some("makefile") },
..Default::default()
},
)?;
let (additional_build_deps, _test_deps) = context.get_project_wide_deps();
import_build_deps(&mut source, &additional_build_deps);
let binary_name = source.name().unwrap();
let mut binary = control.add_binary(&binary_name);
binary.set_architecture(Some("any"));
binary
.as_mut_deb822()
.insert("Depends", "${misc:Depends}, ${shlibs:Depends}");
control.commit()?;
Ok(())
}
fn process_makefile_pl(context: &mut ProcessorContext) -> Result<(), Error> {
let mut control = context.create_control_file()?;
let upstream_name = match context.metadata.name() {
Some(name) => name,
None => {
return Err(Error::MissingUpstreamInfo(
"unable to determine the name of the perl project".to_string(),
))
}
};
let mut source = control.add_source(&crate::names::perl_package_name(upstream_name));
if let Some(ref maintainer) = context.maintainer {
source.set_maintainer(maintainer);
}
source.set_section(Some("perl"));
source.set_rules_requires_root(false);
source.set_testsuite("autopkgtest-pkg-perl");
source.set_standards_version(&latest_standards_version().to_string());
let (build_deps, _test_deps) = context.get_project_wide_deps();
import_build_deps(&mut source, &build_deps);
context.bootstrap_debhelper(&mut source, DebhelperConfig::default())?;
let binary_name = source.name().unwrap();
let mut binary = control.add_binary(&binary_name);
binary.set_architecture(Some("all"));
binary.as_mut_deb822().insert("Depends", "${perl:Depends}");
control.commit()?;
Ok(())
}
fn populate_cargo_features(
cargo: &mut toml_edit::DocumentMut,
features: &HashMap<String, Vec<String>>,
) {
if let Some(features_section) = cargo["features"].as_table_mut() {
for (feature, reqs) in features.iter() {
features_section[feature] = toml_edit::value(toml_edit::Array::new());
for req in reqs.iter() {
features_section[feature]
.as_array_mut()
.unwrap()
.push(toml_edit::Value::from(req.to_string()));
}
}
}
}
fn cargo_version_to_use<'a>(base_version: &'a str, metadata_version: Option<&'a str>) -> &'a str {
if base_version == "0" || base_version.contains("git") {
metadata_version.unwrap_or("0.0.0")
} else {
base_version
}
}
fn crate_version_is_newer(available: &semver::Version, desired: &semver::Version) -> bool {
(available.major, available.minor) > (desired.major, desired.minor)
}
fn process_debcargo(context: &mut ProcessorContext) -> Result<(), Error> {
context.kickstart_tree(false)?;
let cratename = match context
.metadata
.get("Cargo-Crate")
.and_then(|v| v.datum.as_str())
{
Some(cratename) => cratename.to_string(),
None => context.metadata.name().unwrap().replace("_", "-"),
};
use semver::Version as VersionInfo;
let base_version = crate::names::debian_to_upstream_version(&context.upstream_version);
let version_to_use = cargo_version_to_use(base_version, context.metadata.version());
let desired_version = VersionInfo::parse(version_to_use).map_err(|e| {
Error::Other(format!(
"Invalid semver version '{}': {}",
version_to_use, e
))
})?;
let rt = tokio::runtime::Runtime::new().unwrap();
let data = rt
.block_on(upstream_ontologist::providers::rust::load_crate_info(
&cratename,
))
.map_err(|e| {
Error::MissingUpstreamInfo(format!(
"Unable to load crate info for {}: {}",
cratename, e
))
})?
.ok_or(Error::MissingUpstreamInfo(format!(
"crates.io has no crate {}",
cratename
)))?;
let mut features = None;
let mut crate_version = None;
let mut semver_suffix = false;
for version_info in data.versions {
let available_version = &version_info.num;
if crate_version_is_newer(available_version, &desired_version) {
semver_suffix = true;
break;
}
if VersionInfo::parse(&debian_analyzer::debcargo::unmangle_debcargo_version(
&context.upstream_version,
))
.unwrap()
== version_info.num
{
crate_version = Some(version_info.num);
features = Some(version_info.features.clone());
}
}
let mut control = debian_analyzer::debcargo::DebcargoEditor::new();
control.cargo = Some(toml_edit::DocumentMut::new());
control.cargo.as_mut().unwrap()["package"]["name"] = toml_edit::value(cratename);
if let Some(crate_version) = crate_version {
control.cargo.as_mut().unwrap()["package"]["version"] =
toml_edit::value(crate_version.to_string());
}
if let Some(features) = features {
populate_cargo_features(control.cargo.as_mut().unwrap(), &features);
}
control.debcargo["semver_suffix"] = toml_edit::value(semver_suffix);
control.debcargo["overlay"] = toml_edit::value(".");
control.commit()?;
Ok(())
}
fn process_cargo(context: &mut ProcessorContext) -> Result<(), Error> {
context.kickstart_tree(true)?;
let mut control = context.create_control_file()?;
let upstream_name = match context.metadata.name() {
Some(name) => name,
None => {
return Err(Error::MissingUpstreamInfo(
"unable to determine the name from cargo.toml".to_string(),
))
}
};
let mut source =
control.add_source(&upstream_package_to_debian_source_name("rust", upstream_name).unwrap());
context.bootstrap_debhelper(
&mut source,
DebhelperConfig {
addons: vec!["rust"],
..Default::default()
},
)?;
if let Some(ref maintainer) = context.maintainer {
source.set_maintainer(maintainer);
}
if let Some(url) = context.metadata.repository_browse() {
source.set_vcs_browser(Some(url));
};
if let Some(url) = context.metadata.repository() {
source.set_vcs_git(url);
};
source.set_section(Some("rust"));
let (build_deps, _test_deps) = context.get_project_wide_deps();
import_build_deps(&mut source, &build_deps);
source.set_standards_version(&latest_standards_version().to_string());
if let Some(homepage_str) = context.metadata.homepage() {
match Url::parse(homepage_str) {
Ok(url) => source.set_homepage(&url),
Err(_) => {} }
}
control.commit()?;
Ok(())
}
type Processor = fn(&mut ProcessorContext) -> Result<(), Error>;
fn select_processor(buildsystem_name: &str, use_debcargo: bool) -> Processor {
match buildsystem_name {
"setup.py" => process_setup_py,
"node" => process_npm,
"maven" | "gradle" => process_maven,
"Dist::Zilla" => process_dist_zilla,
"meson" => process_meson,
"Module::Build::Tiny" => process_perl_build_tiny,
"cargo" if use_debcargo => process_debcargo, "cargo" => process_cargo,
"golang" => process_golang,
"R" => process_r,
"octave" => process_octave,
"cmake" => process_cmake,
"make" => process_make, _ => process_default,
}
}
pub fn process(
session: &dyn Session,
wt: &dyn PyWorkingTree,
subpath: PathBuf,
debian_path: PathBuf,
upstream_version: String,
metadata: &UpstreamMetadata,
compat_release: String,
buildsystem: Box<dyn BuildSystem>,
buildsystem_subpath: PathBuf,
maintainer: Option<String>,
_kickstart_from_dist: Option<Box<dyn FnOnce(&dyn PyWorkingTree, &Path) -> Result<(), Error>>>,
use_debcargo: bool,
) -> Result<(), Error> {
let processor = select_processor(buildsystem.name(), use_debcargo);
let mut context = ProcessorContext {
session,
wt,
subpath,
debian_path,
upstream_version,
metadata,
compat_release,
buildsystem,
_buildsystem_subpath: buildsystem_subpath,
maintainer,
_kickstart_from_dist,
};
processor(&mut context)
}
fn check_python3_support(wt: &dyn PyWorkingTree, subpath: &Path) -> Result<bool, Error> {
let setup_py_path = subpath.join("setup.py");
if wt.has_filename(&setup_py_path) {
match wt.get_file_text(&setup_py_path) {
Ok(content) => {
let content_str = String::from_utf8_lossy(&content);
if content_str.contains("Programming Language :: Python :: 3")
|| content_str.contains("python_requires") && content_str.contains(">=3")
{
return Ok(true);
}
if content_str.contains("python_requires")
&& (content_str.contains(">=2.7") || content_str.contains(">=2.6"))
{
return Ok(true); }
}
Err(_) => {} }
}
let pyproject_path = subpath.join("pyproject.toml");
if wt.has_filename(&pyproject_path) {
match wt.get_file_text(&pyproject_path) {
Ok(content) => {
let content_str = String::from_utf8_lossy(&content);
if content_str.contains("python") && content_str.contains(">=3") {
return Ok(true);
}
}
Err(_) => {} }
}
let tox_path = subpath.join("tox.ini");
if wt.has_filename(&tox_path) {
match wt.get_file_text(&tox_path) {
Ok(content) => {
let content_str = String::from_utf8_lossy(&content);
if content_str.contains("py3") || content_str.contains("python3") {
return Ok(true);
}
}
Err(_) => {} }
}
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
#[allow(unpredictable_function_pointer_comparisons)]
fn test_select_processor() {
assert!(select_processor("maven", false) == process_maven as Processor);
assert!(select_processor("gradle", false) == process_maven as Processor);
assert!(select_processor("setup.py", false) == process_setup_py as Processor);
assert!(select_processor("node", false) == process_npm as Processor);
assert!(select_processor("cargo", false) == process_cargo as Processor);
assert!(select_processor("cargo", true) == process_debcargo as Processor);
assert!(select_processor("bazel", false) == process_default as Processor);
}
#[test]
fn test_debhelper_rules() {
let mut output = Vec::new();
debhelper_rules(&mut output, None, None, HashMap::new()).unwrap();
let rules_content = String::from_utf8(output).unwrap();
assert!(rules_content.contains("#!/usr/bin/make -f"));
assert!(rules_content.contains("%:\n\tdh $@\n"));
assert!(!rules_content.contains("--buildsystem="));
let mut output = Vec::new();
debhelper_rules(&mut output, Some("python"), None, HashMap::new()).unwrap();
let rules_content = String::from_utf8(output).unwrap();
assert!(rules_content.contains("dh $@ --buildsystem=python"));
let mut output = Vec::new();
let mut env = HashMap::new();
env.insert("TEST_VAR", "test-value");
env.insert("ANOTHER_VAR", "another-value");
debhelper_rules(&mut output, None, None, env).unwrap();
let rules_content = String::from_utf8(output).unwrap();
assert!(rules_content.contains("export TEST_VAR=test-value"));
assert!(rules_content.contains("export ANOTHER_VAR=another-value"));
}
#[test]
fn test_enable_dh_addon() {
let mut control = debian_control::lossless::Control::new();
let mut source = control.add_source("test-package");
enable_dh_addon(&mut source, "python3");
let build_deps = source.build_depends().unwrap();
let deps_string = build_deps.to_string();
assert!(deps_string.contains("dh-sequence-python3"));
enable_dh_addon(&mut source, "nodejs");
let build_deps = source.build_depends().unwrap();
let deps_string = build_deps.to_string();
assert!(deps_string.contains("dh-sequence-python3"));
assert!(deps_string.contains("dh-sequence-nodejs"));
}
#[test]
fn test_import_build_deps() {
let mut control = debian_control::lossless::Control::new();
let mut source = control.add_source("test-package");
source.set_build_depends(&"debhelper-compat (= 13)".parse().unwrap());
let new_build_deps: debian_control::lossless::relations::Relations =
"python3-all, dh-python".parse().unwrap();
import_build_deps(&mut source, &new_build_deps);
let build_deps = source.build_depends().unwrap();
let deps_string = build_deps.to_string();
assert!(deps_string.contains("debhelper-compat (= 13)"));
assert!(deps_string.contains("python3-all"));
assert!(deps_string.contains("dh-python"));
let versioned_deps: debian_control::lossless::relations::Relations =
"python3-all (>= 3.9)".parse().unwrap();
import_build_deps(&mut source, &versioned_deps);
let build_deps = source.build_depends().unwrap();
let deps_string = build_deps.to_string();
assert!(deps_string.contains("python3-all (>= 3.9)"));
}
#[test]
fn test_debhelper_config() {
let config = DebhelperConfig {
addons: vec!["python3", "nodejs"],
env: {
let mut env = HashMap::new();
env.insert("TEST_VAR", "test-value");
env
},
buildsystem: Some("pybuild"),
build_directory: None,
};
assert_eq!(config.addons.len(), 2);
assert_eq!(config.addons[0], "python3");
assert_eq!(config.addons[1], "nodejs");
assert_eq!(config.env.get("TEST_VAR"), Some(&"test-value"));
assert_eq!(config.buildsystem, Some("pybuild"));
let default_config = DebhelperConfig::default();
assert!(default_config.addons.is_empty());
assert!(default_config.env.is_empty());
assert_eq!(default_config.buildsystem, None);
assert_eq!(default_config.build_directory, None);
}
#[test]
fn test_populate_cargo_features() {
let mut cargo = toml_edit::DocumentMut::new();
cargo["features"] = toml_edit::Item::Table(toml_edit::Table::new());
let mut features = HashMap::new();
features.insert(
"default".to_string(),
vec!["std".to_string(), "alloc".to_string()],
);
features.insert("std".to_string(), vec![]);
populate_cargo_features(&mut cargo, &features);
let features_section = cargo["features"].as_table().unwrap();
let default = features_section["default"].as_array().unwrap();
let default_values: Vec<&str> = default.iter().map(|v| v.as_str().unwrap()).collect();
assert_eq!(default_values, vec!["std", "alloc"]);
let std = features_section["std"].as_array().unwrap();
assert_eq!(std.len(), 0);
}
#[test]
fn test_populate_cargo_features_missing_section_is_noop() {
let mut cargo = toml_edit::DocumentMut::new();
let mut features = HashMap::new();
features.insert("foo".to_string(), vec!["bar".to_string()]);
populate_cargo_features(&mut cargo, &features);
assert!(cargo.get("features").is_none());
}
#[test]
fn test_populate_cargo_features_non_table_left_untouched() {
let mut cargo = toml_edit::DocumentMut::new();
cargo["features"] = toml_edit::value("not-a-table");
let mut features = HashMap::new();
features.insert("foo".to_string(), vec!["bar".to_string()]);
populate_cargo_features(&mut cargo, &features);
assert_eq!(cargo["features"].as_str(), Some("not-a-table"));
}
fn init_working_tree(path: &Path) -> breezyshim::workingtree::GenericWorkingTree {
use breezyshim::controldir::ControlDirFormat;
breezyshim::init();
let format = ControlDirFormat::default();
let transport =
breezyshim::transport::get_transport(&url::Url::from_file_path(path).unwrap(), None)
.unwrap();
let controldir = format.initialize_on_transport(&transport).unwrap();
controldir.create_repository(None).unwrap();
controldir.create_branch(None).unwrap();
controldir.create_workingtree().unwrap()
}
#[test]
fn test_prepare_empty_subpath_creates_versioned_dir() {
use breezyshim::tree::Tree;
let td = tempfile::tempdir().unwrap();
let wt = init_working_tree(td.path());
let subpath = Path::new("debian");
assert!(!wt.has_filename(subpath));
prepare_empty_subpath(&wt, subpath).unwrap();
assert!(wt.has_filename(subpath));
assert!(wt.is_versioned(subpath));
}
#[test]
fn test_prepare_empty_subpath_existing_dir_left_versioned() {
use breezyshim::tree::{MutableTree, Tree};
let td = tempfile::tempdir().unwrap();
let wt = init_working_tree(td.path());
let subpath = Path::new("debian");
wt.mkdir(subpath).unwrap();
assert!(wt.has_filename(subpath));
prepare_empty_subpath(&wt, subpath).unwrap();
assert!(wt.has_filename(subpath));
assert!(wt.is_versioned(subpath));
}
#[test]
fn test_check_python3_support_classifier() {
use breezyshim::tree::MutableTree;
let td = tempfile::tempdir().unwrap();
let wt = init_working_tree(td.path());
wt.put_file_bytes_non_atomic(
Path::new("setup.py"),
b"setup(classifiers=['Programming Language :: Python :: 3'])",
)
.unwrap();
assert!(check_python3_support(&wt, Path::new("")).unwrap());
}
#[test]
fn test_check_python3_support_no_setup_py_defaults_true() {
let td = tempfile::tempdir().unwrap();
let wt = init_working_tree(td.path());
assert!(check_python3_support(&wt, Path::new("")).unwrap());
}
#[test]
fn test_check_python3_support_pyproject() {
use breezyshim::tree::MutableTree;
let td = tempfile::tempdir().unwrap();
let wt = init_working_tree(td.path());
wt.put_file_bytes_non_atomic(
Path::new("pyproject.toml"),
b"[project]\nrequires-python = \">=3.8\"\n",
)
.unwrap();
assert!(check_python3_support(&wt, Path::new("")).unwrap());
}
#[test]
fn test_cargo_version_to_use_release() {
assert_eq!(cargo_version_to_use("1.2.3", Some("9.9.9")), "1.2.3");
assert_eq!(cargo_version_to_use("1.2.3", None), "1.2.3");
}
#[test]
fn test_cargo_version_to_use_zero_snapshot() {
assert_eq!(cargo_version_to_use("0", Some("4.5.6")), "4.5.6");
assert_eq!(cargo_version_to_use("0", None), "0.0.0");
}
#[test]
fn test_cargo_version_to_use_git_snapshot() {
assert_eq!(
cargo_version_to_use("0+git20250809.4f78468", Some("2.0.0")),
"2.0.0"
);
assert_eq!(cargo_version_to_use("0+git20250809.4f78468", None), "0.0.0");
}
#[test]
fn test_crate_version_is_newer() {
use semver::Version;
let desired = Version::new(1, 2, 0);
assert!(crate_version_is_newer(&Version::new(2, 0, 0), &desired));
assert!(crate_version_is_newer(&Version::new(1, 3, 0), &desired));
assert!(!crate_version_is_newer(&Version::new(1, 2, 0), &desired));
assert!(!crate_version_is_newer(&Version::new(1, 2, 9), &desired));
assert!(!crate_version_is_newer(&Version::new(0, 9, 0), &desired));
assert!(!crate_version_is_newer(&Version::new(1, 1, 9), &desired));
}
#[test]
fn test_check_python3_support_tox() {
use breezyshim::tree::MutableTree;
let td = tempfile::tempdir().unwrap();
let wt = init_working_tree(td.path());
wt.put_file_bytes_non_atomic(Path::new("tox.ini"), b"[tox]\nenvlist = py39,py310\n")
.unwrap();
assert!(check_python3_support(&wt, Path::new("")).unwrap());
}
}