Skip to main content

cargo_deb/
assets.rs

1use crate::config::{is_glob_pattern, PackageConfig};
2use crate::error::{CDResult, CargoDebError};
3use crate::listener::Listener;
4use crate::parse::manifest::CargoDebAssetArrayOrTable;
5use crate::util::compress::gzipped;
6use crate::util::read_file_to_bytes;
7use rayon::prelude::*;
8use std::borrow::Cow;
9use std::env::consts::DLL_SUFFIX;
10use std::path::{Path, PathBuf};
11use std::{fmt, fs};
12
13
14#[derive(Debug, Clone)]
15pub enum AssetSource {
16    /// Copy file from the path (and strip binary if needed).
17    Path(PathBuf),
18    /// A symlink existing in the file system
19    Symlink(PathBuf),
20    /// Write data to destination as-is.
21    Data(Vec<u8>),
22}
23
24impl AssetSource {
25    /// Symlink must exist on disk to be preserved
26    #[must_use]
27    pub fn from_path(path: impl Into<PathBuf>, preserve_existing_symlink: bool) -> Self {
28        let path = path.into();
29        if preserve_existing_symlink || !path.exists() { // !exists means a symlink to bogus path
30            if let Ok(md) = fs::symlink_metadata(&path) {
31                if md.is_symlink() {
32                    return Self::Symlink(path);
33                }
34            }
35        }
36        Self::Path(path)
37    }
38
39    #[must_use]
40    pub fn path(&self) -> Option<&Path> {
41        match self {
42            Self::Symlink(p) |
43            Self::Path(p) => Some(p),
44            Self::Data(_) => None,
45        }
46    }
47
48    #[must_use]
49    pub fn into_path(self) -> Option<PathBuf> {
50        match self {
51            Self::Symlink(p) |
52            Self::Path(p) => Some(p),
53            Self::Data(_) => None,
54        }
55    }
56
57    #[must_use]
58    pub fn archive_as_symlink_only(&self) -> bool {
59        matches!(self, Self::Symlink(_))
60    }
61
62    #[must_use]
63    pub fn file_size(&self) -> Option<u64> {
64        match *self {
65            Self::Path(ref p) => fs::metadata(p).ok().map(|m| m.len()),
66            Self::Data(ref d) => Some(d.len() as u64),
67            Self::Symlink(_) => None,
68        }
69    }
70
71    pub fn data(&self) -> CDResult<Cow<'_, [u8]>> {
72        Ok(match self {
73            Self::Path(p) => {
74                let data = read_file_to_bytes(p)
75                    .map_err(|e| CargoDebError::IoFile("Unable to read asset to add to archive", e, p.clone()))?;
76                Cow::Owned(data)
77            },
78            Self::Data(d) => Cow::Borrowed(d),
79            Self::Symlink(p) => {
80                let data = read_file_to_bytes(p)
81                    .map_err(|e| CargoDebError::IoFile("Symlink unexpectedly used to read file data", e, p.clone()))?;
82                Cow::Owned(data)
83            },
84        })
85    }
86
87    pub(crate) fn magic_bytes(&self) -> Option<[u8; 4]> {
88        match self {
89            Self::Path(p) | Self::Symlink(p) => {
90                let mut buf = [0; 4];
91                use std::io::Read;
92                let mut file = std::fs::File::open(p).ok()?;
93                file.read_exact(&mut buf[..]).ok()?;
94                Some(buf)
95            },
96            Self::Data(d) => {
97                d.get(..4).and_then(|b| b.try_into().ok())
98            },
99        }
100    }
101}
102
103#[derive(Debug, Clone)]
104pub(crate) struct Assets {
105    pub unresolved: Vec<UnresolvedAsset>,
106    pub resolved: Vec<Asset>,
107}
108
109#[derive(Debug, Clone, serde::Deserialize)]
110#[serde(try_from = "CargoDebAssetArrayOrTable")]
111pub(crate) enum RawAssetOrAuto {
112    Auto,
113    RawAsset(RawAsset),
114}
115
116impl RawAssetOrAuto {
117    pub fn asset(self) -> Option<RawAsset> {
118        match self {
119            Self::RawAsset(a) => Some(a),
120            Self::Auto => None,
121        }
122    }
123}
124
125impl From<RawAsset> for RawAssetOrAuto {
126    fn from(r: RawAsset) -> Self {
127        Self::RawAsset(r)
128    }
129}
130
131#[derive(Debug, Clone, serde::Deserialize)]
132#[serde(try_from = "RawAssetOrAuto")]
133pub(crate) struct RawAsset {
134    pub source_path: PathBuf,
135    pub target_path: PathBuf,
136    pub chmod: Option<u32>,
137}
138
139impl TryFrom<RawAssetOrAuto> for RawAsset {
140    type Error = &'static str;
141
142    fn try_from(maybe_auto: RawAssetOrAuto) -> Result<Self, Self::Error> {
143        maybe_auto.asset().ok_or("$auto is not allowed here")
144    }
145}
146
147impl Assets {
148    pub(crate) const fn new(unresolved: Vec<UnresolvedAsset>, resolved: Vec<Asset>) -> Self {
149        Self {
150            unresolved,
151            resolved,
152        }
153    }
154
155    pub(crate) fn iter(&self) -> impl Iterator<Item = &AssetCommon> {
156        self.resolved.iter().map(|u| &u.c).chain(self.unresolved.iter().map(|r| &r.c))
157    }
158}
159
160#[derive(Debug, Copy, Clone, Eq, PartialEq)]
161pub enum AssetKind {
162    Any,
163    CargoExampleBinary,
164    SeparateDebugSymbols,
165}
166
167#[derive(Debug, Copy, Clone, Eq, PartialEq)]
168pub enum IsBuilt {
169    No,
170    SamePackage,
171    /// needs --workspace to build
172    Workspace,
173}
174
175fn get_file_mode(path: &Path) -> CDResult<u32> {
176    
177    #[cfg(not(unix))]
178    {
179        Err(CargoDebError::ImplicitFileModeFromPathNotSupported(path.to_path_buf()))
180    }
181    
182    #[cfg(unix)]
183    {
184        let metadata = fs::metadata(path)
185            .map_err(|e| CargoDebError::IoFile(
186                "Unable to read file metadata for permissions", 
187                e, 
188                path.to_owned()
189            ))?;
190        use std::os::unix::fs::PermissionsExt;
191        Ok(metadata.permissions().mode() & 0o7777)
192    }
193}
194
195#[derive(Debug, Clone)]
196pub struct UnresolvedAsset {
197    pub source_path: PathBuf,
198    pub c: AssetCommon,
199}
200
201impl UnresolvedAsset {
202    pub(crate) fn new(source_path: PathBuf, target_path: PathBuf, chmod: Option<u32>, is_built: IsBuilt, asset_kind: AssetKind) -> Self {
203        Self {
204            source_path,
205            c: AssetCommon { target_path, chmod, asset_kind, is_built },
206        }
207    }
208
209    /// Convert `source_path` (with glob or dir) to actual path
210    pub fn resolve(&self, preserve_symlinks: bool) -> CDResult<Vec<Asset>> {
211        let Self { ref source_path, c: AssetCommon { ref target_path, chmod, is_built, asset_kind } } = *self;
212
213        let source_prefix_len = is_glob_pattern(source_path.as_os_str()).then(|| {
214            let file_name_is_glob = source_path
215                .file_name()
216                .is_some_and(is_glob_pattern);
217
218            if file_name_is_glob {
219                // skip to the component before the glob
220                let glob_component_pos = source_path
221                    .parent()
222                    .and_then(|parent| parent.iter().position(is_glob_pattern));
223                glob_component_pos.unwrap_or_else(|| {
224                    source_path
225                        .iter()
226                        .count()
227                })
228            } else {
229                // extract the only file name component
230                source_path
231                    .iter()
232                    .count()
233                    .saturating_sub(1)
234            }
235        });
236
237        let matched_assets = glob::glob(source_path.to_str().ok_or("utf8 path")?)?
238            // Remove dirs from globs without throwing away errors
239            .map(|entry| {
240                let source_file = entry?;
241                Ok(if source_file.is_dir() { None } else { Some(source_file) })
242            })
243            .filter_map(|res: Result<Option<PathBuf>, glob::GlobError>| {
244                Some(res.transpose()?.map_err(CargoDebError::from).and_then(|source_file| {
245                    let target_file = if let Some(source_prefix_len) = source_prefix_len {
246                        target_path.join(
247                            source_file
248                            .iter()
249                            .skip(source_prefix_len)
250                            .collect::<PathBuf>())
251                    } else {
252                        target_path.clone()
253                    };
254                    // Use provided chmod or read from filesystem
255                    let file_chmod = match chmod {
256                        Some(chmod) => chmod,
257                        None => get_file_mode(&source_file)?,
258                    };
259                    log::debug!("asset {} -> {} {} {:o}", source_file.display(), target_file.display(), if is_built != IsBuilt::No {"copy"} else {"build"}, file_chmod);
260                    
261                    let asset = Asset::new(
262                        AssetSource::from_path(source_file, preserve_symlinks),
263                        target_file,
264                        Some(file_chmod),
265                        is_built,
266                        asset_kind,
267                    );
268                    if source_prefix_len.is_some() {
269                        Ok(asset.processed("glob", None))
270                    } else {
271                        Ok(asset)
272                    }
273                }))
274            })
275            .collect::<CDResult<Vec<_>>>()
276            .map_err(|e| e.context(format_args!("Error while glob searching {}", source_path.display())))?;
277
278        if matched_assets.is_empty() {
279            return Err(CargoDebError::AssetFileNotFound(
280                source_path.clone(),
281                Asset::normalized_target_path(target_path.clone(), Some(source_path)),
282                source_prefix_len.is_some(), is_built != IsBuilt::No));
283        }
284        Ok(matched_assets)
285    }
286}
287
288#[derive(Debug, Clone)]
289pub struct AssetCommon {
290    pub target_path: PathBuf,
291    pub chmod: Option<u32>,
292    pub(crate) asset_kind: AssetKind,
293    is_built: IsBuilt,
294}
295
296pub(crate) struct AssetFmt<'a> {
297    c: &'a AssetCommon,
298    cwd: &'a Path,
299    source: Option<&'a Path>,
300    processed_from: Option<&'a ProcessedFrom>,
301}
302
303impl<'a> AssetFmt<'a> {
304    pub fn new(asset: &'a Asset, cwd: &'a Path) -> Self {
305        Self {
306            c: &asset.c,
307            source: asset.source.path(),
308            processed_from: asset.processed_from.as_ref(),
309            cwd,
310        }
311    }
312
313    pub fn unresolved(asset: &'a UnresolvedAsset, cwd: &'a Path) -> Self {
314        Self {
315            c: &asset.c,
316            source: Some(&asset.source_path),
317            processed_from: None,
318            cwd,
319        }
320    }
321}
322
323impl fmt::Display for AssetFmt<'_> {
324    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
325        let mut src = self.source;
326        let action = self.processed_from.map(|proc| {
327            src = proc.original_path.as_deref().or(src);
328            proc.action
329        });
330        if let Some(src) = src {
331            write!(f, "{} ", src.strip_prefix(self.cwd).unwrap_or(src).display())?;
332        }
333        if let Some(action) = action {
334            write!(f, "({action}{}) ", if self.c.is_built() { "; built" } else { "" })?;
335        } else if self.c.is_built() {
336            write!(f, "(built) ")?;
337        }
338        write!(f, "-> {}", self.c.target_path.display())?;
339        Ok(())
340    }
341}
342
343#[derive(Debug, Clone)]
344pub struct Asset {
345    pub source: AssetSource,
346    /// For prettier path display not "/tmp/blah.tmp"
347    pub processed_from: Option<ProcessedFrom>,
348    pub c: AssetCommon,
349}
350
351#[derive(Debug, Clone)]
352pub struct ProcessedFrom {
353    pub original_path: Option<PathBuf>,
354    pub action: &'static str,
355}
356
357impl Asset {
358    #[must_use]
359    pub fn normalized_target_path(mut target_path: PathBuf, source_path: Option<&Path>) -> PathBuf {
360        // is_dir() is only for paths that exist
361        if target_path.to_string_lossy().ends_with('/') {
362            let file_name = source_path.and_then(|p| p.file_name()).expect("source must be a file");
363            target_path = target_path.join(file_name);
364        }
365
366        if target_path.is_absolute() || target_path.has_root() {
367            target_path = target_path.strip_prefix("/").expect("no root dir").to_owned();
368        }
369        target_path
370    }
371
372    #[must_use]
373    pub fn new(source: AssetSource, target_path: PathBuf, chmod: Option<u32>, is_built: IsBuilt, asset_kind: AssetKind) -> Self {
374        let target_path = Self::normalized_target_path(target_path, source.path());
375        Self {
376            source,
377            processed_from: None,
378            c: AssetCommon { target_path, chmod, asset_kind, is_built },
379        }
380    }
381
382    #[must_use]
383    pub fn processed(mut self, action: &'static str, original_path: impl Into<Option<PathBuf>>) -> Self {
384        debug_assert!(self.processed_from.is_none());
385        self.processed_from = Some(ProcessedFrom {
386            original_path: original_path.into(),
387            action,
388        });
389        self
390    }
391
392    pub(crate) fn is_binary_executable(&self) -> bool {
393        self.c.is_executable()
394            && self.c.target_path.extension().is_none_or(|ext| ext != "sh")
395            && (self.c.is_built() || self.smells_like_elf())
396    }
397
398    fn smells_like_elf(&self) -> bool {
399        self.source.magic_bytes().is_some_and(|b| b == [0x7F, b'E', b'L', b'F'])
400    }
401}
402
403impl AssetCommon {
404    pub(crate) const fn is_executable(&self) -> bool {
405        if let Some(chmod) = self.chmod {
406            0 != chmod & 0o111
407        } else {
408            false
409        }
410    }
411
412    pub(crate) fn is_dynamic_library(&self) -> bool {
413        is_dynamic_library_filename(&self.target_path)
414    }
415
416    pub(crate) fn is_built(&self) -> bool {
417        self.is_built != IsBuilt::No
418    }
419
420    /// Returns the target path for the debug symbol file, which will be
421    /// /usr/lib/debug/<path-to-executable>.debug
422    #[must_use]
423    pub(crate) fn default_debug_target_path(&self, lib_dir_base: &Path) -> PathBuf {
424        // Turn an absolute path into one relative to "/"
425        let relative = self.target_path.strip_prefix(Path::new("/"))
426            .unwrap_or(self.target_path.as_path());
427
428        // Prepend the debug location and add .debug
429        let mut path = Path::new("/").join(lib_dir_base);
430        path.push("debug");
431        path.push(debug_filename(relative));
432        path
433    }
434
435    pub(crate) fn is_same_package(&self) -> bool {
436        self.is_built == IsBuilt::SamePackage
437    }
438}
439
440/// Adds `.debug` to the end of a path to a filename
441fn debug_filename(path: &Path) -> PathBuf {
442    let mut debug_filename = path.as_os_str().to_os_string();
443    debug_filename.push(".debug");
444    debug_filename.into()
445}
446
447pub(crate) fn is_dynamic_library_filename(path: &Path) -> bool {
448    path.file_name()
449        .and_then(|f| f.to_str())
450        .is_some_and(|f| f.ends_with(DLL_SUFFIX))
451}
452
453/// Compress man pages and other assets per Debian Policy.
454///
455/// # References
456///
457/// <https://www.debian.org/doc/debian-policy/ch-docs.html>
458/// <https://lintian.debian.org/tags/manpage-not-compressed.html>
459pub fn compressed_assets(package_deb: &PackageConfig, listener: &dyn Listener) -> CDResult<Vec<(usize, Asset)>> {
460    fn needs_compression(path: &str) -> bool {
461        !path.ends_with(".gz") &&
462            (path.starts_with("usr/share/man/") ||
463                (path.starts_with("usr/share/doc/") && (path.ends_with("/NEWS") || path.ends_with("/changelog"))) ||
464                (path.starts_with("usr/share/info/") && path.ends_with(".info")))
465    }
466
467    package_deb.assets.resolved.iter().enumerate()
468        .filter(|(_, asset)| {
469            asset.c.target_path.starts_with("usr") && !asset.c.is_built() && needs_compression(&asset.c.target_path.to_string_lossy())
470        })
471        .par_bridge()
472        .map(|(idx, orig_asset)| {
473            let mut file_name = orig_asset.c.target_path.file_name().map(|f| f.to_string_lossy().into_owned()).unwrap_or_default();
474            file_name.push_str(".gz");
475            let new_path = orig_asset.c.target_path.with_file_name(file_name);
476            listener.progress("Compressing", format!("'{}'", new_path.display()));
477            let gzdata = gzipped(&orig_asset.source.data()?)
478                .map_err(|e| CargoDebError::Io(e).context("error while gzipping asset"))?;
479            CDResult::Ok((idx, Asset::new(
480                crate::assets::AssetSource::Data(gzdata),
481                new_path,
482                orig_asset.c.chmod,
483                IsBuilt::No,
484                AssetKind::Any,
485            ).processed("compressed",
486                orig_asset.source.path().unwrap_or(&orig_asset.c.target_path).to_path_buf()
487            )))
488        }).collect()
489}
490
491pub fn apply_compressed_assets(package_deb: &mut PackageConfig, new_assets: Vec<(usize, Asset)>) {
492    for (idx, asset) in new_assets {
493        package_deb.assets.resolved[idx] = asset;
494    }
495}
496
497#[cfg(test)]
498mod tests {
499    use super::*;
500    use crate::config::{BuildEnvironment, BuildOptions, DebConfigOverrides, DebugSymbolOptions};
501    use crate::parse::manifest::SystemdUnitsConfig;
502    use crate::util::tests::add_test_fs_paths;
503
504    #[test]
505    fn assets() {
506        let a = Asset::new(
507            AssetSource::Path(PathBuf::from("target/release/bar")),
508            PathBuf::from("baz/"),
509            Some(0o644),
510            IsBuilt::SamePackage,
511            AssetKind::Any,
512        );
513        assert_eq!("baz/bar", a.c.target_path.to_str().unwrap());
514        assert!(a.c.is_built != IsBuilt::No);
515
516        let a = Asset::new(
517            AssetSource::Path(PathBuf::from("foo/bar")),
518            PathBuf::from("/baz/quz"),
519            Some(0o644),
520            IsBuilt::No,
521            AssetKind::Any,
522        );
523        assert_eq!("baz/quz", a.c.target_path.to_str().unwrap());
524        assert!(a.c.is_built == IsBuilt::No);
525    }
526
527    #[test]
528    #[cfg(unix)]
529    fn resolve_without_permissions_reads_from_filesystem() {
530        // When chmod is None, resolve() should read the file's permissions from disk
531        let source_path = PathBuf::from("test-resources/testroot/src/main.rs");
532        assert!(source_path.exists(), "test file must exist");
533
534        let asset = UnresolvedAsset {
535            source_path: source_path.clone(),
536            c: AssetCommon {
537                target_path: PathBuf::from("usr/share/test/"),
538                chmod: None, // no permissions specified
539                asset_kind: AssetKind::Any,
540                is_built: IsBuilt::No,
541            },
542        };
543
544        let resolved = asset.resolve(false).unwrap();
545        assert_eq!(resolved.len(), 1);
546
547        let resolved_asset = &resolved[0];
548        // The chmod should have been populated from the filesystem
549        assert!(resolved_asset.c.chmod.is_some(), "chmod should be read from filesystem when not specified in manifest");
550
551        // Verify the permission value matches what the filesystem reports
552        use std::os::unix::fs::PermissionsExt;
553        let expected_mode = fs::metadata(&source_path).unwrap().permissions().mode() & 0o7777;
554        assert_eq!(resolved_asset.c.chmod.unwrap(), expected_mode);
555    }
556
557    #[test]
558    fn resolve_with_explicit_permissions_ignores_filesystem() {
559        // When chmod is Some, resolve() should use the specified value, not the filesystem
560        let source_path = PathBuf::from("test-resources/testroot/src/main.rs");
561        assert!(source_path.exists(), "test file must exist");
562
563        let asset = UnresolvedAsset {
564            source_path: source_path.clone(),
565            c: AssetCommon {
566                target_path: PathBuf::from("usr/share/test/"),
567                chmod: Some(0o755), // explicit permissions
568                asset_kind: AssetKind::Any,
569                is_built: IsBuilt::No,
570            },
571        };
572
573        let resolved = asset.resolve(false).unwrap();
574        assert_eq!(resolved.len(), 1);
575        assert_eq!(resolved[0].c.chmod, Some(0o755), "explicit chmod should be preserved");
576    }
577
578    #[test]
579    fn assets_globs() {
580        for (glob, paths) in [
581            ("test-resources/testroot/src/*", &["bar/main.rs"][..]),
582            ("test-resources/testroot/*/main.rs", &["bar/main.rs"]),
583            ("test-resources/testroot/*/*", &["bar/src/main.rs", "bar/testchild/Cargo.toml"]),
584            ("test-resources/*/src/*", &["bar/testroot/src/main.rs"]),
585            ("test-resources/*/src/main.rs", &["bar/main.rs"]),
586            ("test-resources/*/*/main.rs", &["bar/main.rs"]),
587            ("test-resources/testroot/**/src/*", &["bar/src/main.rs", "bar/testchild/src/main.rs"]),
588            ("test-resources/testroot/**/*.rs", &["bar/src/main.rs", "bar/testchild/src/main.rs"]),
589        ] {
590            let asset = UnresolvedAsset {
591                source_path: PathBuf::from(glob),
592                c: AssetCommon {
593                    target_path: PathBuf::from("bar/"),
594                    chmod: Some(0o644),
595                    asset_kind: AssetKind::Any,
596                    is_built: IsBuilt::SamePackage,
597                },
598            };
599            let assets = asset
600                .resolve(false)
601                .unwrap()
602                .into_iter()
603                .map(|asset| asset.c.target_path.to_string_lossy().to_string())
604                .collect::<Vec<_>>();
605            if assets != paths {
606                panic!("Glob: `{glob}`:\n  Expected: {paths:?}\n       Got: {assets:?}");
607            }
608        }
609    }
610
611    /// Tests that getting the debug filename from a path returns the same path
612    /// with ".debug" appended
613    #[test]
614    fn test_debug_filename() {
615        let path = Path::new("/my/test/file");
616        assert_eq!(debug_filename(path), Path::new("/my/test/file.debug"));
617    }
618
619    /// Tests that getting the debug target for an Asset that `is_built` returns
620    /// the path "/usr/lib/debug/<path-to-target>.debug"
621    #[test]
622    fn test_debug_target_ok() {
623        let a = Asset::new(
624            AssetSource::Path(PathBuf::from("target/release/bar")),
625            PathBuf::from("/usr/bin/baz/"),
626            Some(0o644),
627            IsBuilt::SamePackage,
628            AssetKind::Any,
629        );
630        let debug_target = a.c.default_debug_target_path("usr/lib".as_ref());
631        assert_eq!(debug_target, Path::new("/usr/lib/debug/usr/bin/baz/bar.debug"));
632    }
633
634    /// Tests that getting the debug target for an Asset that `is_built` and that
635    /// has a relative path target returns the path "/usr/lib/debug/<path-to-target>.debug"
636    #[test]
637    fn test_debug_target_ok_relative() {
638        let a = Asset::new(
639            AssetSource::Path(PathBuf::from("target/release/bar")),
640            PathBuf::from("baz/"),
641            Some(0o644),
642            IsBuilt::Workspace,
643            AssetKind::Any,
644        );
645        let debug_target = a.c.default_debug_target_path("usr/lib".as_ref());
646        assert_eq!(debug_target, Path::new("/usr/lib/debug/baz/bar.debug"));
647    }
648
649    fn to_canon_static_str(s: &str) -> &'static str {
650        let cwd = std::env::current_dir().unwrap();
651        let abs_path = cwd.join(s);
652        let abs_path_string = abs_path.to_string_lossy().into_owned();
653        Box::leak(abs_path_string.into_boxed_str())
654    }
655
656    #[test]
657    fn add_systemd_assets_with_no_config_does_nothing() {
658        let mut mock_listener = crate::listener::MockListener::new();
659        mock_listener.expect_progress().return_const(());
660
661        // supply a systemd unit file as if it were available on disk
662        let _g = add_test_fs_paths(&[to_canon_static_str("cargo-deb.service")]);
663
664        let (_config, mut package_debs) = BuildEnvironment::from_manifest(BuildOptions {
665            manifest_path: Some(Path::new("Cargo.toml")),
666            debug: DebugSymbolOptions {
667                #[cfg(feature = "default_enable_dbgsym")]
668                generate_dbgsym_package: Some(false),
669                #[cfg(feature = "default_enable_separate_debug_symbols")]
670                separate_debug_symbols: Some(false),
671                ..Default::default()
672            },
673            ..Default::default()
674        }, &mock_listener).unwrap();
675        let package_deb = package_debs.pop().unwrap();
676
677        let num_unit_assets = package_deb.assets.resolved.iter()
678            .filter(|a| a.c.target_path.starts_with("usr/lib/systemd/system/"))
679            .count();
680
681        assert_eq!(0, num_unit_assets);
682    }
683
684    #[test]
685    fn add_systemd_assets_with_config_adds_unit_assets() {
686        let mut mock_listener = crate::listener::MockListener::new();
687        mock_listener.expect_progress().return_const(());
688
689        // supply a systemd unit file as if it were available on disk
690        let _g = add_test_fs_paths(&[to_canon_static_str("cargo-deb.service")]);
691
692        let (_config, mut package_debs) = BuildEnvironment::from_manifest(BuildOptions {
693            manifest_path: Some(Path::new("Cargo.toml")),
694            debug: DebugSymbolOptions {
695                #[cfg(feature = "default_enable_dbgsym")]
696                generate_dbgsym_package: Some(false),
697                #[cfg(feature = "default_enable_separate_debug_symbols")]
698                separate_debug_symbols: Some(false),
699                ..Default::default()
700            },
701            overrides: DebConfigOverrides {
702                systemd_units: Some(vec![SystemdUnitsConfig::default()]),
703                maintainer_scripts_rel_path: Some(PathBuf::new()),
704                ..Default::default()
705            },
706            ..Default::default()
707        }, &mock_listener).unwrap();
708        let package_deb = package_debs.pop().unwrap();
709
710        let num_unit_assets = package_deb.assets.resolved
711            .iter()
712            .filter(|a| a.c.target_path.starts_with("usr/lib/systemd/system/"))
713            .count();
714
715        assert_eq!(1, num_unit_assets);
716    }
717}