use crate::config::CargoConfig;
use crate::dependencies::resolve;
use crate::{dh_installsystemd, debian_architecture_from_rust_triple};
use crate::error::{CDResult, CargoDebError};
use crate::listener::Listener;
use crate::ok_or::OkOrThen;
use crate::pathbytes::AsUnixPathBytes;
use crate::util::read_file_to_bytes;
use cargo_toml::DebugSetting;
use cargo_toml::OptionalFile;
use rayon::prelude::*;
use serde::Deserialize;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::env::consts::EXE_SUFFIX;
use std::env::consts::{DLL_PREFIX, DLL_SUFFIX};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::SystemTime;
fn is_glob_pattern(s: &Path) -> bool {
s.to_bytes().iter().any(|&c| c == b'*' || c == b'[' || c == b']' || c == b'!')
}
#[derive(Debug, Clone)]
pub enum AssetSource {
Path(PathBuf),
Symlink(PathBuf),
Data(Vec<u8>),
}
impl AssetSource {
#[must_use]
pub fn from_path(path: impl Into<PathBuf>, preserve_existing_symlink: bool) -> Self {
let path = path.into();
if preserve_existing_symlink || !path.exists() { if let Ok(md) = fs::symlink_metadata(&path) {
if md.is_symlink() {
return Self::Symlink(path)
}
}
}
Self::Path(path)
}
#[must_use]
pub fn path(&self) -> Option<&Path> {
match self {
AssetSource::Symlink(ref p) |
AssetSource::Path(ref p) => Some(p),
_ => None,
}
}
#[must_use] pub fn archive_as_symlink_only(&self) -> bool {
matches!(self, AssetSource::Symlink(_))
}
#[must_use]
pub fn file_size(&self) -> Option<u64> {
match *self {
AssetSource::Path(ref p) => fs::metadata(p).ok().map(|m| m.len()),
AssetSource::Data(ref d) => Some(d.len() as u64),
AssetSource::Symlink(_) => None,
}
}
pub fn data(&self) -> CDResult<Cow<'_, [u8]>> {
Ok(match self {
AssetSource::Path(p) => {
let data = read_file_to_bytes(p)
.map_err(|e| CargoDebError::IoFile("unable to read asset to add to archive", e, p.to_owned()))?;
Cow::Owned(data)
},
AssetSource::Data(d) => Cow::Borrowed(d),
AssetSource::Symlink(_) => return Err(CargoDebError::Str("Symlink unexpectedly used to read file data")),
})
}
#[must_use]
pub fn debug_source(&self) -> Option<PathBuf> {
match self {
AssetSource::Path(p) |
AssetSource::Symlink(p) => Some(debug_filename(p)),
_ => None,
}
}
}
#[derive(Clone, Debug, Deserialize, Default)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub(crate) struct SystemdUnitsConfig {
pub unit_scripts: Option<PathBuf>,
pub unit_name: Option<String>,
pub enable: Option<bool>,
pub start: Option<bool>,
pub restart_after_upgrade: Option<bool>,
pub stop_on_upgrade: Option<bool>,
}
impl From<&SystemdUnitsConfig> for dh_installsystemd::Options {
fn from(config: &SystemdUnitsConfig) -> Self {
Self {
no_enable: !config.enable.unwrap_or(true),
no_start: !config.start.unwrap_or(true),
restart_after_upgrade: config.restart_after_upgrade.unwrap_or(true),
no_stop_on_upgrade: !config.stop_on_upgrade.unwrap_or(true),
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct Assets {
pub unresolved: Vec<UnresolvedAsset>,
pub resolved: Vec<Asset>,
}
impl Assets {
fn new() -> Assets {
Assets {
unresolved: vec![],
resolved: vec![],
}
}
fn with_resolved_assets(assets: Vec<Asset>) -> Assets {
Assets {
unresolved: vec![],
resolved: assets,
}
}
fn with_unresolved_assets(assets: Vec<UnresolvedAsset>) -> Assets {
Assets {
unresolved: assets,
resolved: vec![],
}
}
fn is_empty(&self) -> bool {
self.unresolved.is_empty() && self.resolved.is_empty()
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum IsBuilt {
No,
SamePackage,
Workspace,
}
#[derive(Debug, Clone)]
pub struct UnresolvedAsset {
pub source_path: PathBuf,
pub c: AssetCommon,
}
#[derive(Debug, Clone)]
pub struct AssetCommon {
pub target_path: PathBuf,
pub chmod: u32,
is_built: IsBuilt,
is_example: bool,
}
#[derive(Debug, Clone)]
pub struct Asset {
pub source: AssetSource,
pub c: AssetCommon,
}
impl Asset {
#[must_use]
pub fn new(source: AssetSource, mut target_path: PathBuf, chmod: u32, is_built: IsBuilt, is_example: bool) -> Self {
if target_path.to_string_lossy().ends_with('/') {
let file_name = source.path().and_then(|p| p.file_name()).expect("source must be a file");
target_path = target_path.join(file_name);
}
if target_path.is_absolute() || target_path.has_root() {
target_path = target_path.strip_prefix("/").expect("no root dir").to_owned();
}
Self {
source,
c: AssetCommon {
target_path, chmod, is_built, is_example,
},
}
}
}
impl AssetCommon {
fn is_executable(&self) -> bool {
0 != self.chmod & 0o111
}
fn is_dynamic_library(&self) -> bool {
is_dynamic_library_filename(&self.target_path)
}
#[must_use]
pub fn debug_target(&self) -> Option<PathBuf> {
if self.is_built != IsBuilt::No {
let relative = match self.target_path.strip_prefix(Path::new("/")) {
Ok(path) => path,
Err(_) => self.target_path.as_path(),
};
let debug_path = Path::new("/usr/lib/debug").join(relative);
Some(debug_filename(&debug_path))
} else {
None
}
}
}
fn debug_filename(path: &Path) -> PathBuf {
let mut debug_filename = path.as_os_str().to_os_string();
debug_filename.push(".debug");
Path::new(&debug_filename).to_path_buf()
}
fn is_dynamic_library_filename(path: &Path) -> bool {
path.file_name()
.and_then(|f| f.to_str())
.map_or(false, |f| f.ends_with(DLL_SUFFIX))
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum ArchSpec {
Require(String),
NegRequire(String),
}
fn get_architecture_specification(depend: &str) -> CDResult<(String, Option<ArchSpec>)> {
use ArchSpec::*;
let re = regex::Regex::new(r#"(.*)\[(!?)(.*)\]"#).unwrap();
match re.captures(depend) {
Some(caps) => {
let spec = if &caps[2] == "!" {
NegRequire(caps[3].to_string())
} else {
assert_eq!(&caps[2], "");
Require(caps[3].to_string())
};
Ok((caps[1].trim().to_string(), Some(spec)))
}
None => Ok((depend.to_string(), None)),
}
}
fn match_architecture(spec: ArchSpec, target_arch: &str) -> CDResult<bool> {
let (neg, spec) = match spec {
ArchSpec::NegRequire(pkg) => (true, pkg),
ArchSpec::Require(pkg) => (false, pkg),
};
let output = Command::new("dpkg-architecture")
.args(["-a", target_arch, "-i", &spec])
.output()
.map_err(|e| CargoDebError::CommandFailed(e, "dpkg-architecture"))?;
if neg {
Ok(!output.status.success())
} else {
Ok(output.status.success())
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct Config {
pub package_manifest_dir: PathBuf,
pub deb_output_path: Option<String>,
pub target: Option<String>,
pub target_dir: PathBuf,
pub name: String,
pub deb_name: String,
pub deb_version: String,
pub license: Option<String>,
pub license_file: Option<PathBuf>,
pub license_file_skip_lines: usize,
pub copyright: String,
pub changelog: Option<String>,
pub homepage: Option<String>,
pub documentation: Option<String>,
pub repository: Option<String>,
pub description: String,
pub extended_description: Option<String>,
pub maintainer: String,
pub depends: String,
pub pre_depends: Option<String>,
pub recommends: Option<String>,
pub suggests: Option<String>,
pub enhances: Option<String>,
pub section: Option<String>,
pub priority: String,
pub conflicts: Option<String>,
pub breaks: Option<String>,
pub replaces: Option<String>,
pub provides: Option<String>,
pub architecture: String,
pub conf_files: Option<String>,
pub(crate) assets: Assets,
pub triggers_file: Option<PathBuf>,
pub maintainer_scripts: Option<PathBuf>,
pub features: Vec<String>,
pub default_features: bool,
pub debug_enabled: bool,
pub separate_debug_symbols: bool,
pub preserve_symlinks: bool,
pub(crate) systemd_units: Option<Vec<SystemdUnitsConfig>>,
pub default_timestamp: u64,
}
impl Config {
pub fn from_manifest(manifest_path: &Path, selected_package_name: Option<&str>, output_path: Option<String>, target: Option<&str>, variant: Option<&str>, deb_version: Option<String>, deb_revision: Option<String>, listener: &dyn Listener, selected_profile: &str) -> CDResult<Config> {
let metadata = cargo_metadata(manifest_path)?;
let available_package_names = || {
metadata.packages.iter()
.filter(|p| metadata.workspace_members.iter().any(|w| w == &p.id))
.map(|p| p.name.as_str())
.collect::<Vec<_>>().join(", ")
};
let target_package = if let Some(name) = selected_package_name {
metadata.packages.iter().find(|p| p.name == name)
.ok_or_else(|| CargoDebError::PackageNotFoundInWorkspace(name.into(), available_package_names()))
} else {
metadata.resolve.root.as_ref().and_then(|root_id| {
metadata.packages.iter()
.find(move |p| &p.id == root_id)
})
.ok_or_else(|| CargoDebError::NoRootFoundInWorkspace(available_package_names()))
}?;
let workspace_root_manifest_path = Path::new(&metadata.workspace_root).join("Cargo.toml");
let workspace_root_manifest = cargo_toml::Manifest::<CargoPackageMetadata>::from_path_with_metadata(workspace_root_manifest_path).ok();
let target_dir = Path::new(&metadata.target_directory);
let manifest_path = Path::new(&target_package.manifest_path);
let package_manifest_dir = manifest_path.parent().unwrap();
let manifest_bytes =
fs::read(manifest_path).map_err(|e| CargoDebError::IoFile("unable to read manifest", e, manifest_path.to_owned()))?;
let manifest_mdate = std::fs::metadata(manifest_path)?.modified().unwrap_or_else(|_| SystemTime::now());
let default_timestamp = if let Ok(source_date_epoch) = std::env::var("SOURCE_DATE_EPOCH") {
source_date_epoch.parse().map_err(|e| CargoDebError::NumParse("SOURCE_DATE_EPOCH", e))?
} else {
manifest_mdate.duration_since(SystemTime::UNIX_EPOCH).map_err(CargoDebError::SystemTime)?.as_secs()
};
let mut manifest = cargo_toml::Manifest::<CargoPackageMetadata>::from_slice_with_metadata(&manifest_bytes)
.map_err(|e| CargoDebError::TomlParsing(e, manifest_path.into()))?;
let ws_root = workspace_root_manifest.as_ref().map(|ws| (ws, Path::new(&metadata.workspace_root)));
manifest.complete_from_path_and_workspace(manifest_path, ws_root)
.map_err(move |e| CargoDebError::TomlParsing(e, manifest_path.to_path_buf()))?;
Self::from_manifest_inner(manifest, workspace_root_manifest.as_ref(), target_package, package_manifest_dir, output_path, target_dir, target, variant, deb_version, deb_revision, listener, selected_profile, default_timestamp)
}
fn from_manifest_inner(
mut manifest: cargo_toml::Manifest<CargoPackageMetadata>,
root_manifest: Option<&cargo_toml::Manifest<CargoPackageMetadata>>,
cargo_metadata: &CargoMetadataPackage,
package_manifest_dir: &Path,
deb_output_path: Option<String>,
target_dir: &Path,
target: Option<&str>,
variant: Option<&str>,
deb_version: Option<String>,
deb_revision: Option<String>,
listener: &dyn Listener,
selected_profile: &str,
default_timestamp: u64,
) -> CDResult<Self> {
let target_dir = if let Some(target) = target {
target_dir.join(target)
} else {
target_dir.to_owned()
};
let debug_enabled = if selected_profile == "release" {
debug_flag(&manifest) || root_manifest.map_or(false, debug_flag)
} else {
false
};
let package = manifest.package.as_mut().unwrap();
let mut deb = if let Some(variant) = variant {
package.name = format!("{}-{variant}", package.name);
let mut deb = package.metadata.take()
.and_then(|m| m.deb).unwrap_or_default();
let variant = deb.variants
.as_mut()
.and_then(|v| v.remove(variant))
.ok_or_else(|| CargoDebError::VariantNotFound(variant.to_string()))?;
variant.inherit_from(deb)
} else {
package.metadata.take().and_then(|m| m.deb).unwrap_or_default()
};
let (license_file, license_file_skip_lines) = manifest_license_file(package, deb.license_file.as_ref())?;
manifest_check_config(package, package_manifest_dir, &deb, listener);
let extended_description = manifest_extended_description(
deb.extended_description.take(),
deb.extended_description_file.as_ref().map(Path::new).or(package.readme().as_path()),
)?;
if deb_version.is_none() && deb_revision.is_none() && deb.revision.is_none() {
listener.warning("The next version of cargo-deb will add a \"-1\" suffix to versions.\nUse --deb-revision=\"\" or package.metadata.deb.revision=\"\" to keep the current behavior.".to_string());
}
let mut config = Config {
default_timestamp,
package_manifest_dir: package_manifest_dir.to_owned(),
deb_output_path,
target: target.map(|t| t.to_string()),
target_dir,
name: package.name.clone(),
deb_name: deb.name.take().unwrap_or_else(|| debian_package_name(&package.name)),
deb_version: deb_version.unwrap_or_else(|| manifest_version_string(package, deb_revision.or(deb.revision))),
license: package.license.take().map(|v| v.unwrap()),
license_file,
license_file_skip_lines,
copyright: deb.copyright.take().ok_or_then(|| {
if package.authors().is_empty() {
return Err("The package must have a copyright or authors property".into());
}
Ok(package.authors().join(", "))
})?,
homepage: package.homepage().map(From::from),
documentation: package.documentation().map(From::from),
repository: package.repository.take().map(|v| v.unwrap()),
description: package.description.take().map(|v| v.unwrap()).unwrap_or_else(||format!("[generated from Rust crate {}]", package.name)),
extended_description,
maintainer: deb.maintainer.take().ok_or_then(|| {
Ok(package.authors().get(0)
.ok_or("The package must have a maintainer or authors property")?.to_owned())
})?,
depends: deb.depends.take().map(DependencyList::into_depends_string).unwrap_or_else(|| "$auto".to_owned()),
pre_depends: deb.pre_depends.take().map(DependencyList::into_depends_string),
recommends: deb.recommends.take().map(DependencyList::into_depends_string),
suggests: deb.suggests.take().map(DependencyList::into_depends_string),
enhances: deb.enhances.take(),
conflicts: deb.conflicts.take(),
breaks: deb.breaks.take(),
replaces: deb.replaces.take(),
provides: deb.provides.take(),
section: deb.section.take(),
priority: deb.priority.take().unwrap_or_else(|| "optional".to_owned()),
architecture: debian_architecture_from_rust_triple(target.unwrap_or(crate::DEFAULT_TARGET)).to_owned(),
conf_files: deb.conf_files.map(|x| format_conffiles(&x)),
assets: Assets::new(),
triggers_file: deb.triggers_file.map(PathBuf::from),
changelog: deb.changelog.take(),
maintainer_scripts: deb.maintainer_scripts.map(PathBuf::from),
features: deb.features.take().unwrap_or_default(),
default_features: deb.default_features.unwrap_or(true),
separate_debug_symbols: deb.separate_debug_symbols.unwrap_or(false),
debug_enabled,
preserve_symlinks: deb.preserve_symlinks.unwrap_or(false),
systemd_units: match deb.systemd_units {
None => None,
Some(SystemUnitsSingleOrMultiple::Single(s)) => Some(vec![s]),
Some(SystemUnitsSingleOrMultiple::Multi(v)) => Some(v),
},
};
config.take_assets(package, deb.assets.take(), &cargo_metadata.targets, selected_profile, listener)?;
config.add_copyright_asset()?;
config.add_changelog_asset()?;
config.add_systemd_assets()?;
Ok(config)
}
pub(crate) fn get_dependencies(&self, listener: &dyn Listener) -> CDResult<String> {
let mut deps = HashSet::new();
for word in self.depends.split(',') {
let word = word.trim();
if word == "$auto" {
let bin = self.all_binaries();
let resolved = bin.par_iter()
.filter(|bin| !bin.archive_as_symlink_only())
.filter_map(|p| p.path())
.filter_map(|bname| match resolve(bname, &self.target) {
Ok(bindeps) => Some(bindeps),
Err(err) => {
listener.warning(format!("{} (no auto deps for {})", err, bname.display()));
None
},
})
.collect::<Vec<_>>();
for dep in resolved.into_iter().flat_map(|s| s.into_iter()) {
deps.insert(dep);
}
} else {
let (dep, arch_spec) = get_architecture_specification(word)?;
if let Some(spec) = arch_spec {
if match_architecture(spec, &self.architecture)? {
deps.insert(dep);
}
} else {
deps.insert(dep);
}
}
}
Ok(deps.into_iter().collect::<Vec<_>>().join(", "))
}
pub fn extend_cargo_build_flags(&self, flags: &mut Vec<String>) {
if flags.iter().any(|f| f == "--workspace" || f == "--all") {
return;
}
for a in self.assets.unresolved.iter().filter(|a| a.c.is_built != IsBuilt::No) {
if is_glob_pattern(&a.source_path) {
log::debug!("building entire workspace because of glob {}", a.source_path.display());
flags.push("--workspace".into());
return;
}
}
let mut build_bins = vec![];
let mut build_examples = vec![];
let mut build_libs = false;
let mut same_package = true;
let resolved = self.assets.resolved.iter().map(|a| (&a.c, a.source.path()));
let unresolved = self.assets.unresolved.iter().map(|a| (&a.c, Some(a.source_path.as_ref())));
for (asset_target, source_path) in resolved.chain(unresolved).filter(|(c,_)| c.is_built != IsBuilt::No) {
if asset_target.is_built != IsBuilt::SamePackage {
log::debug!("building workspace because {} is from another package", source_path.unwrap_or(&asset_target.target_path).display());
same_package = false;
}
if asset_target.is_dynamic_library() || source_path.map_or(false, is_dynamic_library_filename) {
log::debug!("building libs for {}", source_path.unwrap_or(&asset_target.target_path).display());
build_libs = true;
} else if asset_target.is_executable() {
if let Some(source_path) = source_path {
let name = source_path.file_name().unwrap().to_str().expect("utf-8 target name");
let name = name.strip_suffix(EXE_SUFFIX).unwrap_or(name);
if asset_target.is_example {
build_examples.push(name);
} else {
build_bins.push(name);
}
}
}
}
if !same_package {
flags.push("--workspace".into());
}
flags.extend(build_bins.iter().map(|name| {
log::debug!("building bin for {}", name);
format!("--bin={name}")
}));
flags.extend(build_examples.iter().map(|name| {
log::debug!("building example for {}", name);
format!("--example={name}")
}));
if build_libs {
flags.push("--lib".into());
}
}
pub fn resolve_assets(&mut self) -> CDResult<()> {
for UnresolvedAsset { source_path, c: AssetCommon { target_path, chmod, is_built, is_example } } in self.assets.unresolved.drain(..) {
let source_prefix: PathBuf = source_path.iter()
.take_while(|part| !is_glob_pattern(part.as_ref()))
.collect();
let source_is_glob = is_glob_pattern(&source_path);
let file_matches = glob::glob(source_path.to_str().expect("utf8 path"))?
.map(|entry| {
let source_file = entry?;
Ok(if source_file.is_dir() { None } else { Some(source_file) })
})
.filter_map(|res| match res {
Ok(None) => None,
Ok(Some(x)) => Some(Ok(x)),
Err(x) => Some(Err(x)),
})
.collect::<CDResult<Vec<_>>>()?;
if file_matches.is_empty() {
return Err(CargoDebError::AssetFileNotFound(source_path));
}
for source_file in file_matches {
let target_file = if source_is_glob {
target_path.join(source_file.strip_prefix(&source_prefix).unwrap())
} else {
target_path.clone()
};
log::debug!("asset {} -> {} {} {:o}", source_file.display(), target_file.display(), if is_built == IsBuilt::No {"copy"} else {"build"}, chmod);
self.assets.resolved.push(Asset::new(
AssetSource::from_path(source_file, self.preserve_symlinks),
target_file,
chmod,
is_built,
is_example,
));
}
}
self.sort_assets_by_type();
Ok(())
}
pub(crate) fn add_copyright_asset(&mut self) -> CDResult<()> {
let copyright_file = crate::data::generate_copyright_asset(self)?;
log::debug!("added copyright");
self.assets.resolved.push(Asset::new(
AssetSource::Data(copyright_file),
Path::new("usr/share/doc").join(&self.deb_name).join("copyright"),
0o644,
IsBuilt::No,
false,
));
Ok(())
}
pub fn add_debug_assets(&mut self, original_binaries: Vec<Asset>) {
let mut assets_to_add: Vec<Asset> = Vec::new();
for asset in original_binaries.into_iter().filter(|a| a.source.path().is_some()) {
let debug_source = asset.source.debug_source().expect("debug asset");
if debug_source.exists() {
log::debug!("added debug file {}", debug_source.display());
let debug_target = asset.c.debug_target().expect("debug asset");
assets_to_add.push(Asset::new(
AssetSource::Path(debug_source),
debug_target,
0o644,
IsBuilt::No,
false,
));
} else {
log::debug!("no debug file {}", debug_source.display());
}
}
self.assets.resolved.append(&mut assets_to_add);
}
fn add_changelog_asset(&mut self) -> CDResult<()> {
if self.changelog.is_some() {
if let Some(changelog_file) = crate::data::generate_changelog_asset(self)? {
log::debug!("added changelog");
self.assets.resolved.push(Asset::new(
AssetSource::Data(changelog_file),
Path::new("usr/share/doc").join(&self.deb_name).join("changelog.Debian.gz"),
0o644,
IsBuilt::No,
false,
));
}
}
Ok(())
}
fn add_systemd_assets(&mut self) -> CDResult<()> {
if let Some(ref config_vec) = self.systemd_units {
for config in config_vec {
let units_dir_option = config.unit_scripts.as_ref()
.or(self.maintainer_scripts.as_ref());
if let Some(unit_dir) = units_dir_option {
let search_path = self.path_in_package(unit_dir);
let package = &self.name;
let unit_name = config.unit_name.as_deref();
let units = dh_installsystemd::find_units(&search_path, package, unit_name);
for (source, target) in units {
self.assets.resolved.push(Asset::new(
AssetSource::from_path(source, self.preserve_symlinks), target.path,
target.mode,
IsBuilt::No,
false,
));
}
}
}
} else {
log::debug!("no systemd units to generate");
}
Ok(())
}
fn all_binaries(&self) -> Vec<&AssetSource> {
self.assets.resolved.iter()
.filter(|asset| {
asset.c.is_dynamic_library() || asset.c.is_executable()
})
.map(|asset| &asset.source)
.collect()
}
pub(crate) fn built_binaries_mut(&mut self) -> Vec<&mut Asset> {
self.assets.resolved.iter_mut()
.filter(move |asset| {
asset.c.is_built != IsBuilt::No && (asset.c.is_dynamic_library() || asset.c.is_executable())
})
.collect()
}
pub(crate) fn repository_type(&self) -> Option<&str> {
if let Some(ref repo) = self.repository {
if repo.starts_with("git+")
|| repo.ends_with(".git")
|| repo.contains("git@")
|| repo.contains("github.com")
|| repo.contains("gitlab.com")
{
return Some("Git");
}
if repo.starts_with("cvs+") || repo.contains("pserver:") || repo.contains("@cvs.") {
return Some("Cvs");
}
if repo.starts_with("hg+") || repo.contains("hg@") || repo.contains("/hg.") {
return Some("Hg");
}
if repo.starts_with("svn+") || repo.contains("/svn.") {
return Some("Svn");
}
return None;
}
None
}
pub(crate) fn path_in_build<P: AsRef<Path>>(&self, rel_path: P, profile: &str) -> PathBuf {
self.path_in_build_(rel_path.as_ref(), profile)
}
pub(crate) fn path_in_build_(&self, rel_path: &Path, profile: &str) -> PathBuf {
let profile = match profile {
"dev" => "debug",
p => p,
};
let mut path = self.target_dir.join(profile);
path.push(rel_path);
path
}
pub(crate) fn path_in_package<P: AsRef<Path>>(&self, rel_path: P) -> PathBuf {
self.package_manifest_dir.join(rel_path)
}
pub(crate) fn deb_temp_dir(&self) -> PathBuf {
self.target_dir.join("debian").join(&self.name)
}
pub(crate) fn deb_output_path(&self, filename: &str) -> PathBuf {
if let Some(ref path_str) = self.deb_output_path {
let path = Path::new(path_str);
if path_str.ends_with('/') || path.is_dir() {
path.join(filename)
} else {
path.to_owned()
}
} else {
self.default_deb_output_dir().join(filename)
}
}
pub(crate) fn default_deb_output_dir(&self) -> PathBuf {
self.target_dir.join("debian")
}
pub(crate) fn cargo_config(&self) -> CDResult<Option<CargoConfig>> {
CargoConfig::new(&self.package_manifest_dir)
}
pub(crate) fn sort_assets_by_type(&mut self) {
self.assets.resolved.sort_by(|a,b| {
a.c.is_executable().cmp(&b.c.is_executable())
.then(a.c.is_dynamic_library().cmp(&b.c.is_dynamic_library()))
.then(a.c.target_path.extension().cmp(&b.c.target_path.extension()))
.then(a.c.target_path.parent().cmp(&b.c.target_path.parent()))
});
}
}
fn debian_package_name(crate_name: &str) -> String {
crate_name.bytes().map(|c| {
if c != b'_' {c.to_ascii_lowercase() as char} else {'-'}
}).collect()
}
fn debug_flag(manifest: &cargo_toml::Manifest<CargoPackageMetadata>) -> bool {
manifest.profile.release.as_ref()
.and_then(|r| r.debug.as_ref())
.map_or(false, |debug| match debug {
DebugSetting::None => false,
_ => true,
})
}
fn manifest_check_config(package: &cargo_toml::Package<CargoPackageMetadata>, manifest_dir: &Path, deb: &CargoDeb, listener: &dyn Listener) {
let readme = package.readme().as_path();
if package.description().is_none() {
listener.warning("description field is missing in Cargo.toml".to_owned());
}
if package.license().is_none() && package.license_file().is_none() {
listener.warning("license field is missing in Cargo.toml".to_owned());
}
if let Some(readme) = readme {
if deb.extended_description.is_none() && deb.extended_description_file.is_none() && (readme.ends_with(".md") || readme.ends_with(".markdown")) {
listener.info(format!("extended-description field missing. Using {}, but markdown may not render well.", readme.display()));
}
} else {
for p in &["README.md", "README.markdown", "README.txt", "README"] {
if manifest_dir.join(p).exists() {
listener.warning(format!("{p} file exists, but is not specified in `readme` Cargo.toml field"));
break;
}
}
}
}
fn manifest_extended_description(desc: Option<String>, desc_file: Option<&Path>) -> CDResult<Option<String>> {
Ok(if desc.is_some() {
desc
} else if let Some(desc_file) = desc_file {
Some(fs::read_to_string(desc_file)
.map_err(|err| CargoDebError::IoFile(
"unable to read extended description from file", err, PathBuf::from(desc_file)))?)
} else {
None
})
}
fn manifest_license_file(package: &cargo_toml::Package<CargoPackageMetadata>, license_file: Option<&LicenseFile>) -> CDResult<(Option<PathBuf>, usize)> {
Ok(match license_file {
Some(LicenseFile::Vec(args)) => {
let mut args = args.iter();
let file = args.next();
let lines = if let Some(lines) = args.next() {
lines.parse().map_err(|e| CargoDebError::NumParse("invalid number of lines", e))?
} else {0};
(file.map(|s|s.into()), lines)
},
Some(LicenseFile::String(s)) => {
(Some(s.into()), 0)
}
None => {
(package.license_file().as_ref().map(|s| s.into()), 0)
}
})
}
impl Config {
fn take_assets(&mut self, package: &cargo_toml::Package<CargoPackageMetadata>, assets: Option<Vec<Vec<String>>>, build_targets: &[CargoMetadataTarget], profile: &str, listener: &dyn Listener) -> CDResult<()> {
let assets = if let Some(assets) = assets {
let profile_target_dir = format!("target/{profile}");
let mut unresolved_assets = Vec::with_capacity(assets.len());
for mut asset_line in assets {
let mut asset_parts = asset_line.drain(..);
let source_path = PathBuf::from(asset_parts.next()
.ok_or("missing path (first array entry) for asset in Cargo.toml")?);
if source_path.starts_with("target/debug/") {
listener.warning(format!("Packaging of development-only binaries is intentionally unsupported in cargo-deb.
Please only use `target/release/` directory for built products, not `{}`.
To add debug information or additional assertions use `[profile.release]` in `Cargo.toml` instead.
This will be hard error in a future release of cargo-deb.", source_path.display()));
}
let (is_built, source_path, is_example) = if let Ok(rel_path) = source_path.strip_prefix("target/release").or_else(|_| source_path.strip_prefix(&profile_target_dir)) {
let is_example = rel_path.starts_with("examples");
(self.find_is_built_file_in_package(rel_path, build_targets, if is_example { "example" } else { "bin" }), self.path_in_build(rel_path, profile), is_example)
} else {
(IsBuilt::No, self.path_in_package(&source_path), false)
};
let target_path = PathBuf::from(asset_parts.next().ok_or("missing target (second array entry) for asset in Cargo.toml. Use something like \"usr/local/bin/\".")?);
let chmod = u32::from_str_radix(&asset_parts.next().ok_or("missing chmod (third array entry) for asset in Cargo.toml. Use an octal string like \"777\".")?, 8)
.map_err(|e| CargoDebError::NumParse("unable to parse chmod argument", e))?;
unresolved_assets.push(UnresolvedAsset {
source_path,
c: AssetCommon { target_path, chmod, is_built, is_example },
})
}
Assets::with_unresolved_assets(unresolved_assets)
} else {
let mut implied_assets: Vec<_> = build_targets.iter()
.filter_map(|t| {
if t.crate_types.iter().any(|ty| ty == "bin") && t.kind.iter().any(|k| k == "bin") {
Some(Asset::new(
AssetSource::Path(self.path_in_build(&t.name, profile)),
Path::new("usr/bin").join(&t.name),
0o755,
self.is_built_file_in_package(t),
false,
))
} else if t.crate_types.iter().any(|ty| ty == "cdylib") && t.kind.iter().any(|k| k == "cdylib") {
let lib_name = format!("{DLL_PREFIX}{}{DLL_SUFFIX}", t.name);
Some(Asset::new(
AssetSource::Path(self.path_in_build(&lib_name, profile)),
Path::new("usr/lib").join(lib_name),
0o644,
self.is_built_file_in_package(t),
false,
))
} else {
None
}
})
.collect();
if let OptionalFile::Path(readme) = package.readme() {
let path = PathBuf::from(readme);
let target_path = Path::new("usr/share/doc")
.join(&package.name)
.join(path.file_name().ok_or("bad README path")?);
implied_assets.push(Asset::new(AssetSource::Path(path), target_path, 0o644, IsBuilt::No, false));
}
Assets::with_resolved_assets(implied_assets)
};
if assets.is_empty() {
return Err("No binaries or cdylibs found. The package is empty. Please specify some assets to package in Cargo.toml".into());
}
self.assets = assets;
Ok(())
}
fn find_is_built_file_in_package(&self, rel_path: &Path, build_targets: &[CargoMetadataTarget], expected_kind: &str) -> IsBuilt {
let source_name = rel_path.file_name().expect("asset filename").to_str().expect("utf-8 names");
let source_name = source_name.strip_suffix(EXE_SUFFIX).unwrap_or(source_name);
if build_targets.iter()
.filter(|t| t.name == source_name && t.kind.iter().any(|k| k == expected_kind))
.any(|t| self.is_built_file_in_package(t) == IsBuilt::SamePackage)
{
IsBuilt::SamePackage
} else {
IsBuilt::Workspace
}
}
fn is_built_file_in_package(&self, build_target: &CargoMetadataTarget) -> IsBuilt {
if build_target.src_path.starts_with(&self.package_manifest_dir) {
IsBuilt::SamePackage
} else {
IsBuilt::Workspace
}
}
}
fn manifest_version_string(package: &cargo_toml::Package<CargoPackageMetadata>, revision: Option<String>) -> String {
let debianized_version;
let mut version = package.version();
let mut parts = version.splitn(2, '-');
let semver_main = parts.next().unwrap();
if let Some(semver_pre) = parts.next() {
let pre_ascii = semver_pre.as_bytes();
if pre_ascii.iter().any(|c| !c.is_ascii_digit()) && pre_ascii.iter().any(u8::is_ascii_digit) {
debianized_version = format!("{semver_main}~{semver_pre}");
version = &debianized_version;
}
}
if let Some(revision) = revision.filter(|r| !r.is_empty()) {
format!("{version}-{revision}")
} else {
version.to_owned()
}
}
#[derive(Clone, Debug, Deserialize, Default)]
struct CargoPackageMetadata {
pub deb: Option<CargoDeb>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(untagged)]
enum LicenseFile {
String(String),
Vec(Vec<String>),
}
#[derive(Deserialize)]
#[derive(Clone, Debug)]
#[serde(untagged)]
enum SystemUnitsSingleOrMultiple {
Single(SystemdUnitsConfig),
Multi(Vec<SystemdUnitsConfig>)
}
#[derive(Clone, Debug, Deserialize)]
#[serde(untagged)]
enum DependencyList {
String(String),
Vec(Vec<String>),
}
impl DependencyList {
fn into_depends_string(self) -> String {
match self {
Self::String(s) => s,
Self::Vec(vals) => vals.join(", "),
}
}
}
#[derive(Clone, Debug, Deserialize, Default)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
struct CargoDeb {
pub name: Option<String>,
pub maintainer: Option<String>,
pub copyright: Option<String>,
pub license_file: Option<LicenseFile>,
pub changelog: Option<String>,
pub depends: Option<DependencyList>,
pub pre_depends: Option<DependencyList>,
pub recommends: Option<DependencyList>,
pub suggests: Option<DependencyList>,
pub enhances: Option<String>,
pub conflicts: Option<String>,
pub breaks: Option<String>,
pub replaces: Option<String>,
pub provides: Option<String>,
pub extended_description: Option<String>,
pub extended_description_file: Option<String>,
pub section: Option<String>,
pub priority: Option<String>,
pub revision: Option<String>,
pub conf_files: Option<Vec<String>>,
pub assets: Option<Vec<Vec<String>>>,
pub triggers_file: Option<String>,
pub maintainer_scripts: Option<String>,
pub features: Option<Vec<String>>,
pub default_features: Option<bool>,
pub separate_debug_symbols: Option<bool>,
pub preserve_symlinks: Option<bool>,
pub systemd_units: Option<SystemUnitsSingleOrMultiple>,
pub variants: Option<HashMap<String, CargoDeb>>,
}
impl CargoDeb {
fn inherit_from(self, parent: CargoDeb) -> CargoDeb {
CargoDeb {
name: self.name.or(parent.name),
maintainer: self.maintainer.or(parent.maintainer),
copyright: self.copyright.or(parent.copyright),
license_file: self.license_file.or(parent.license_file),
changelog: self.changelog.or(parent.changelog),
depends: self.depends.or(parent.depends),
pre_depends: self.pre_depends.or(parent.pre_depends),
recommends: self.recommends.or(parent.recommends),
suggests: self.suggests.or(parent.suggests),
enhances: self.enhances.or(parent.enhances),
conflicts: self.conflicts.or(parent.conflicts),
breaks: self.breaks.or(parent.breaks),
replaces: self.replaces.or(parent.replaces),
provides: self.provides.or(parent.provides),
extended_description: self.extended_description.or(parent.extended_description),
extended_description_file: self.extended_description_file.or(parent.extended_description_file),
section: self.section.or(parent.section),
priority: self.priority.or(parent.priority),
revision: self.revision.or(parent.revision),
conf_files: self.conf_files.or(parent.conf_files),
assets: self.assets.or(parent.assets),
triggers_file: self.triggers_file.or(parent.triggers_file),
maintainer_scripts: self.maintainer_scripts.or(parent.maintainer_scripts),
features: self.features.or(parent.features),
default_features: self.default_features.or(parent.default_features),
separate_debug_symbols: self.separate_debug_symbols.or(parent.separate_debug_symbols),
preserve_symlinks: self.preserve_symlinks.or(parent.preserve_symlinks),
systemd_units: self.systemd_units.or(parent.systemd_units),
variants: self.variants.or(parent.variants),
}
}
}
#[derive(Deserialize)]
struct CargoMetadata {
packages: Vec<CargoMetadataPackage>,
resolve: CargoMetadataResolve,
#[serde(default)]
workspace_members: Vec<String>,
target_directory: String,
#[serde(default)]
workspace_root: String,
}
#[derive(Deserialize)]
struct CargoMetadataResolve {
root: Option<String>,
}
#[derive(Deserialize)]
struct CargoMetadataPackage {
pub id: String,
pub name: String,
pub targets: Vec<CargoMetadataTarget>,
pub manifest_path: String,
}
#[derive(Deserialize)]
struct CargoMetadataTarget {
pub name: String,
pub kind: Vec<String>,
pub crate_types: Vec<String>,
pub src_path: PathBuf,
}
fn cargo_metadata(manifest_path: &Path) -> CDResult<CargoMetadata> {
let mut cmd = Command::new("cargo");
cmd.arg("metadata");
cmd.arg("--format-version=1");
cmd.arg("--manifest-path"); cmd.arg(manifest_path);
let output = cmd.output()
.map_err(|e| CargoDebError::CommandFailed(e, "cargo (is it in your PATH?)"))?;
if !output.status.success() {
return Err(CargoDebError::CommandError("cargo", "metadata".to_owned(), output.stderr));
}
let stdout = String::from_utf8(output.stdout).unwrap();
let metadata = serde_json::from_str(&stdout)?;
Ok(metadata)
}
fn format_conffiles<S: AsRef<str>>(files: &[S]) -> String {
files.iter().fold(String::new(), |mut acc, x| {
let pth = x.as_ref();
if !pth.starts_with('/') {
acc.push('/');
}
acc + pth + "\n"
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::tests::add_test_fs_paths;
#[test]
fn match_arm_arch() {
assert_eq!("armhf", debian_architecture_from_rust_triple("arm-unknown-linux-gnueabihf"));
}
#[test]
fn arch_spec() {
use ArchSpec::*;
assert_eq!(
get_architecture_specification("libjpeg64-turbo [armhf]").expect("arch"),
("libjpeg64-turbo".to_owned(), Some(Require("armhf".to_owned()))));
assert_eq!(
get_architecture_specification("libjpeg64-turbo [!amd64]").expect("arch"),
("libjpeg64-turbo".to_owned(), Some(NegRequire("amd64".to_owned()))));
}
#[test]
fn assets() {
let a = Asset::new(
AssetSource::Path(PathBuf::from("target/release/bar")),
PathBuf::from("baz/"),
0o644,
IsBuilt::SamePackage,
false,
);
assert_eq!("baz/bar", a.c.target_path.to_str().unwrap());
assert!(a.c.is_built != IsBuilt::No);
let a = Asset::new(
AssetSource::Path(PathBuf::from("foo/bar")),
PathBuf::from("/baz/quz"),
0o644,
IsBuilt::No,
false,
);
assert_eq!("baz/quz", a.c.target_path.to_str().unwrap());
assert!(a.c.is_built == IsBuilt::No);
}
#[test]
fn test_debug_filename() {
let path = Path::new("/my/test/file");
assert_eq!(debug_filename(path), Path::new("/my/test/file.debug"));
}
#[test]
fn test_debug_target_ok() {
let a = Asset::new(
AssetSource::Path(PathBuf::from("target/release/bar")),
PathBuf::from("/usr/bin/baz/"),
0o644,
IsBuilt::SamePackage,
false,
);
let debug_target = a.c.debug_target().expect("Got unexpected None");
assert_eq!(debug_target, Path::new("/usr/lib/debug/usr/bin/baz/bar.debug"));
}
#[test]
fn test_debug_target_ok_relative() {
let a = Asset::new(
AssetSource::Path(PathBuf::from("target/release/bar")),
PathBuf::from("baz/"),
0o644,
IsBuilt::Workspace,
false,
);
let debug_target = a.c.debug_target().expect("Got unexpected None");
assert_eq!(debug_target, Path::new("/usr/lib/debug/baz/bar.debug"));
}
#[test]
fn test_debug_target_not_built() {
let a = Asset::new(
AssetSource::Path(PathBuf::from("target/release/bar")),
PathBuf::from("baz/"),
0o644,
IsBuilt::No,
false,
);
assert_eq!(a.c.debug_target(), None);
}
#[test]
fn test_debug_source_path() {
let a = AssetSource::Path(PathBuf::from("target/release/bar"));
let debug_source = a.debug_source().expect("Got unexpected None");
assert_eq!(debug_source, Path::new("target/release/bar.debug"));
}
#[test]
fn test_debug_source_data() {
let data: Vec<u8> = Vec::new();
let a = AssetSource::Data(data);
assert_eq!(a.debug_source(), None);
}
fn to_canon_static_str(s: &str) -> &'static str {
let cwd = std::env::current_dir().unwrap();
let abs_path = cwd.join(s);
let abs_path_string = abs_path.to_string_lossy().into_owned();
Box::leak(abs_path_string.into_boxed_str())
}
#[test]
fn add_systemd_assets_with_no_config_does_nothing() {
let mut mock_listener = crate::listener::MockListener::new();
mock_listener.expect_info().return_const(());
let _g = add_test_fs_paths(&[to_canon_static_str("cargo-deb.service")]);
let config = Config::from_manifest(Path::new("Cargo.toml"), None, None, None, None, None, None, &mock_listener, "release").unwrap();
let num_unit_assets = config.assets.resolved.iter()
.filter(|a| a.c.target_path.starts_with("lib/systemd/system/"))
.count();
assert_eq!(0, num_unit_assets);
}
#[test]
fn add_systemd_assets_with_config_adds_unit_assets() {
let mut mock_listener = crate::listener::MockListener::new();
mock_listener.expect_info().return_const(());
let _g = add_test_fs_paths(&[to_canon_static_str("cargo-deb.service")]);
let mut config = Config::from_manifest(Path::new("Cargo.toml"), None, None, None, None, None, None, &mock_listener, "release").unwrap();
config.systemd_units.get_or_insert(vec![SystemdUnitsConfig::default()]);
config.maintainer_scripts.get_or_insert(PathBuf::new());
config.add_systemd_assets().unwrap();
let num_unit_assets = config.assets.resolved
.iter()
.filter(|a| a.c.target_path.starts_with("lib/systemd/system/"))
.count();
assert_eq!(1, num_unit_assets);
}
#[test]
fn format_conffiles_empty() {
let actual = format_conffiles::<String>(&[]);
assert_eq!("", actual);
}
#[test]
fn format_conffiles_one() {
let actual = format_conffiles(&["/etc/my-pkg/conf.toml"]);
assert_eq!("/etc/my-pkg/conf.toml\n", actual);
}
#[test]
fn format_conffiles_multiple() {
let actual = format_conffiles(&["/etc/my-pkg/conf.toml", "etc/my-pkg/conf2.toml"]);
assert_eq!("/etc/my-pkg/conf.toml\n/etc/my-pkg/conf2.toml\n", actual);
}
}
#[test]
fn deb_ver() {
let mut c = cargo_toml::Package::new("test", "1.2.3-1");
assert_eq!("1.2.3-1", manifest_version_string(&c, None));
assert_eq!("1.2.3-1-2", manifest_version_string(&c, Some("2".into())));
c.version = cargo_toml::Inheritable::Set("1.2.0-beta.3".into());
assert_eq!("1.2.0~beta.3", manifest_version_string(&c, None));
assert_eq!("1.2.0~beta.3-4", manifest_version_string(&c, Some("4".into())));
c.version = cargo_toml::Inheritable::Set("1.2.0-new".into());
assert_eq!("1.2.0-new", manifest_version_string(&c, None));
assert_eq!("1.2.0-new", manifest_version_string(&c, Some("".into())));
assert_eq!("1.2.0-new-11", manifest_version_string(&c, Some("11".into())));
}