cargo_deb/parse/
manifest.rs

1use crate::assets::{RawAsset, RawAssetOrAuto};
2use crate::config::BuildProfile;
3use crate::error::{CDResult, CargoDebError};
4use crate::listener::Listener;
5use crate::CargoLockingFlags;
6use cargo_toml::{DebugSetting, StripSetting};
7use log::debug;
8use serde::de::DeserializeOwned;
9use serde::Deserialize;
10use std::borrow::Cow;
11use std::collections::{BTreeMap, HashMap};
12use std::fs;
13use std::path::{Path, PathBuf};
14use std::process::Command;
15
16/// Configuration settings for the `systemd_units` functionality.
17///
18/// `unit_scripts`: (optional) relative path to a directory containing correctly
19/// named systemd unit files. See `dh_lib::pkgfile()` and `dh_installsystemd.rs`
20/// for more details on file naming. If not supplied, defaults to the
21/// `maintainer_scripts` directory.
22///
23/// `unit_name`: (optjonal) in cases where the `unit_scripts` directory contains
24/// multiple units, only process those matching this unit name.
25///
26/// For details on the other options please see `dh_installsystemd::Options`.
27#[derive(Clone, Debug, Deserialize, Default)]
28#[serde(rename_all = "kebab-case", deny_unknown_fields)]
29pub(crate) struct SystemdUnitsConfig {
30    pub unit_scripts: Option<PathBuf>,
31    pub unit_name: Option<String>,
32    pub enable: Option<bool>,
33    pub start: Option<bool>,
34    pub restart_after_upgrade: Option<bool>,
35    pub stop_on_upgrade: Option<bool>,
36}
37
38#[derive(PartialEq, Copy, Clone, Debug)]
39pub(crate) enum ManifestDebugFlags {
40    /// Don't bother stripping again
41    FullyStrippedByCargo,
42    /// Explicitly doesn't want debug symbols
43    SymbolsDisabled,
44    SymbolsPackedExternally,
45    SomeSymbolsAdded,
46    FullSymbolsAdded,
47    /// Not explicitly specified either way
48    Default,
49}
50
51pub(crate) fn find_profile<'a>(manifest: &'a cargo_toml::Manifest<CargoPackageMetadata>, selected_profile: &str) -> Option<&'a cargo_toml::Profile> {
52    if selected_profile == "release" {
53        manifest.profile.release.as_ref()
54    } else {
55        manifest.profile.custom.get(selected_profile)
56    }
57}
58
59fn from_toml_value<T: DeserializeOwned>(toml: &str) -> Option<T> {
60    // support parsing `true` as bool, but other values as strings
61    toml::de::ValueDeserializer::parse(toml).and_then(|deserializer| T::deserialize(deserializer)).ok().or_else(|| {
62        toml::de::ValueDeserializer::parse(&format!("\"{toml}\"")).and_then(|deserializer| T::deserialize(deserializer))
63            .inspect_err(|e| log::warn!("error parsing profile override: {toml}\n{e}")).ok()
64    })
65}
66
67pub(crate) fn debug_flags(manifest_profile: Option<&cargo_toml::Profile>, profile_override: &BuildProfile) -> ManifestDebugFlags {
68    let profile_uppercase = profile_override.profile_name().to_ascii_uppercase();
69    let cargo_var = |name| {
70        let name = format!("CARGO_PROFILE_{profile_uppercase}_{name}");
71        std::env::var(&name).ok().inspect(|v| log::debug!("{name} = {v}"))
72    };
73
74    let strip = cargo_var("STRIP").and_then(|var| from_toml_value::<StripSetting>(&var))
75        .or(manifest_profile.and_then(|p| p.strip)).inspect(|v| log::debug!("strip={v:?}"));
76    if strip == Some(StripSetting::Symbols) {
77        return ManifestDebugFlags::FullyStrippedByCargo;
78    }
79
80    let debug = profile_override.override_debug.clone().inspect(|o| log::debug!("override={o}")).or_else(|| cargo_var("DEBUG"))
81        .and_then(|var| from_toml_value::<DebugSetting>(&var))
82        .or(manifest_profile.and_then(|p| p.debug)).inspect(|v| log::debug!("debug={v:?}"));
83    match debug {
84        None => ManifestDebugFlags::Default,
85        Some(DebugSetting::None) => ManifestDebugFlags::SymbolsDisabled,
86        Some(_) if manifest_profile.and_then(|p| p.split_debuginfo.as_deref()).is_some_and(|p| p != "off") => ManifestDebugFlags::SymbolsPackedExternally,
87        Some(DebugSetting::Full) if strip != Some(StripSetting::Debuginfo) => ManifestDebugFlags::FullSymbolsAdded,
88        Some(_) => ManifestDebugFlags::SomeSymbolsAdded,
89    }
90}
91
92/// Debian-compatible version of the semver version
93pub(crate) fn manifest_version_string<'a>(package: &'a cargo_toml::Package<CargoPackageMetadata>, revision: Option<&str>) -> Cow<'a, str> {
94    let mut version = Cow::Borrowed(package.version());
95
96    // Make debian's version ordering (newer versions) more compatible with semver's.
97    // Keep "semver-1" and "semver-xxx" as-is (assuming these are irrelevant, or debian revision already),
98    // but change "semver-beta.1" to "semver~beta.1"
99    if let Some((semver_main, semver_pre)) = version.split_once('-') {
100        let pre_ascii = semver_pre.as_bytes();
101        if pre_ascii.iter().any(|c| !c.is_ascii_digit()) && pre_ascii.iter().any(u8::is_ascii_digit) {
102            version = Cow::Owned(format!("{semver_main}~{semver_pre}"));
103        }
104    }
105
106    let revision = revision.unwrap_or("1");
107    if !revision.is_empty() && revision != "0" {
108        let v = version.to_mut();
109        v.push('-');
110        v.push_str(revision);
111    }
112    version
113}
114
115#[derive(Clone, Debug, Deserialize, Default)]
116pub(crate) struct CargoPackageMetadata {
117    pub deb: Option<CargoDeb>,
118}
119
120#[derive(Clone, Debug, Deserialize)]
121#[serde(untagged)]
122pub(crate) enum LicenseFile {
123    String(String),
124    Vec(Vec<String>),
125}
126
127#[derive(Deserialize, Clone, Debug)]
128#[serde(untagged)]
129pub(crate) enum SystemUnitsSingleOrMultiple {
130    Single(SystemdUnitsConfig),
131    Multi(Vec<SystemdUnitsConfig>),
132}
133
134#[derive(Clone, Debug, Deserialize)]
135#[serde(untagged)]
136pub(crate) enum DependencyList {
137    String(String),
138    Vec(Vec<String>),
139}
140
141impl DependencyList {
142    pub(crate) fn to_depends_string(&self) -> String {
143        match self {
144            Self::String(s) => s.to_owned(),
145            Self::Vec(vals) => vals.join(", "),
146        }
147    }
148}
149
150/// Type-alias for list of assets
151pub(crate) type RawAssetList = Vec<RawAssetOrAuto>;
152
153#[derive(Default)]
154pub(crate) struct MergeMap<'a> {
155    by_path: BTreeMap<&'a PathBuf, (&'a PathBuf, u32)>,
156    has_auto: bool,
157}
158
159#[derive(Deserialize)]
160#[serde(untagged)]
161pub(crate) enum CargoDebAssetArrayOrTable {
162    Table(CargoDebAsset),
163    Array([String; 3]),
164    Auto(String),
165    Invalid(toml::Value),
166}
167
168#[derive(Clone, Debug, Deserialize, Default)]
169pub(crate) struct CargoDebAsset {
170    pub source: String,
171    pub dest: String,
172    pub mode: String,
173}
174
175#[derive(Clone, Debug, Deserialize, Default)]
176#[serde(rename_all = "kebab-case", deny_unknown_fields)]
177pub(crate) struct CargoDeb {
178    pub name: Option<String>,
179    pub maintainer: Option<String>,
180    pub copyright: Option<String>,
181    pub license_file: Option<LicenseFile>,
182    pub changelog: Option<String>,
183    pub depends: Option<DependencyList>,
184    pub pre_depends: Option<DependencyList>,
185    pub recommends: Option<DependencyList>,
186    pub suggests: Option<DependencyList>,
187    pub enhances: Option<DependencyList>,
188    pub conflicts: Option<DependencyList>,
189    pub breaks: Option<DependencyList>,
190    pub replaces: Option<DependencyList>,
191    pub provides: Option<DependencyList>,
192    pub extended_description: Option<String>,
193    pub extended_description_file: Option<String>,
194    pub section: Option<String>,
195    pub priority: Option<String>,
196    pub revision: Option<String>,
197    pub conf_files: Option<Vec<String>>,
198    pub assets: Option<RawAssetList>,
199    pub merge_assets: Option<MergeAssets>,
200    pub triggers_file: Option<String>,
201    pub maintainer_scripts: Option<String>,
202    pub features: Option<Vec<String>>,
203    pub default_features: Option<bool>,
204    pub separate_debug_symbols: Option<bool>,
205    pub dbgsym: Option<bool>,
206    pub compress_debug_symbols: Option<bool>,
207    pub preserve_symlinks: Option<bool>,
208    pub systemd_units: Option<SystemUnitsSingleOrMultiple>,
209    pub variants: Option<HashMap<String, CargoDeb>>,
210
211    /// Cargo build profile, defaults to `release`
212    pub profile: Option<String>,
213}
214
215/// Struct containing merge configuration
216#[derive(Clone, Debug, Deserialize, Default)]
217#[serde(deny_unknown_fields)]
218pub(crate) struct MergeAssets {
219    /// Merge assets by appending this list,
220    pub append: Option<RawAssetList>,
221    /// Merge assets using the src as the key,
222    pub by: Option<MergeByKey>,
223}
224
225/// Enumeration of merge by key strategies
226#[derive(Clone, Debug, Deserialize)]
227pub(crate) enum MergeByKey {
228    #[serde(rename = "src")]
229    Src(RawAssetList),
230    #[serde(rename = "dest")]
231    Dest(RawAssetList),
232}
233
234impl MergeByKey {
235    /// Merges w/ a parent asset list
236    fn merge(self, parent: &RawAssetList) -> RawAssetList {
237        let mut merge_map = MergeMap::default();
238        for asset in parent {
239            match asset {
240                RawAssetOrAuto::Auto => { merge_map.has_auto = true; },
241                RawAssetOrAuto::RawAsset(asset) => self.prep_parent_item(&mut merge_map, asset),
242            }
243        }
244
245        self.merge_with(merge_map)
246    }
247
248    /// Folds the parent asset into a merge-map preparing to prepare for a merge,
249    ///
250    fn prep_parent_item<'a>(&'a self, merge_map: &mut MergeMap<'a>, RawAsset { source_path: src,target_path: dest, chmod: perm }: &'a RawAsset) {
251        match &self {
252            Self::Src(_) => {
253                merge_map.by_path.insert(src, (dest, *perm));
254            },
255            Self::Dest(_) => {
256                merge_map.by_path.insert(dest, (src, *perm));
257            },
258        }
259    }
260
261    /// Merges w/ a parent merge map and returns the resulting asset list,
262    ///
263    fn merge_with<'a>(&'a self, mut merge_map: MergeMap<'a>) -> RawAssetList {
264        let (assets, merge_fn, combine_fn): (_, fn(&mut MergeMap<'a>, &'a RawAsset), fn(_) -> RawAsset) = match self {
265            Self::Src(assets) => (
266                assets,
267                |parent, RawAsset { source_path: src, target_path: dest, chmod: perm }| {
268                    if let Some((replaced_dest, replaced_perm)) = parent.by_path.insert(src, (dest, *perm)) {
269                        debug!("Replacing {:?} w/ {:?}", (replaced_dest, replaced_perm), (dest, perm));
270                    }
271                },
272                |(src, (dest, perm))| RawAsset { source_path: src, target_path: dest, chmod: perm },
273            ),
274            Self::Dest(assets) => (
275                assets,
276                |parent, RawAsset { source_path: src, target_path: dest, chmod: perm }| {
277                    if let Some((replaced_src, replaced_perm)) = parent.by_path.insert(dest, (src, *perm)) {
278                        debug!("Replacing {:?} w/ {:?}", (replaced_src, replaced_perm), (src, perm));
279                    }
280                },
281                |(dest, (src, perm))| RawAsset { source_path: src, target_path: dest, chmod: perm },
282            ),
283        };
284
285        for asset in assets {
286            match asset {
287                RawAssetOrAuto::RawAsset(asset) => {
288                    merge_fn(&mut merge_map, asset);
289                },
290                RawAssetOrAuto::Auto => merge_map.has_auto = true,
291            }
292        }
293
294        merge_map.by_path
295            .into_iter()
296            .map(|(path1, (path2, perm))| (path1.clone(), (path2.clone(), perm)))
297            .map(combine_fn)
298            .map(RawAssetOrAuto::RawAsset)
299            .chain(merge_map.has_auto.then_some(RawAssetOrAuto::Auto))
300            .collect()
301    }
302}
303
304impl CargoDeb {
305    /// Inherit unset fields from parent,
306    ///
307    /// **Note**: For backwards compat, if `merge_assets` is set, this will apply **after** the variant has overridden the assets.
308    ///
309    pub(crate) fn inherit_from(self, parent: Self, listener: &dyn Listener) -> Self {
310        let mut assets = self.assets.or(parent.assets);
311
312        if let Some(merge_assets) = self.merge_assets {
313            let old_assets = assets.get_or_insert_with(|| {
314                listener.warning("variant has merge-assets, but not assets to merge".into());
315                vec![]
316            });
317            if let Some(mut append) = merge_assets.append {
318                old_assets.append(&mut append);
319            }
320
321            if let Some(strategy) = merge_assets.by {
322                assets = Some(strategy.merge(old_assets));
323            }
324        }
325
326        Self {
327            name: self.name.or(parent.name),
328            maintainer: self.maintainer.or(parent.maintainer),
329            copyright: self.copyright.or(parent.copyright),
330            license_file: self.license_file.or(parent.license_file),
331            changelog: self.changelog.or(parent.changelog),
332            depends: self.depends.or(parent.depends),
333            pre_depends: self.pre_depends.or(parent.pre_depends),
334            recommends: self.recommends.or(parent.recommends),
335            suggests: self.suggests.or(parent.suggests),
336            enhances: self.enhances.or(parent.enhances),
337            conflicts: self.conflicts.or(parent.conflicts),
338            breaks: self.breaks.or(parent.breaks),
339            replaces: self.replaces.or(parent.replaces),
340            provides: self.provides.or(parent.provides),
341            extended_description: self.extended_description.or(parent.extended_description),
342            extended_description_file: self.extended_description_file.or(parent.extended_description_file),
343            section: self.section.or(parent.section),
344            priority: self.priority.or(parent.priority),
345            revision: self.revision.or(parent.revision),
346            conf_files: self.conf_files.or(parent.conf_files),
347            assets,
348            merge_assets: None,
349            triggers_file: self.triggers_file.or(parent.triggers_file),
350            maintainer_scripts: self.maintainer_scripts.or(parent.maintainer_scripts),
351            features: self.features.or(parent.features),
352            default_features: self.default_features.or(parent.default_features),
353            dbgsym: self.dbgsym.or(parent.dbgsym),
354            separate_debug_symbols: self.separate_debug_symbols.or(parent.separate_debug_symbols),
355            compress_debug_symbols: self.compress_debug_symbols.or(parent.compress_debug_symbols),
356            preserve_symlinks: self.preserve_symlinks.or(parent.preserve_symlinks),
357            systemd_units: self.systemd_units.or(parent.systemd_units),
358            variants: self.variants.or(parent.variants),
359            profile: self.profile.or(parent.profile),
360        }
361    }
362}
363
364#[derive(Deserialize)]
365struct CargoMetadata {
366    pub packages: Vec<CargoMetadataPackage>,
367    #[serde(default)]
368    pub workspace_members: Vec<String>,
369    #[serde(default)]
370    pub workspace_default_members: Vec<String>,
371    pub target_directory: String,
372    pub build_directory: Option<String>,
373    #[serde(default)]
374    pub workspace_root: String,
375}
376
377#[derive(Deserialize)]
378struct CargoMetadataPackage {
379    pub id: String,
380    pub name: String,
381    pub targets: Vec<CargoMetadataTarget>,
382    pub manifest_path: PathBuf,
383    pub metadata: Option<toml::Value>,
384}
385
386#[derive(Debug, Deserialize)]
387pub(crate) struct CargoMetadataTarget {
388    pub name: String,
389    pub kind: Vec<String>,
390    pub crate_types: Vec<String>,
391    pub src_path: PathBuf,
392}
393
394pub(crate) struct ManifestFound {
395    pub build_targets: Vec<CargoMetadataTarget>,
396    pub manifest_path: PathBuf,
397    pub workspace_root_manifest_path: PathBuf,
398    pub root_manifest: Option<cargo_toml::Manifest<CargoPackageMetadata>>,
399    pub target_dir: PathBuf,
400    pub build_dir: Option<PathBuf>,
401    pub manifest: cargo_toml::Manifest<CargoPackageMetadata>,
402}
403
404fn get_selected_package(metadata: &mut CargoMetadata, selected_package_name: Option<&str>) -> Result<CargoMetadataPackage, CargoDebError> {
405    let available_package_names = || {
406        metadata.packages.iter()
407            .filter(|p| metadata.workspace_members.iter().any(|w| w == &p.id))
408            .map(|p| p.name.as_str())
409            .collect::<Vec<_>>().join(", ")
410    };
411    let target_package_pos = if let Some(name) = selected_package_name {
412        let name_no_ver = name.split('@').next().unwrap_or_default();
413        metadata.packages.iter().position(|p| p.name == name_no_ver)
414            .ok_or_else(|| CargoDebError::PackageNotFoundInWorkspace(name.into(), available_package_names()))
415    } else {
416        pick_default_package_from_workspace(metadata)
417            .ok_or_else(|| CargoDebError::NoRootFoundInWorkspace(available_package_names()))
418    }?;
419    Ok(metadata.packages.swap_remove(target_package_pos))
420}
421
422fn pick_default_package_from_workspace(metadata: &CargoMetadata) -> Option<usize> {
423    // ignore default_members if there are multiple due to ambiguity
424    if let [root_id] = metadata.workspace_default_members.as_slice() {
425        if let Some(pos) = metadata.packages.iter().position(move |p| &p.id == root_id) {
426            return Some(pos);
427        }
428    }
429
430    // if the root manifest is a package, use it
431    let root_manifest_path = Path::new(&metadata.workspace_root).join("Cargo.toml");
432    if let Some(pos) = metadata.packages.iter().position(move |p| p.manifest_path == root_manifest_path) {
433        return Some(pos);
434    }
435
436    // find (active) package with an explicit cargo-deb metadata
437    let default_members = if !metadata.workspace_default_members.is_empty() {
438        &metadata.workspace_default_members[..]
439    } else {
440        &metadata.workspace_members
441    };
442    let mut packages_with_deb_meta = metadata.packages.iter().enumerate().filter_map(|(i, package)| {
443        if !package.metadata.as_ref()?.as_table()?.contains_key("deb") {
444            return None;
445        }
446        default_members.contains(&package.id).then_some(i)
447    });
448    let expected_single_id = packages_with_deb_meta.next()?;
449    packages_with_deb_meta.next().is_none().then_some(expected_single_id)
450}
451
452fn parse_manifest_only(manifest_path: &Path) -> Result<cargo_toml::Manifest<CargoPackageMetadata>, CargoDebError> {
453    let manifest_bytes = fs::read(manifest_path)
454        .map_err(|e| CargoDebError::IoFile("Unable to read manifest", e, manifest_path.to_owned()))?;
455
456    cargo_toml::Manifest::<CargoPackageMetadata>::from_slice_with_metadata(&manifest_bytes)
457            .map_err(|e| CargoDebError::TomlParsing(e, manifest_path.into()))
458}
459
460pub(crate) fn cargo_metadata(initial_manifest_path: Option<&Path>, selected_package_name: Option<&str>, cargo_locking_flags: CargoLockingFlags) -> Result<ManifestFound, CargoDebError> {
461    let mut metadata = run_cargo_metadata(initial_manifest_path, cargo_locking_flags)?;
462    let target_package = get_selected_package(&mut metadata, selected_package_name)?;
463
464    let target_dir = PathBuf::from(metadata.target_directory);
465    let workspace_root = PathBuf::from(metadata.workspace_root);
466    let build_dir = metadata.build_directory.map(PathBuf::from);
467
468    let manifest_path = Path::new(&target_package.manifest_path);
469    let mut manifest = parse_manifest_only(manifest_path)?;
470
471    let workspace_root_manifest_path = workspace_root.join("Cargo.toml");
472    let root_manifest = if manifest.workspace.is_none() && manifest_path != workspace_root_manifest_path {
473        parse_manifest_only(&workspace_root_manifest_path).inspect_err(|e| log::error!("{e}")).ok()
474    } else { None };
475
476    manifest.complete_from_path_and_workspace(manifest_path, root_manifest.as_ref().map(|ws| (ws, workspace_root.as_path())))
477        .map_err(move |e| CargoDebError::TomlParsing(e, manifest_path.to_path_buf()))?;
478
479    Ok(ManifestFound {
480        manifest_path: target_package.manifest_path,
481        workspace_root_manifest_path,
482        build_targets: target_package.targets,
483        root_manifest,
484        build_dir,
485        target_dir,
486        manifest,
487    })
488}
489
490/// Returns the workspace metadata based on the `Cargo.toml` that we want to build,
491/// and directory that paths may be relative to
492fn run_cargo_metadata(manifest_rel_path: Option<&Path>, cargo_locking_flags: CargoLockingFlags) -> CDResult<CargoMetadata> {
493    let mut cmd = Command::new("cargo");
494    cmd.args(["metadata", "--format-version=1", "--no-deps"]);
495    cmd.args(cargo_locking_flags.flags());
496
497    if let Some(path) = manifest_rel_path {
498        cmd.args(["--manifest-path".as_ref(), path.as_os_str()]);
499    }
500
501    let output = cmd.output()
502        .map_err(|e| CargoDebError::CommandFailed(e, "cargo".into()))?;
503    if !output.status.success() {
504        return Err(CargoDebError::CommandError("cargo", "metadata".to_owned(), output.stderr));
505    }
506
507    Ok(serde_json::from_slice(&output.stdout)?)
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513    use crate::listener::NoOpListener;
514    use itertools::Itertools;
515
516    #[test]
517    fn test_merge_assets() {
518        // Test merging assets by dest
519        fn create_test_asset(src: impl Into<PathBuf>, target_path: impl Into<PathBuf>, perm: u32) -> RawAsset {
520            RawAsset {
521                source_path: src.into(), target_path: target_path.into(), chmod: perm
522            }
523        }
524
525        // Test merging assets by dest
526        let original_asset = create_test_asset(
527            "lib/test/empty.txt",
528            "/opt/test/empty.txt",
529            0o777
530        );
531
532        let merge_asset = create_test_asset(
533            "lib/test_variant/empty.txt",
534            "/opt/test/empty.txt",
535            0o655,
536        );
537
538        let parent = CargoDeb { assets: Some(vec![ original_asset.into() ]), .. Default::default() };
539        let variant = CargoDeb { merge_assets: Some(MergeAssets { append: None, by: Some(MergeByKey::Dest(vec![ merge_asset.into() ])) }), .. Default::default() };
540
541        let merged = variant.inherit_from(parent, &NoOpListener);
542        let mut merged = merged.assets.expect("should have assets").into_iter().filter_map(|a| a.asset()).collect_vec();
543        let merged_asset = merged.pop().expect("should have an asset");
544        assert_eq!("lib/test_variant/empty.txt", merged_asset.source_path.as_os_str(), "should have merged the source location");
545        assert_eq!("/opt/test/empty.txt", merged_asset.target_path.as_os_str(), "should preserve dest location");
546        assert_eq!(0o655, merged_asset.chmod, "should have merged the dest location");
547
548        // Test merging assets by src
549        let original_asset = create_test_asset(
550            "lib/test/empty.txt",
551            "/opt/test/empty.txt",
552            0o777
553        );
554
555        let merge_asset = create_test_asset(
556            "lib/test/empty.txt",
557            "/opt/test_variant/empty.txt",
558            0o655,
559        );
560
561        let parent = CargoDeb { assets: Some(vec![ original_asset.into() ]), .. Default::default() };
562        let variant = CargoDeb { merge_assets: Some(MergeAssets { append: None, by: Some(MergeByKey::Src(vec![ merge_asset.into() ])) }), .. Default::default() };
563
564        let merged = variant.inherit_from(parent, &NoOpListener);
565        let mut merged = merged.assets.expect("should have assets").into_iter().filter_map(|a| a.asset()).collect_vec();
566        let merged_asset = merged.pop().expect("should have an asset");
567        assert_eq!("lib/test/empty.txt", merged_asset.source_path.as_os_str(), "should have merged the source location");
568        assert_eq!("/opt/test_variant/empty.txt", merged_asset.target_path.as_os_str(), "should preserve dest location");
569        assert_eq!(0o655, merged_asset.chmod, "should have merged the dest location");
570
571        // Test merging assets by appending
572        let original_asset = create_test_asset(
573            "lib/test/empty.txt",
574            "/opt/test/empty.txt",
575            0o777
576        );
577
578        let merge_asset = create_test_asset(
579            "lib/test/empty.txt",
580            "/opt/test_variant/empty.txt",
581            0o655,
582        );
583        
584        let parent = CargoDeb { assets: Some(vec![ original_asset.into() ]), .. Default::default() };
585        let variant = CargoDeb { merge_assets: Some(MergeAssets { append: Some(vec![merge_asset.into()]), by: None }), .. Default::default() };
586        
587        let merged = variant.inherit_from(parent, &NoOpListener);
588        let mut merged = merged.assets.expect("should have assets").into_iter().filter_map(|a| a.asset()).collect_vec();
589
590        let merged_asset = merged.pop().expect("should have an asset");
591        assert_eq!("lib/test/empty.txt", merged_asset.source_path.as_os_str(), "should have merged the source location");
592        assert_eq!("/opt/test_variant/empty.txt", merged_asset.target_path.as_os_str(), "should preserve dest location");
593        assert_eq!(0o655, merged_asset.chmod, "should have merged the dest location");
594
595        let merged_asset = merged.pop().expect("should have an asset");
596        assert_eq!("lib/test/empty.txt", merged_asset.source_path.as_os_str(), "should have merged the source location");
597        assert_eq!("/opt/test/empty.txt", merged_asset.target_path.as_os_str(), "should preserve dest location");
598        assert_eq!(0o777, merged_asset.chmod, "should have merged the dest location");
599
600        // Test backwards compatibility for variants that have set assets
601        let original_asset = create_test_asset(
602            "lib/test/empty.txt",
603            "/opt/test/empty.txt",
604            0o777,
605        );
606
607        let merge_asset = create_test_asset(
608            "lib/test_variant/empty.txt",
609            "/opt/test/empty.txt",
610            0o655,
611        );
612
613        let additional_asset = create_test_asset(
614            "lib/test/other-empty.txt",
615            "/opt/test/other-empty.txt",
616            0o655,
617        );
618
619        let parent = CargoDeb { assets: Some(vec![ original_asset.into() ]), .. Default::default() };
620        let variant = CargoDeb { merge_assets: Some(MergeAssets { append: None, by: Some(MergeByKey::Dest(vec![ merge_asset.clone().into() ])) }), assets: Some(vec![ merge_asset.into(), additional_asset.into() ]), .. Default::default() };
621
622        let merged = variant.inherit_from(parent, &NoOpListener);
623        let mut merged = merged.assets.expect("should have assets");
624        let merged_asset = merged.remove(0).asset().unwrap();
625        assert_eq!("lib/test_variant/empty.txt", merged_asset.source_path.as_os_str(), "should have merged the source location");
626        assert_eq!("/opt/test/empty.txt", merged_asset.target_path.as_os_str(), "should preserve dest location");
627        assert_eq!(0o655, merged_asset.chmod, "should have merged the dest location");
628
629        let additional_asset = merged.remove(0).asset().unwrap();
630        assert_eq!("lib/test/other-empty.txt", additional_asset.source_path.as_os_str(), "should have merged the source location");
631        assert_eq!("/opt/test/other-empty.txt", additional_asset.target_path.as_os_str(), "should preserve dest location");
632        assert_eq!(0o655, additional_asset.chmod, "should have merged the dest location");
633    }
634}
635
636#[test]
637fn deb_ver() {
638    let mut c = cargo_toml::Package::new("test", "1.2.3-1");
639    assert_eq!("1.2.3-1-1", manifest_version_string(&c, None));
640    assert_eq!("1.2.3-1-2", manifest_version_string(&c, Some("2")));
641    assert_eq!("1.2.3-1", manifest_version_string(&c, Some("")));
642    c.version = cargo_toml::Inheritable::Set("1.2.0-beta.3".into());
643    assert_eq!("1.2.0~beta.3-1", manifest_version_string(&c, None));
644    assert_eq!("1.2.0~beta.3-4", manifest_version_string(&c, Some("4")));
645    assert_eq!("1.2.0~beta.3", manifest_version_string(&c, Some("")));
646    c.version = cargo_toml::Inheritable::Set("1.2.0-new".into());
647    assert_eq!("1.2.0-new-1", manifest_version_string(&c, None));
648    assert_eq!("1.2.0-new-11", manifest_version_string(&c, Some("11")));
649    assert_eq!("1.2.0-new", manifest_version_string(&c, Some("0")));
650}