use crate::config::CargoConfig;
use crate::dependencies::resolve;
use crate::dh_installsystemd;
use crate::error::*;
use crate::listener::Listener;
use crate::ok_or::OkOrThen;
use crate::util::read_file_to_bytes;
use rayon::prelude::*;
use serde::Deserialize;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::convert::From;
use std::env::consts::{DLL_PREFIX, DLL_SUFFIX};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
fn is_glob_pattern(s: &str) -> bool {
s.contains('*') || s.contains('[') || s.contains(']') || s.contains('!')
}
#[derive(Debug, Clone)]
pub enum AssetSource {
Path(PathBuf),
Data(Vec<u8>),
}
impl AssetSource {
#[must_use]
pub fn path(&self) -> Option<&Path> {
match *self {
AssetSource::Path(ref p) => Some(p),
_ => None,
}
}
#[must_use]
pub fn len(&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),
}
}
pub fn data(&self) -> CDResult<Cow<'_, [u8]>> {
Ok(match *self {
AssetSource::Path(ref 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(ref d) => {
Cow::Borrowed(d)
},
})
}
#[must_use]
pub fn debug_source(&self) -> Option<PathBuf> {
match *self {
AssetSource::Path(ref 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, Clone)]
pub struct UnresolvedAsset {
pub source_path: PathBuf,
pub target_path: PathBuf,
pub chmod: u32,
pub is_built: bool,
}
#[derive(Debug, Clone)]
pub struct Asset {
pub source: AssetSource,
pub target_path: PathBuf,
pub chmod: u32,
is_built: bool,
}
impl Asset {
#[must_use]
pub fn new(source: AssetSource, mut target_path: PathBuf, chmod: u32, is_built: 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,
target_path,
chmod,
is_built,
}
}
fn is_executable(&self) -> bool {
0 != (self.chmod & 0o111)
}
fn is_dynamic_library(&self) -> bool {
self.target_path.file_name()
.and_then(|f| f.to_str())
.map_or(false, |f| f.ends_with(DLL_SUFFIX))
}
#[must_use]
pub fn debug_target(&self) -> Option<PathBuf> {
if self.is_built {
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()
}
#[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)]
pub struct Config {
pub 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 build_depends: Option<String>,
pub recommends: 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 strip: bool,
pub separate_debug_symbols: bool,
pub preserve_symlinks: bool,
pub(crate) systemd_units: Option<SystemdUnitsConfig>,
_use_constructor_to_make_this_struct_: (),
}
impl Config {
pub fn from_manifest(manifest_path: &Path, package_name: Option<&str>, output_path: Option<String>, target: Option<&str>, variant: Option<&str>, deb_version: Option<String>, listener: &dyn Listener) -> 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 root_package = if let Some(name) = 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(|p| &p.id == root_id)
})
.ok_or_else(|| CargoDebError::NoRootFoundInWorkspace(available_package_names()))
}?;
let target_dir = Path::new(&metadata.target_directory);
let manifest_path = Path::new(&root_package.manifest_path);
let manifest_dir = manifest_path.parent().unwrap();
let content = fs::read(&manifest_path)
.map_err(|e| CargoDebError::IoFile("unable to read Cargo.toml", e, manifest_path.to_owned()))?;
toml::from_slice::<Cargo>(&content)?.into_config(root_package, manifest_dir, output_path, target_dir, target, variant, deb_version, listener)
}
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_map(|p| p.path())
.filter_map(|bname| match resolve(bname, &self.architecture, listener) {
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 resolve_assets(&mut self) -> CDResult<()> {
for UnresolvedAsset { source_path, target_path, chmod, is_built } in self.assets.unresolved.drain(..) {
let source_prefix: PathBuf = source_path.iter()
.take_while(|part| !is_glob_pattern(part.to_str().unwrap()))
.collect();
let source_is_glob = is_glob_pattern(source_path.to_str().unwrap());
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()
};
self.assets.resolved.push(Asset::new(
AssetSource::Path(source_file),
target_file,
chmod,
is_built,
));
}
}
Ok(())
}
pub(crate) fn add_copyright_asset(&mut self) -> CDResult<()> {
let copyright_file = crate::data::generate_copyright_asset(self)?;
self.assets.resolved.push(Asset::new(
AssetSource::Data(copyright_file),
Path::new("usr/share/doc")
.join(&self.deb_name)
.join("copyright"),
0o644,
false,
));
Ok(())
}
pub fn add_debug_assets(&mut self) {
let mut assets_to_add: Vec<Asset> = Vec::new();
for asset in self.built_binaries().into_iter().filter(|a| a.source.path().is_some()) {
let debug_source = asset.source.debug_source().expect("debug asset");
if debug_source.exists() {
let debug_target = asset.debug_target().expect("debug asset");
assets_to_add.push(Asset::new(
AssetSource::Path(debug_source),
debug_target,
0o644,
false,
));
}
}
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)? {
self.assets.resolved.push(Asset::new(
AssetSource::Data(changelog_file),
Path::new("usr/share/doc")
.join(&self.deb_name)
.join("changelog.Debian.gz"),
0o644,
false,
));
}
}
Ok(())
}
fn add_systemd_assets(&mut self) -> CDResult<()> {
if let Some(ref config) = self.systemd_units {
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_workspace(unit_dir);
let package = &self.name;
let unit_name = if let Some(ref unit_name) = config.unit_name {
Some(unit_name.as_str())
} else {
None
};
let units = dh_installsystemd::find_units(&search_path, package, unit_name);
for (source, target) in &units {
self.assets.resolved.push(Asset::new(
AssetSource::Path(source.clone()),
target.path.clone(),
target.mode,
false,
));
}
}
}
Ok(())
}
fn all_binaries(&self) -> Vec<&AssetSource> {
self.binaries(false).iter().map(|asset| &asset.source).collect()
}
pub(crate) fn built_binaries(&self) -> Vec<&Asset> {
self.binaries(true)
}
fn binaries(&self, built_only: bool) -> Vec<&Asset> {
self.assets
.resolved
.iter()
.filter(|asset| {
(!built_only || asset.is_built)
&& (asset.is_dynamic_library() || asset.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) -> PathBuf {
self.target_dir.join("release").join(rel_path)
}
pub(crate) fn path_in_workspace<P: AsRef<Path>>(&self, rel_path: P) -> PathBuf {
self.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.target_dir)
}
}
#[derive(Clone, Debug, Deserialize)]
struct Cargo {
pub package: cargo_toml::Package<CargoPackageMetadata>,
pub profile: Option<cargo_toml::Profiles>,
}
impl Cargo {
fn into_config(
mut self,
root_package: &CargoMetadataPackage,
manifest_dir: &Path,
deb_output_path: Option<String>,
target_dir: &Path,
target: Option<&str>,
variant: Option<&str>,
deb_version: Option<String>,
listener: &dyn Listener,
) -> CDResult<Config> {
let target_dir = if let Some(target) = target {
target_dir.join(target)
} else {
target_dir.to_owned()
};
let mut deb = if let Some(variant) = variant {
self.package.name = format!("{}-{}", self.package.name, variant);
let mut deb = self.package
.metadata
.take()
.and_then(|m| m.deb)
.unwrap_or_else(CargoDeb::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 {
self.package
.metadata
.take()
.and_then(|m| m.deb)
.unwrap_or_else(CargoDeb::default)
};
let (license_file, license_file_skip_lines) = self.license_file(deb.license_file.as_ref())?;
let readme = self.package.readme.as_ref();
self.check_config(manifest_dir, readme, &deb, listener);
let mut config = Config {
manifest_dir: manifest_dir.to_owned(),
deb_output_path,
target: target.map(|t| t.to_string()),
target_dir,
name: self.package.name.clone(),
deb_name: deb.name.take().unwrap_or_else(|| self.package.name.clone()),
deb_version: deb_version.unwrap_or(self.version_string(deb.revision)),
license: self.package.license.take(),
license_file,
license_file_skip_lines,
copyright: deb.copyright.take().ok_or_then(|| {
if self.package.authors.is_empty() {
return Err("The package must have a copyright or authors property".into());
}
Ok(self.package.authors.join(", "))
})?,
homepage: self.package.homepage.clone(),
documentation: self.package.documentation.clone(),
repository: self.package.repository.take(),
description: self.package.description.take().unwrap_or_else(||format!("[generated from Rust crate {}]", self.package.name)),
extended_description: self.extended_description(
deb.extended_description.take(),
deb.extended_description_file.as_ref().or(readme))?,
maintainer: deb.maintainer.take().ok_or_then(|| {
Ok(self.package.authors.get(0)
.ok_or("The package must have a maintainer or authors property")?.to_owned())
})?,
depends: deb.depends.take().unwrap_or_else(|| "$auto".to_owned()),
build_depends: deb.build_depends.take(),
recommends: deb.recommends.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: get_arch(target.unwrap_or(crate::DEFAULT_TARGET)).to_owned(),
conf_files: deb.conf_files.map(|x| x.iter().fold(String::new(), |a, b| a + b + "\n")),
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),
strip: self.profile.as_ref().and_then(|p|p.release.as_ref())
.and_then(|r| r.debug.as_ref())
.map_or(true, |debug| match *debug {
toml::Value::Integer(0) => false,
toml::Value::Boolean(value) => value,
_ => true
}),
preserve_symlinks: deb.preserve_symlinks.unwrap_or(false),
systemd_units: deb.systemd_units.take(),
_use_constructor_to_make_this_struct_: (),
};
let assets = self.take_assets(&config, deb.assets.take(), &root_package.targets, readme)?;
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());
}
config.assets = assets;
config.add_copyright_asset()?;
config.add_changelog_asset()?;
config.add_systemd_assets()?;
Ok(config)
}
fn check_config(&self, manifest_dir: &Path, readme: Option<&String>, deb: &CargoDeb, listener: &dyn Listener) {
if self.package.description.is_none() {
listener.warning("description field is missing in Cargo.toml".to_owned());
}
if self.package.license.is_none() && self.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.warning(format!("extended-description field missing. Using {}, but markdown may not render well.",readme));
}
} else {
for p in &["README.md", "README.markdown", "README.txt", "README"] {
if manifest_dir.join(p).exists() {
listener.warning(format!("{} file exists, but is not specified in `readme` Cargo.toml field", p));
break;
}
}
}
}
fn extended_description(&self, desc: Option<String>, desc_file: Option<&String>) -> 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 license_file(&mut self, license_file: Option<&Vec<String>>) -> CDResult<(Option<PathBuf>, usize)> {
if let Some(args) = license_file {
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};
Ok((file.map(|s|s.into()), lines))
} else {
Ok((self.package.license_file.as_ref().map(|s| s.into()), 0))
}
}
fn take_assets(&self, options: &Config, assets: Option<Vec<Vec<String>>>, targets: &[CargoMetadataTarget], readme: Option<&String>) -> CDResult<Assets> {
Ok(if let Some(assets) = assets {
let mut unresolved_assets = vec![];
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")?);
let (is_built, source_path) = if let Ok(rel_path) = source_path.strip_prefix("target/release") {
(true, options.path_in_build(rel_path))
} else {
(false, options.path_in_workspace(&source_path))
};
let target_path = PathBuf::from(asset_parts.next().ok_or("missing target (second array entry) for asset in Cargo.toml")?);
let chmod = u32::from_str_radix(&asset_parts.next().ok_or("missing chmod (third array entry) for asset in Cargo.toml")?, 8)
.map_err(|e| CargoDebError::NumParse("unable to parse chmod argument", e))?;
unresolved_assets.push(UnresolvedAsset {
source_path,
target_path,
chmod,
is_built,
})
}
Assets::with_unresolved_assets(unresolved_assets)
} else {
let mut implied_assets: Vec<_> = 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(options.path_in_build(&t.name)),
Path::new("usr/bin").join(&t.name),
0o755,
true,
))
} else if t.crate_types.iter().any(|ty| ty == "cdylib") && t.kind.iter().any(|k| k == "cdylib") {
let lib_name = format!("{}{}{}", DLL_PREFIX, t.name, DLL_SUFFIX);
Some(Asset::new(
AssetSource::Path(options.path_in_build(&lib_name)),
Path::new("usr/lib").join(lib_name),
0o644,
true,
))
} else {
None
}
})
.collect();
if let Some(readme) = readme {
let target_path = Path::new("usr/share/doc").join(&self.package.name).join(readme);
implied_assets.push(Asset::new(
AssetSource::Path(PathBuf::from(readme)),
target_path,
0o644,
false,
));
}
Assets::with_resolved_assets(implied_assets)
})
}
fn version_string(&self, revision: Option<String>) -> String {
let debianized_version;
let mut version = &self.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(|c| c.is_ascii_digit()) {
debianized_version = format!("{}~{}", semver_main, semver_pre);
version = &debianized_version;
}
}
if let Some(revision) = revision {
format!("{}-{}", version, revision)
} else {
version.to_owned()
}
}
}
#[derive(Clone, Debug, Deserialize)]
struct CargoPackageMetadata {
pub deb: Option<CargoDeb>,
}
#[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<Vec<String>>,
pub changelog: Option<String>,
pub depends: Option<String>,
pub build_depends: Option<String>,
pub recommends: 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<SystemdUnitsConfig>,
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),
build_depends: self.build_depends.or(parent.build_depends),
recommends: self.recommends.or(parent.recommends),
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,
}
#[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>,
}
fn cargo_metadata(manifest_path: &Path) -> CDResult<CargoMetadata> {
let mut cmd = Command::new("cargo");
cmd.arg("metadata");
cmd.arg("--format-version=1");
cmd.arg(format!("--manifest-path={}", manifest_path.display()));
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)
}
pub(crate) fn get_arch(target: &str) -> &str {
let mut parts = target.split('-');
let arch = parts.next().unwrap();
let abi = parts.last().unwrap_or("");
match (arch, abi) {
("aarch64", _) => "arm64",
("mips64", "gnuabin32") => "mipsn32",
("mips64el", "gnuabin32") => "mipsn32el",
("mipsisa32r6", _) => "mipsr6",
("mipsisa32r6el", _) => "mipsr6el",
("mipsisa64r6", "gnuabi64") => "mips64r6",
("mipsisa64r6", "gnuabin32") => "mipsn32r6",
("mipsisa64r6el", "gnuabi64") => "mips64r6el",
("mipsisa64r6el", "gnuabin32") => "mipsn32r6el",
("powerpc", "gnuspe") => "powerpcspe",
("powerpc64", _) => "ppc64",
("powerpc64le", _) => "ppc64el",
("riscv64gc", _) => "riscv64",
("i586", _) | ("i686", _) | ("x86", _) => "i386",
("x86_64", "gnux32") => "x32",
("x86_64", _) => "amd64",
(arm, gnueabi) if arm.starts_with("arm") && gnueabi.ends_with("hf") => "armhf",
(arm, _) if arm.starts_with("arm") => "armel",
(other_arch, _) => other_arch,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::tests::add_test_fs_paths;
#[test]
fn match_arm_arch() {
assert_eq!("armhf", get_arch("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,
true,
);
assert_eq!("baz/bar", a.target_path.to_str().unwrap());
assert!(a.is_built);
let a = Asset::new(
AssetSource::Path(PathBuf::from("foo/bar")),
PathBuf::from("/baz/quz"),
0o644,
false,
);
assert_eq!("baz/quz", a.target_path.to_str().unwrap());
assert!(!a.is_built);
}
#[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,
true,
);
let debug_target = a.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,
true,
);
let debug_target = a.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,
false,
);
assert_eq!(a.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(());
add_test_fs_paths(&vec![to_canon_static_str("cargo-deb.service")]);
let config = Config::from_manifest(
Path::new("Cargo.toml"),
None,
None,
None,
None,
None,
&mut mock_listener,
).unwrap();
let num_unit_assets = config.assets.resolved
.iter()
.filter(|v| v.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(());
add_test_fs_paths(&vec![to_canon_static_str("cargo-deb.service")]);
let mut config = Config::from_manifest(
Path::new("Cargo.toml"),
None,
None,
None,
None,
None,
&mut mock_listener,
).unwrap();
config.systemd_units.get_or_insert(SystemdUnitsConfig::default());
config.maintainer_scripts.get_or_insert(PathBuf::new());
config.add_systemd_assets().unwrap();
let num_unit_assets = config.assets.resolved
.iter()
.filter(|v| v.target_path.starts_with("lib/systemd/system/"))
.count();
assert_eq!(1, num_unit_assets);
}
}
#[test]
fn deb_ver() {
let mut c = Cargo {
package: cargo_toml::Package {
version: "1.2.3-1".into(),
authors: vec![],
autobenches: false,
autobins: false,
autotests: false,
autoexamples: false,
categories: vec![],
name: "test".into(),
edition: Default::default(),
homepage: None,
keywords: vec![],
publish: Default::default(),
repository: None,
workspace: None,
license: None,
license_file: None,
links: None,
metadata: None,
readme: None,
documentation: None,
description: Default::default(),
build: None,
},
profile: None,
};
assert_eq!("1.2.3-1", c.version_string(None));
assert_eq!("1.2.3-1-2", c.version_string(Some("2".into())));
c.package.version = "1.2.0-beta.3".into();
assert_eq!("1.2.0~beta.3", c.version_string(None));
assert_eq!("1.2.0~beta.3-4", c.version_string(Some("4".into())));
c.package.version = "1.2.0-new".into();
assert_eq!("1.2.0-new", c.version_string(None));
assert_eq!("1.2.0-new-11", c.version_string(Some("11".into())));
}