use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::fs;
use std::io::{self, BufRead, ErrorKind, Read, Seek, Write as IoWrite};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::str::FromStr;
use anyhow::format_err;
use chrono::{self, Datelike};
use control::BuildDeps;
use flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use flate2::Compression;
use itertools::Itertools;
use regex::Regex;
use tar::{Archive, Builder};
use tempfile;
use crate::config::{package_field_for_feature, testing_ignore_debpolv, Config, PackageKey};
use crate::crates::{
all_dependencies_and_features, show_dep, transitive_deps, AutoPatches, CrateDepInfo, CrateInfo,
};
use crate::errors::Result;
use crate::util::{self, copy_tree, expect_success, get_transitive_val, traverse_depth};
use self::changelog::{ChangelogEntry, ChangelogIterator};
use self::control::{base_deb_name, deb_upstream_version};
use self::control::{Description, Package, PkgTest, PkgTestRestriction, Source};
use self::copyright::debian_copyright;
pub use self::dependency::{deb_dep_add_nocheck, deb_deps};
pub mod changelog;
pub mod control;
pub mod copyright;
mod dependency;
pub struct DebInfo {
upstream_name: String,
base_package_name: String,
name_suffix: Option<String>,
uscan_version_pattern: Option<String>,
package_name: String,
deb_upstream_version: String,
debcargo_version: String,
package_source_dir: PathBuf,
orig_tarball_path: PathBuf,
}
impl DebInfo {
pub fn new(
crate_info: &CrateInfo,
debcargo_version: &str,
semver_suffix: bool,
repack_suffix: Option<&str>,
) -> Self {
let upstream_name = crate_info.package_id().name().to_string();
let name_dashed = base_deb_name(&upstream_name);
let base_package_name = name_dashed.to_lowercase();
let (name_suffix, uscan_version_pattern, package_name) = if semver_suffix {
let semver = crate_info.semver();
let name_suffix = format!("-{semver}");
let uscan = format!("[-_]?({semver}\\.\\d[\\-+\\.:\\~\\da-zA-Z]*)");
let pkgname = format!("{base_package_name}{name_suffix}");
(Some(name_suffix), Some(uscan), pkgname)
} else {
(None, None, base_package_name.clone())
};
let deb_upstream_version = deb_upstream_version(crate_info.version(), repack_suffix);
let package_source_dir = PathBuf::from(format!(
"{}-{}-{}",
Source::pkg_prefix(),
package_name,
deb_upstream_version
));
let orig_tarball_path = PathBuf::from(format!(
"{}-{}_{}.orig.tar.gz",
Source::pkg_prefix(),
package_name,
deb_upstream_version
));
DebInfo {
upstream_name,
base_package_name,
name_suffix,
uscan_version_pattern,
package_name,
deb_upstream_version,
debcargo_version: debcargo_version.to_string(),
package_source_dir,
orig_tarball_path,
}
}
pub fn upstream_name(&self) -> &str {
self.upstream_name.as_str()
}
pub fn base_package_name(&self) -> &str {
self.base_package_name.as_str()
}
pub fn name_suffix(&self) -> Option<&str> {
self.name_suffix.as_deref()
}
pub fn package_name(&self) -> &str {
self.package_name.as_str()
}
pub fn deb_upstream_version(&self) -> &str {
self.deb_upstream_version.as_str()
}
pub fn debcargo_version(&self) -> &str {
self.debcargo_version.as_str()
}
pub fn package_source_dir(&self) -> &Path {
self.package_source_dir.as_ref()
}
pub fn orig_tarball_path(&self) -> &Path {
self.orig_tarball_path.as_ref()
}
}
pub fn prepare_orig_tarball(crate_info: &CrateInfo, tarball: &Path, repacked: bool) -> Result<()> {
let crate_file = crate_info.crate_file();
let tempdir = tempfile::Builder::new()
.prefix("debcargo")
.tempdir_in(".")?;
let temp_archive_path = tempdir.path().join(tarball);
let mut create = fs::OpenOptions::new();
create.write(true).create_new(true);
if repacked {
debcargo_info!("crate tarball was modified; repacking for debian");
let mut f = crate_file.file();
f.seek(io::SeekFrom::Start(0))?;
let mut archive = Archive::new(GzDecoder::new(f));
let mut new_archive = Builder::new(GzEncoder::new(
create.open(&temp_archive_path)?,
Compression::best(),
));
for entry in archive.entries()? {
let entry = entry?;
let path = entry.path()?.into_owned();
match crate_info.filter_path(&entry.path()?) {
Err(e) => debcargo_bail!(e),
Ok(r) => {
if r {
writeln!(
io::stderr(),
"Filtered out files from .orig.tar.gz: {:?}",
entry.path()?
)?;
} else {
new_archive.append_data(&mut entry.header().clone(), path, entry)?;
}
}
}
}
new_archive.finish()?;
} else {
fs::copy(crate_file.path(), &temp_archive_path)?;
}
fs::rename(temp_archive_path, tarball)?;
Ok(())
}
pub fn apply_overlay_and_patches(
crate_info: &mut CrateInfo,
config_path: Option<&Path>,
config: &Config,
output_dir: &Path,
auto_patches: &AutoPatches,
) -> Result<tempfile::TempDir> {
let tempdir = tempfile::Builder::new()
.prefix("debcargo")
.tempdir_in(".")?;
let overlay = config.overlay_dir(config_path);
if let Some(p) = overlay.as_ref() {
let p = fs::canonicalize(p.as_path())?;
for anc in tempdir.path().ancestors() {
if p == fs::canonicalize(anc)? {
debcargo_bail!(
"Aborting: refusing to copy an ancestor {} into a descendant {}",
p.display(),
fs::canonicalize(tempdir.path())?.display(),
);
}
}
copy_tree(p.as_path(), tempdir.path())?;
}
if tempdir.path().join("control").exists() {
debcargo_warn!(
"Most of the time you shouldn't overlay debian/control, \
it's a maintenance burden. Use debcargo.toml instead."
);
}
let patches_dir = tempdir.path().join("patches");
let series_file = patches_dir.join("series");
let auto_dir = patches_dir.join("auto");
if auto_dir.exists() {
std::fs::remove_dir_all(&auto_dir)?;
}
for (name, contents) in auto_patches {
if !auto_dir.exists() {
fs::create_dir_all(&auto_dir)?;
}
fs::write(auto_dir.join(name), contents)?;
}
if series_file.exists() {
let file = fs::File::open(&series_file)?;
let lines = io::BufReader::new(file).lines();
let mut new_series = String::new();
for name in auto_patches.keys() {
new_series.push_str(&format!("auto/{name}\n"));
}
for line in lines {
let line = line?;
if !line.starts_with("auto/") {
new_series.push_str(&line);
new_series.push('\n');
}
}
fs::write(&series_file, new_series)?;
} else if !auto_patches.is_empty() {
let mut new_series = String::new();
for name in auto_patches.keys() {
new_series.push_str(&format!("auto/{name}\n"));
}
fs::write(&series_file, new_series)?;
}
if series_file.exists() {
debcargo_info!("applying patches..");
let output_dir = &fs::canonicalize(output_dir)?;
let stderr = || {
fs::OpenOptions::new()
.append(true)
.open("/dev/stderr")
.unwrap()
};
if let Err(err) = expect_success(
Command::new("quilt")
.stdout(stderr())
.current_dir(output_dir)
.env("QUILT_PATCHES", &patches_dir)
.args(["push", "--quiltrc=-", "-a"]),
"failed to apply patches using quilt",
) {
debcargo_warn!(format!("{err}, attempting cleanup"));
let _ = expect_success(
Command::new("quilt")
.stdout(stderr())
.current_dir(output_dir)
.env("QUILT_PATCHES", &patches_dir)
.args(["pop", "--quiltrc=-", "-a", "-f"]),
"failed to unapply partially applied patches",
);
fs::remove_dir_all(output_dir.join(".pc"))?;
if !auto_patches.is_empty() {
let debian_dir = output_dir.join("debian").join("patches");
debcargo_info!(format!(
"copying re-generated auto-patches to {debian_dir:?}"
));
if let Err(err) = fs::create_dir_all(&debian_dir) {
debcargo_warn!(format!("Failed to create {debian_dir:?} - {err}"));
} else {
if let Err(err) = copy_tree(&patches_dir, &debian_dir) {
debcargo_warn!(format!("Failed to copy auto patches - {err}"));
}
}
}
debcargo_bail!("applying patches failed! see above for details..");
}
debcargo_info!("reloading Cargo.toml..");
let reload_result = crate_info.replace_manifest(&output_dir.join("Cargo.toml"));
debcargo_info!("unapplying patches again..");
expect_success(
Command::new("quilt")
.stdout(stderr())
.current_dir(output_dir)
.env("QUILT_PATCHES", &patches_dir)
.args(["pop", "--quiltrc=-", "-a"]),
"failed to unapply patches",
)?;
fs::remove_dir_all(output_dir.join(".pc"))?;
reload_result?;
}
Ok(tempdir)
}
#[allow(clippy::too_many_arguments)]
pub fn prepare_debian_folder(
crate_info: &mut CrateInfo,
deb_info: &DebInfo,
config_path: Option<&Path>,
config: &Config,
output_dir: &Path,
tempdir: &tempfile::TempDir,
auto_patches: &AutoPatches,
changelog_ready: bool,
copyright_guess_harder: bool,
overlay_write_back: bool,
) -> Result<()> {
let mut create = fs::OpenOptions::new();
create.write(true).create_new(true);
let crate_name = crate_info.package_id().name();
let crate_version = crate_info.package_id().version();
let upstream_name = deb_info.upstream_name();
let maintainer = config.maintainer();
let uploaders: Vec<&str> = config
.uploaders()
.into_iter()
.flatten()
.map(String::as_str)
.collect();
let mut new_hints = vec![];
let mut file = |name: &str| {
let path = tempdir.path();
let f = path.join(name);
fs::create_dir_all(f.parent().unwrap())?;
create.open(&f).or_else(|e| match e.kind() {
ErrorKind::AlreadyExists => {
let hintname = name.to_owned() + util::HINT_SUFFIX;
let hint = path.join(&hintname);
if hint.exists() {
fs::remove_file(&hint)?;
}
new_hints.push(hintname);
create.open(&hint)
}
_ => Err(e),
})
};
{
let checksum = crate_info
.checksum()
.unwrap_or("Could not get crate checksum");
let mut cargo_checksum_json = file("cargo-checksum.json")?;
writeln!(
cargo_checksum_json,
r#"{{"package":"{checksum}","files":{{}}}}"#
)?;
}
{
let mut copyright = io::BufWriter::new(file("copyright")?);
let year_range = if changelog_ready {
changelog_first_last(tempdir.path())?
} else {
let last = chrono::Local::now().year();
match changelog_first_last(tempdir.path()) {
Ok((first, _)) => (first, last),
Err(_) => (last, last),
}
};
let excludes = config.excludes.as_deref().unwrap_or_default();
let dep5_copyright = debian_copyright(
output_dir,
crate_info.manifest(),
crate_info.manifest_path(),
maintainer,
&uploaders,
year_range,
copyright_guess_harder,
excludes,
)?;
write!(copyright, "{dep5_copyright}")?;
}
{
let meta = crate_info.metadata();
let mut fields = indexmap::IndexMap::<&str, String>::new();
if config.crate_src_path(config_path).is_none() {
fields.insert("Archive", "crates.io".to_owned());
}
if let Some(repository) = meta.repository.as_deref() {
let repository = repository.trim_end_matches('/');
fields.insert("Repository-Browse", repository.to_owned());
fields.insert(
"Repository",
git_clone_url(repository).unwrap_or_else(|| repository.to_owned()),
);
}
let documentation = meta.documentation.clone().or_else(|| {
config
.crate_src_path(config_path)
.is_none()
.then(|| format!("https://docs.rs/{crate_name}/{crate_version}"))
});
if let Some(documentation) = documentation {
fields.insert("Documentation", documentation);
}
if !fields.is_empty() {
fs::create_dir_all(tempdir.path().join("upstream"))?;
let metadata = file("upstream/metadata")?;
serde_norway::to_writer(metadata, &fields)?;
}
}
{
let mut watch = file("watch")?;
if config.crate_src_path(config_path).is_some() {
write!(watch, "FIXME add uscan directive for local crate")?;
} else {
let uscan_version_pattern = deb_info
.uscan_version_pattern
.as_ref()
.map_or_else(|| "@ANY_VERSION@".to_string(), ToString::to_string);
writeln!(watch, "Version: 5\n")?;
writeln!(
watch,
"Source: https://qa.debian.org/cgi-bin/fakeupstream.cgi?upstream=crates.io/{upstream_name}",
)?;
writeln!(
watch,
"Matching-Pattern: .*/crates/{upstream_name}/{uscan_version_pattern}/download"
)?;
writeln!(
watch,
r"Filenamemangle: s/.*\/(.*)\/download/{upstream_name}-$1\.tar\.gz/g",
)?;
writeln!(
watch,
r"Uversionmangle: s/(\d)[_\.\-\+]?((RC|rc|pre|dev|beta|alpha)\.?\d*)$/$1~$2/"
)?;
writeln!(watch, r"Dversionmangle: s/@DEB_EXT@//g",)?;
writeln!(watch, r"Compression: gzip")?;
if let Some(repack_suffix) = config.repack_suffix() {
writeln!(watch, r"Repack: yes")?;
writeln!(watch, r"Repacksuffix: +{repack_suffix}")?;
}
}
}
{
fs::create_dir_all(tempdir.path().join("source"))?;
let mut source_format = file("source/format")?;
writeln!(source_format, "3.0 (quilt)")?;
}
let (source, has_dev_depends, default_test_broken) =
prepare_debian_control(deb_info, crate_info, config, &mut file)?;
if testing_ignore_debpolv() {
fs::create_dir_all(tempdir.path().join("debcargo_testing_bin"))?;
let mut env_hack = file("debcargo_testing_bin/env")?;
#[cfg(unix)]
env_hack.set_permissions(fs::Permissions::from_mode(0o775))?;
writeln!(
env_hack,
r#"#!/bin/sh
case "$*" in */usr/share/cargo/bin/dh-cargo-built-using*)
echo "debcargo testing: suppressing dh-cargo-built-using";;
*) /usr/bin/env "$@";; esac
"#
)?;
}
{
let mut rules = file("rules")?;
#[cfg(unix)]
rules.set_permissions(fs::Permissions::from_mode(0o775))?;
if has_dev_depends || testing_ignore_debpolv() {
write!(
rules,
"{}",
concat!(
"#!/usr/bin/make -f\n",
"%:\n",
"\tdh $@ --buildsystem cargo\n"
)
)?;
if testing_ignore_debpolv() {
writeln!(rules, "export RUSTC_BOOTSTRAP := 1")?;
writeln!(
rules,
"export PATH := $(CURDIR)/debian/debcargo_testing_bin:$(PATH)"
)?;
}
} else {
write!(
rules,
"{}{}",
concat!(
"#!/usr/bin/make -f\n",
"%:\n",
"\tdh $@ --buildsystem cargo\n",
"\n",
"override_dh_auto_test:\n",
),
if default_test_broken {
"\tdh_auto_test -- test --all || true\n"
} else {
"\tdh_auto_test -- test --all\n"
},
)?;
}
}
if !changelog_ready {
let author = control::get_deb_author()?;
let crate_src = match config.crate_src_path(config_path) {
Some(_) => "local source",
None => "crates.io",
};
let autogenerated_item = format!(
" * Package {crate_name} {crate_version} from {crate_src} using debcargo {}",
deb_info.debcargo_version()
);
let autogenerated_re = Regex::new(&format!(
r"^ \* Package (.*) (.*) from {crate_src} using debcargo (.*)$",
))?;
let (mut changelog, changelog_data) = changelog_or_new(tempdir.path())?;
let (changelog_old, mut changelog_items, deb_version_suffix) = {
let ver_bump = &|e: Option<&str>| -> Result<Option<String>> {
Ok(match e {
Some(x) => {
let e = ChangelogEntry::from_str(x)?;
if e.version_parts().0 == deb_info.deb_upstream_version() {
Some(e.deb_version_suffix_bump())
} else {
None
}
}
None => None,
})
};
let mut chit = ChangelogIterator::from(&changelog_data);
let e1 = chit.next();
match e1 {
Some(x) if x.contains(changelog::DEFAULT_DIST) => {
let mut e = ChangelogEntry::from_str(x)?;
if author == e.maintainer {
if let Some(pos) = e.items.iter().position(|x| autogenerated_re.is_match(x))
{
e.items[pos] = autogenerated_item;
} else {
e.items.push(autogenerated_item);
}
} else {
e.items.insert(0, autogenerated_item);
e.items.insert(1, String::new());
let ename = e.maintainer_name();
e.items.insert(2, format!(" [ {ename} ]"));
}
(&changelog_data[x.len()..], e.items, ver_bump(chit.next())?)
}
_ => (
changelog_data.as_str(),
vec![autogenerated_item],
ver_bump(e1)?,
),
}
};
let source_deb_version = format!(
"{}-{}",
deb_info.deb_upstream_version(),
deb_version_suffix.unwrap_or_else(|| "1".to_string())
);
if !uploaders.contains(&author.as_str()) {
debcargo_warn!(
"You ({}) are not in Uploaders; adding \"Team upload\" to d/changelog",
author
);
if !changelog_items.contains(&changelog::COMMENT_TEAM_UPLOAD.to_string()) {
changelog_items.insert(0, changelog::COMMENT_TEAM_UPLOAD.to_string());
}
}
let changelog_new_entry = ChangelogEntry::new(
source.name().to_string(),
source_deb_version,
changelog::DEFAULT_DIST.to_string(),
"urgency=medium".to_string(),
author,
changelog::local_now(),
changelog_items,
);
changelog.seek(io::SeekFrom::Start(0))?;
if changelog_old.is_empty() {
write!(changelog, "{changelog_new_entry}")?;
} else {
write!(changelog, "{changelog_new_entry}\n{changelog_old}")?;
}
let pos = changelog.stream_position()?;
changelog.set_len(pos)?;
}
if overlay_write_back {
let overlay = config.overlay_dir(config_path);
if let Some(p) = overlay.as_ref() {
if !changelog_ready {
new_hints.push("changelog".to_string());
}
let auto_dir = p.join("patches").join("auto");
if auto_dir.exists() {
std::fs::remove_dir_all(&auto_dir)?;
}
if !auto_patches.is_empty() {
fs::create_dir_all(&auto_dir)?;
new_hints.push("patches/series".to_string());
for name in auto_patches.keys() {
new_hints.push(format!("patches/auto/{name}"));
}
}
for hint in &new_hints {
let newpath = tempdir.path().join(hint);
let oldpath = p.join(hint);
fs::copy(newpath, oldpath).expect("could not write back");
debcargo_info!("Wrote back file to overlay: {}", hint);
}
}
}
fs::rename(tempdir.path(), output_dir.join("debian"))?;
Ok(())
}
fn prepare_debian_control<F: FnMut(&str) -> std::result::Result<fs::File, io::Error>>(
deb_info: &DebInfo,
crate_info: &CrateInfo,
config: &Config,
mut file: F,
) -> Result<(Source, bool, bool)> {
let crate_name = crate_info.crate_name();
let base_pkgname = deb_info.base_package_name();
let name_suffix = deb_info.name_suffix();
let lib = crate_info.is_lib() && config.build_lib_package();
let mut bins = crate_info.get_binary_targets();
if lib && !bins.is_empty() && !config.build_bin_package() {
bins.clear();
}
let bin_name = if config.bin_name.eq(&Config::default().bin_name) {
let default_bin_name = deb_info.base_package_name();
if !bins.is_empty() {
debcargo_info!(
"Generate binary crate with default name '{}', set bin_name to override or bin = false to disable.",
&default_bin_name
);
}
default_bin_name
} else {
config.bin_name.as_str()
};
let maintainer = config.maintainer();
let requires_root = config.requires_root();
let uploaders: Vec<&str> = config
.uploaders()
.into_iter()
.flatten()
.map(String::as_str)
.collect();
let features_with_deps = all_dependencies_and_features(crate_info.manifest());
let dev_depends = deb_deps(config.allow_prerelease_deps, &crate_info.dev_dependencies())?;
let has_dev_deps = !dev_depends.is_empty();
log::trace!(
"features_with_deps: {:?}",
features_with_deps
.iter()
.map(|(&f, (ff, dd))| { (f, (ff, dd.iter().map(show_dep).collect::<Vec<_>>())) })
.collect::<Vec<_>>()
);
let meta = crate_info.metadata();
let test_is_marked_broken = |f: &str| config.package_test_is_broken(PackageKey::feature(f));
let test_is_broken = |f: &str| {
let getparents = |f: &str| features_with_deps.get(f).map(|(d, _)| d);
match get_transitive_val(&getparents, &test_is_marked_broken, f) {
Err((k, vv)) => debcargo_bail!(
"{} {}: {}: {:?}",
"error trying to recursively determine test_is_broken for",
k,
"dependencies have inconsistent config values",
vv
),
Ok(v) => Ok(v.unwrap_or(false)),
}
};
let test_architecture = |f: &str| -> Result<Option<_>> {
let getparents = |f: &str| features_with_deps.get(f).map(|(d, _)| d);
let feature_get_test_architecture =
|f: &str| config.package_test_architecture(PackageKey::feature(f));
match get_transitive_val(&getparents, &feature_get_test_architecture, f) {
Err((k, vv)) => debcargo_bail!(
"{} {}: {}: {:?}",
"error trying to recursively determine test_architecture for",
k,
"dependencies have inconsistent config values",
vv
),
Ok(Some(v)) if v.is_empty() => Ok(None), Ok(Some(v)) => Ok(Some(v.clone())),
Ok(None) if f != "default" && f != "@" => {
log::debug!(
"feature '{f}': no explicit test_arch value, falling back to skipping riscv64!"
);
Ok(Some(vec!["!riscv64".to_string()]))
}
Ok(None) => Ok(None),
}
};
let test_restrictions = |f: &str| {
let getparents = |f: &str| features_with_deps.get(f).map(|(d, _)| d);
let feature_get_test_restrictions =
|f: &str| config.package_test_restrictions(PackageKey::feature(f));
match get_transitive_val(&getparents, &feature_get_test_restrictions, f) {
Err((k, vv)) => debcargo_bail!(
"{} {}: {}: {:?}",
"error trying to recursively determine test_restrictions for",
k,
"dependencies have inconsistent config values",
vv
),
Ok(Some(v)) if v.is_empty() => Ok(None), Ok(other) => Ok(other),
}
};
let test_command = |f: &str| {
let getparents = |f: &str| features_with_deps.get(f).map(|(d, _)| d);
let feature_get_test_command =
|f: &str| config.package_test_command(PackageKey::feature(f));
match get_transitive_val(&getparents, &feature_get_test_command, f) {
Err((k, vv)) => debcargo_bail!(
"{} {}: {}: {:?}",
"error trying to recursively determine test_command for",
k,
"dependencies have inconsistent config values",
vv
),
Ok(c) => Ok(c),
}
};
let build_deps = {
let mut build_deps = BuildDeps::default();
build_deps.build_depends.extend(
["debhelper-compat (= 13)", "dh-sequence-cargo"]
.iter()
.map(ToString::to_string),
);
let (default_features, default_deps) = transitive_deps(&features_with_deps, "default")?;
let extra_override_deps = package_field_for_feature(
|x| config.package_depends(x),
PackageKey::feature("default"),
&default_features,
);
let mut combined_deps = default_deps;
let mut extra_bin_override_deps = Vec::new();
if let Some(build_features) = config.build_features() {
for feature in build_features {
let (features, feature_deps) = transitive_deps(&features_with_deps, feature)?;
combined_deps.extend(feature_deps);
let feature_override_deps = package_field_for_feature(
|x| config.package_depends(x),
PackageKey::feature(feature),
&features,
);
extra_bin_override_deps.extend(feature_override_deps);
}
}
let build_deps_arch = toolchain_deps(crate_info.rust_version().as_deref())
.into_iter()
.chain(deb_deps(config.allow_prerelease_deps, &combined_deps)?)
.chain(extra_override_deps)
.chain(extra_bin_override_deps);
if bins.is_empty() && !config.build_extra_package() {
assert!(lib);
build_deps
.build_depends_arch
.extend(build_deps_arch.map(|d| {
if config.skip_nocheck().unwrap_or(false) {
d
} else {
deb_dep_add_nocheck(&d)
}
}));
} else {
build_deps.build_depends_arch.extend(build_deps_arch);
}
build_deps
};
let test_deps: Vec<String> = Some(rustc_dep(crate_info.rust_version().as_deref(), false))
.into_iter()
.chain(dev_depends)
.collect();
let homepage = generate_homepage(
&crate_info.manifest().name(),
&crate_info.manifest().version().to_string(),
meta.homepage.as_deref(),
meta.repository.as_deref(),
config.crate_src_path.is_none(),
);
let plain_upstream_version = control::deb_upstream_version(crate_info.version(), None);
let mut source = Source::new(
base_pkgname,
name_suffix,
crate_name,
&plain_upstream_version,
&homepage,
lib,
maintainer.to_string(),
uploaders.iter().map(ToString::to_string).collect(),
build_deps,
requires_root.cloned(),
)?;
source.apply_overrides(config);
let mut control = io::BufWriter::new(file("control")?);
write!(control, "{source}")?;
let (crate_summary, crate_description) = crate_info.get_summary_description();
let summary_prefix = crate_summary.unwrap_or(format!("Rust crate \"{crate_name}\""));
let description_prefix = {
let tmp = crate_description.unwrap_or_default();
if tmp.is_empty() {
tmp
} else {
format!("{tmp}\n.\n")
}
};
if lib {
let all_features: Vec<&str> = features_with_deps.keys().copied().collect();
let all_features_test_broken = match test_is_marked_broken("@") {
Some(v) => v,
None => all_features
.iter()
.any(|f| test_is_marked_broken(f).unwrap_or(false)),
};
let all_features_test_arch = if let Some(v) = test_architecture("@")? {
v
} else {
let combined = all_features
.iter()
.fold(HashSet::new(), |mut set, f| {
if let Ok(Some(arch)) = test_architecture(f) {
set.extend(arch);
}
set
})
.into_iter()
.collect_vec();
if combined.len() == 1 && combined.contains(&"!riscv64".to_string()) {
vec![]
} else {
combined
}
};
let all_features_test_arch: Vec<&str> =
all_features_test_arch.iter().map(AsRef::as_ref).collect();
let mut all_features_test_restrictions = match test_restrictions("@")? {
Some(v) => v.to_owned(),
None => all_features
.iter()
.fold(HashSet::new(), |mut set, f| {
if let Ok(Some(arch)) = test_restrictions(f) {
set.extend(arch.to_owned());
}
set
})
.into_iter()
.collect_vec(),
};
if all_features_test_restrictions.is_empty() {
all_features_test_restrictions.push(PkgTestRestriction::AllowStderr);
}
if all_features_test_broken
&& !all_features_test_restrictions.contains(&PkgTestRestriction::Flaky)
{
all_features_test_restrictions.push(PkgTestRestriction::Flaky);
}
let all_features_test_depends =
generate_test_dependencies("@", &all_features, config, &test_deps);
let all_features_test_command = {
let all_features = features_with_deps.keys().copied().collect();
let getparents = |f: &str| {
if f == "@" {
Some(&all_features)
} else {
None
}
};
let feature_get_test_command =
|f: &str| config.package_test_command(PackageKey::feature(f));
match get_transitive_val(&getparents, &feature_get_test_command, "@") {
Err((k, vv)) => debcargo_bail!(
"{} {}: {}: {:?}",
"error trying to recursively determine test_command for",
k,
"dependencies have inconsistent config values",
vv
),
Ok(v) => v,
}
}
.map(String::as_str);
let mut testctl = io::BufWriter::new(file("tests/control")?);
write!(
testctl,
"{}",
PkgTest::new(
source.name(),
crate_name,
all_features_test_command.unwrap_or("{stock_cmd}"),
"@",
&plain_upstream_version,
&["--all-features"],
&all_features_test_depends,
&all_features_test_restrictions,
&all_features_test_arch,
)?
)?;
let working_features_with_deps = features_with_deps.clone();
let working_features_with_deps = {
let mut working_features_with_deps = working_features_with_deps;
let potential_corner_case = working_features_with_deps
.keys()
.filter(|x| base_deb_name(x).as_str() != **x)
.copied()
.collect::<Vec<_>>();
for f in potential_corner_case {
let f_ = base_deb_name(f);
if let Some((df1, dd1)) = working_features_with_deps.remove(f_.as_str()) {
working_features_with_deps
.entry(f)
.and_modify(|(df0, dd0)| {
let mut df = BTreeSet::from_iter(df0.drain(..));
df.extend(df1);
df.remove(f_.as_str());
df.remove(f);
let mut dd: HashSet<cargo::core::Dependency> =
HashSet::from_iter(dd0.drain(..));
dd.extend(dd1);
df0.extend(df);
dd0.extend(dd);
});
for (df, _) in &mut working_features_with_deps.values_mut() {
for feat in df.iter_mut() {
if *feat == f_.as_str() {
*feat = f;
}
}
}
let dep_feats = traverse_depth(
&|k: &&'static str| working_features_with_deps.get(k).map(|x| &x.0),
f,
);
if dep_feats.contains(f) {
log::debug!("transitive deps of feature {f}: {dep_feats:?}");
debcargo_bail!(
"Tried to merge features {} and {} as they are not representable separately\n\
in Debian, but this resulted in a feature cycle. You need to manually patch the package.", f, f_);
}
debcargo_warn!(
"Merged features {} and {} as they are not representable separately in Debian.\n\
We checked that this does not break the package in an obvious way (feature cycle), however\n\
if there is a more sophisticated breakage, you'll have to manually patch those \
features instead.", f, f_);
}
}
working_features_with_deps
};
log::trace!(
"working_features_with_deps: {:?}",
working_features_with_deps
.iter()
.map(|(&f, (ff, dd))| { (f, (ff, dd.iter().map(show_dep).collect::<Vec<_>>())) })
.collect::<Vec<_>>()
);
let (mut provides, reduced_features_with_deps) = if config.collapse_features {
collapse_features(&working_features_with_deps)
} else {
reduce_provides(working_features_with_deps)
};
log::trace!(
"reduced_features_with_deps: {:?}",
reduced_features_with_deps
.iter()
.map(|(&f, (ff, dd))| { (f, (ff, dd.iter().map(show_dep).collect::<Vec<_>>())) })
.collect::<Vec<_>>()
);
log::trace!("provides: {provides:?}");
let mut recommends = vec![];
let mut suggests = vec![];
for (&feature, features) in &provides {
if feature.is_empty() {
continue;
} else if feature == "default" || features.contains(&"default") {
recommends.push(feature);
} else {
suggests.push(feature);
}
}
let mut no_features_edge_case = BTreeMap::new();
no_features_edge_case.insert("", (vec![], vec![]));
no_features_edge_case.insert("default", (vec![""], vec![]));
let no_features_edge_case = features_with_deps == no_features_edge_case;
for (feature, (f_deps, o_deps)) in reduced_features_with_deps {
let pk = PackageKey::feature(feature);
let f_provides = provides.remove(feature).unwrap();
let mut crate_features = f_provides.clone();
crate_features.push(feature);
let summary_suffix = if feature.is_empty() {
" - Rust source code".to_string()
} else {
match f_provides.len() {
0 => format!(" - feature \"{feature}\""),
_ => format!(" - feature \"{}\" and {} more", feature, f_provides.len()),
}
};
let description_suffix = if feature.is_empty() {
format!("Source code for Debianized Rust crate \"{crate_name}\"")
} else {
format!(
"This metapackage enables feature \"{}\" for the \
Rust {} crate, by pulling in any additional \
dependencies needed by that feature.{}",
feature,
crate_name,
match f_provides.len() {
0 => String::new(),
1 => format!(
"\n\nAdditionally, this package also provides the \
\"{}\" feature.",
f_provides[0],
),
_ => format!(
"\n\nAdditionally, this package also provides the \
\"{}\", and \"{}\" features.",
f_provides[..f_provides.len() - 1].join("\", \""),
f_provides[f_provides.len() - 1],
),
},
)
};
let mut package = Package::new(
base_pkgname,
name_suffix,
crate_info.version(),
Description {
prefix: summary_prefix.clone(),
suffix: summary_suffix.clone(),
},
Description {
prefix: description_prefix.clone(),
suffix: description_suffix.clone(),
},
if feature.is_empty() {
None
} else {
Some(feature)
},
&f_deps,
deb_deps(config.allow_prerelease_deps, &o_deps)?,
&f_provides.clone(),
if feature.is_empty() { &recommends } else { &[] },
if feature.is_empty() { &suggests } else { &[] },
crate_info.rust_version().as_deref(),
)?;
package.apply_overrides(config, pk, &f_provides);
if package.summary_check_len().is_err() {
writeln!(
control,
concat!(
"\n",
"# FIXME (packages.\"(name)\".section) debcargo ",
"auto-generated summary for {} is very long, consider overriding"
),
package.name(),
)?;
}
write!(control, "\n{package}")?;
if !feature.is_empty() {
let mut overrides =
io::BufWriter::new(file(&format!("{}.lintian-overrides", package.name()))?);
write!(
overrides,
"{} binary: empty-rust-library-declares-provides *",
package.name()
)?;
}
if !no_features_edge_case {
for f in crate_features {
let (feature_deps, _) = transitive_deps(&features_with_deps, f)?;
let mut args = if f == "default" || feature_deps.contains(&"default") {
vec![]
} else {
vec!["--no-default-features"]
};
if !f.is_empty() && f != "default" {
args.push("--features");
args.push(f);
}
let test_depends =
generate_test_dependencies(f, &feature_deps, config, &test_deps);
let test_arch = test_architecture(f)?.unwrap_or_default();
let test_arch: Vec<&str> = test_arch.iter().map(AsRef::as_ref).collect();
let mut test_restrictions = match test_restrictions(f)? {
Some(v) => v.to_owned(),
None => vec![PkgTestRestriction::AllowStderr],
};
if test_is_broken(f)? && !test_restrictions.contains(&PkgTestRestriction::Flaky)
{
test_restrictions.push(PkgTestRestriction::Flaky);
}
let test_command = test_command(f)?.map(String::as_str);
let pkgtest = PkgTest::new(
package.name(),
crate_name,
test_command.unwrap_or("{stock_cmd}"),
f,
&plain_upstream_version,
&args,
&test_depends,
&test_restrictions,
&test_arch,
)?;
write!(testctl, "\n{pkgtest}")?;
}
}
}
assert!(provides.is_empty());
}
if !bins.is_empty() {
let summary_suffix = String::new();
let description_suffix = format!(
"This package contains the following binaries built from the Rust crate\n\"{}\":\n - {}",
crate_name,
bins.join("\n - ")
);
let mut bin_pkg = Package::new_bin(
bin_name,
name_suffix,
if lib {
Some("FIXME-(packages.\"(name)\".section)")
} else {
None
},
Description {
prefix: summary_prefix,
suffix: summary_suffix,
},
Description {
prefix: description_prefix,
suffix: description_suffix,
},
);
bin_pkg.apply_overrides(config, PackageKey::Bin, &[]);
write!(control, "\n{bin_pkg}")?;
}
for configured in config.configured_packages() {
if let PackageKey::Extra(package) = configured {
let mut extra_pkg = Package::new_extra(package.to_string());
extra_pkg.apply_overrides(config, configured, &[]);
write!(control, "\n{extra_pkg}")?;
}
}
Ok((source, has_dev_deps, test_is_broken("default")?))
}
fn git_clone_url(browse: &str) -> Option<String> {
let rest = browse
.strip_prefix("https://")
.or_else(|| browse.strip_prefix("http://"))?;
let (host, path) = rest.split_once('/')?;
let is_gitlab = host == "gitlab.com" || host.starts_with("gitlab.");
let project = if is_gitlab {
let path = path.split("/-/").next().unwrap_or(path);
let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
if segments.len() < 2 {
return None;
}
segments.join("/")
} else {
let is_known_host = host == "github.com"
|| host == "codeberg.org"
|| host == "bitbucket.org"
|| host.starts_with("git.");
let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
if !is_known_host || segments.len() != 2 {
return None;
}
segments.join("/")
};
let project = project.strip_suffix(".git").unwrap_or(&project);
Some(format!("https://{host}/{project}.git"))
}
fn generate_homepage(
name: &str,
version: &str,
homepage: Option<&str>,
repository: Option<&str>,
on_crates_io: bool,
) -> String {
let fallback = if on_crates_io {
format!("https://crates.io/crates/{name}/{version}",)
} else {
String::new()
};
homepage.or(repository).unwrap_or(&fallback).to_owned()
}
fn generate_test_dependencies(
f: &str,
feature_deps: &[&str],
config: &Config,
test_deps: &[String],
) -> Vec<String> {
Some(f)
.iter()
.chain(feature_deps)
.flat_map(|f| {
config
.package_test_depends(PackageKey::feature(f))
.into_iter()
.flatten()
})
.map(ToString::to_string)
.chain(test_deps.to_owned())
.collect::<Vec<_>>()
}
fn collapse_features(
orig_features_with_deps: &CrateDepInfo,
) -> (BTreeMap<&'static str, Vec<&'static str>>, CrateDepInfo) {
let (provides, deps) = orig_features_with_deps.iter().fold(
(Vec::new(), Vec::new()),
|(mut provides, mut deps), (f, (_, f_deps))| {
if f != &"" {
provides.push(*f);
}
deps.append(&mut f_deps.clone());
(provides, deps)
},
);
let mut collapsed_provides = BTreeMap::new();
collapsed_provides.insert("", provides);
let mut collapsed_features_with_deps = BTreeMap::new();
collapsed_features_with_deps.insert("", (Vec::new(), deps));
(collapsed_provides, collapsed_features_with_deps)
}
fn reduce_provides(
mut features_with_deps: CrateDepInfo,
) -> (BTreeMap<&'static str, Vec<&'static str>>, CrateDepInfo) {
let mut features_rev_deps = HashMap::new();
for (&f, dep) in &features_with_deps {
if !features_rev_deps.contains_key(dep) {
features_rev_deps.insert(dep.clone(), vec![]);
}
features_rev_deps.get_mut(dep).unwrap().push(f);
}
for (_, ff) in features_rev_deps {
let f0 = ff[0];
for f in &ff[1..] {
features_with_deps.insert(f, (vec![f0], vec![]));
}
}
let mut provides = BTreeMap::new();
let mut provided = Vec::new();
for (&f, (ref ff, ref dd)) in &features_with_deps {
if !dd.is_empty() {
continue;
}
assert!(!ff.is_empty() || f.is_empty());
let k = if ff.len() == 1 {
ff[0]
} else {
continue;
};
if !provides.contains_key(k) {
provides.insert(k, vec![]);
}
provides.get_mut(k).unwrap().push(f);
provided.push(f);
}
for p in provided {
features_with_deps.remove(p);
}
let provides = features_with_deps
.keys()
.map(|k| {
(
*k,
traverse_depth(&|k: &&'static str| provides.get(k), k)
.into_iter()
.collect::<Vec<_>>(),
)
})
.collect::<BTreeMap<_, _>>();
(provides, features_with_deps)
}
pub(crate) fn toolchain_deps(min_rust_version: Option<&str>) -> Vec<String> {
let rustc = rustc_dep(min_rust_version, true);
["cargo:native".into(), rustc, "libstd-rust-dev".into()].into()
}
fn rustc_dep(min_ver: Option<&str>, native: bool) -> String {
let native = if native { ":native" } else { "" };
if let Some(min_ver) = min_ver {
format!("rustc{native} (>= {min_ver})")
} else {
format!("rustc{native}")
}
}
fn changelog_or_new(tempdir: &Path) -> Result<(fs::File, String)> {
let mut changelog = fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(tempdir.join("changelog"))?;
let mut changelog_data = String::new();
changelog.read_to_string(&mut changelog_data)?;
Ok((changelog, changelog_data))
}
fn changelog_first_last(tempdir: &Path) -> Result<(i32, i32)> {
let mut changelog = fs::File::open(tempdir.join("changelog"))?;
let mut changelog_data = String::new();
changelog.read_to_string(&mut changelog_data)?;
let mut last = None;
let mut first = None;
for x in ChangelogIterator::from(&changelog_data) {
let e = ChangelogEntry::from_str(x)?;
if last.is_none() {
last = Some(e.date.year());
}
first = Some(e.date.year());
}
if let Some(last) = last {
Ok((first.unwrap(), last))
} else {
Err(format_err!("changelog had no entries"))
}
}
#[cfg(test)]
mod test {
use super::{generate_homepage, git_clone_url, rustc_dep};
#[test]
fn git_clone_url_github() {
assert_eq!(
Some("https://github.com/foo/bar.git".to_owned()),
git_clone_url("https://github.com/foo/bar")
);
}
#[test]
fn git_clone_url_strips_existing_git_suffix() {
assert_eq!(
Some("https://github.com/foo/bar.git".to_owned()),
git_clone_url("https://github.com/foo/bar.git")
);
}
#[test]
fn git_clone_url_gitlab_instance() {
assert_eq!(
Some("https://gitlab.example.com/foo/bar.git".to_owned()),
git_clone_url("https://gitlab.example.com/foo/bar")
);
}
#[test]
fn git_clone_url_gitlab_nested_groups() {
assert_eq!(
Some("https://gitlab.com/group/sub/project.git".to_owned()),
git_clone_url("https://gitlab.com/group/sub/project")
);
}
#[test]
fn git_clone_url_gitlab_subpage_after_dash_delimiter() {
assert_eq!(
Some("https://gitlab.com/group/sub/project.git".to_owned()),
git_clone_url("https://gitlab.com/group/sub/project/-/tree/main/lib")
);
}
#[test]
fn git_clone_url_gitlab_group_only_is_not_a_repo() {
assert_eq!(None, git_clone_url("https://gitlab.com/group"));
}
#[test]
fn git_clone_url_subpath_is_not_a_repo_root() {
assert_eq!(None, git_clone_url("https://github.com/foo/bar/tree/main"));
}
#[test]
fn git_clone_url_unknown_host() {
assert_eq!(None, git_clone_url("https://example.com/foo/bar"));
}
#[test]
fn rustc_dep_includes_minver() {
assert_eq!("rustc:native (>= 1.65)", rustc_dep(Some("1.65"), true));
}
#[test]
fn rustc_dep_excludes_minver() {
assert_eq!("rustc:native", rustc_dep(None, true));
}
#[test]
fn rustc_dep_includes_minver_autopkgtest() {
assert_eq!("rustc (>= 1.65)", rustc_dep(Some("1.65"), false));
}
#[test]
fn rustc_dep_excludes_minver_autopkgtest() {
assert_eq!("rustc", rustc_dep(None, false));
}
#[test]
fn homepage_is_homepage() {
assert_eq!(
"https://example.com",
generate_homepage(
"crate",
"1.0",
Some("https://example.com"),
Some("https://example.com/repo"),
true
)
);
assert_eq!(
"https://example.com",
generate_homepage("crate", "1.0", Some("https://example.com"), None, true)
);
assert_eq!(
"https://example.com",
generate_homepage(
"crate",
"1.0",
Some("https://example.com"),
Some("https://example.com/repo"),
false
)
);
assert_eq!(
"https://example.com",
generate_homepage("crate", "1.0", Some("https://example.com"), None, false)
);
}
#[test]
fn homepage_is_repository() {
assert_eq!(
"https://example.com/repo",
generate_homepage("crate", "1.0", None, Some("https://example.com/repo"), true)
);
assert_eq!(
"https://example.com/repo",
generate_homepage("crate", "1.0", None, Some("https://example.com/repo"), true)
);
assert_eq!(
"https://example.com/repo",
generate_homepage(
"crate",
"1.0",
None,
Some("https://example.com/repo"),
false
)
);
assert_eq!(
"https://example.com/repo",
generate_homepage(
"crate",
"1.0",
None,
Some("https://example.com/repo"),
false
)
);
}
#[test]
fn homepage_is_fallback() {
assert_eq!(
"https://crates.io/crates/crate/1.0",
generate_homepage("crate", "1.0", None, None, true)
);
assert_eq!("", generate_homepage("crate", "1.0", None, None, false));
}
}