Skip to main content

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