cargo_deb/
config.rs

1use crate::assets::{AssetFmt, AssetKind, RawAssetOrAuto, Asset, AssetSource, Assets, IsBuilt, UnresolvedAsset, RawAsset};
2use crate::assets::is_dynamic_library_filename;
3use crate::util::compress::gzipped;
4use crate::dependencies::resolve_with_dpkg;
5use crate::dh::dh_installsystemd;
6use crate::error::{CDResult, CargoDebError};
7use crate::listener::Listener;
8use crate::parse::cargo::CargoConfig;
9use crate::parse::manifest::{cargo_metadata, debug_flags, find_profile, manifest_version_string};
10use crate::parse::manifest::{CargoDeb, CargoDebAssetArrayOrTable, CargoMetadataTarget, CargoPackageMetadata, ManifestFound};
11use crate::parse::manifest::{DependencyList, SystemUnitsSingleOrMultiple, SystemdUnitsConfig, LicenseFile, ManifestDebugFlags};
12use crate::util::wordsplit::WordSplit;
13use crate::{debian_architecture_from_rust_triple, debian_triple_from_rust_triple, CargoLockingFlags, OutputPath, DEFAULT_TARGET};
14use itertools::Itertools;
15use rayon::prelude::*;
16use std::borrow::Cow;
17use std::collections::{BTreeSet, HashMap, HashSet};
18use std::env::consts::{DLL_PREFIX, DLL_SUFFIX, EXE_SUFFIX};
19use std::path::{Component, Path, PathBuf};
20use std::process::Command;
21use std::time::SystemTime;
22use std::{fmt, fs, io};
23
24pub(crate) fn is_glob_pattern(s: impl AsRef<Path>) -> bool {
25    // glob crate requires str anyway ;(
26    s.as_ref().to_str().is_some_and(|s| s.as_bytes().iter().any(|&c| c == b'*' || c == b'[' || c == b']' || c == b'!'))
27}
28
29/// Match the official `dh_installsystemd` defaults and rename the confusing
30/// `dh_installsystemd` option names to be consistently positive rather than
31/// mostly, but not always, negative.
32impl From<&SystemdUnitsConfig> for dh_installsystemd::Options {
33    fn from(config: &SystemdUnitsConfig) -> Self {
34        Self {
35            no_enable: !config.enable.unwrap_or(true),
36            no_start: !config.start.unwrap_or(true),
37            restart_after_upgrade: config.restart_after_upgrade.unwrap_or(true),
38            no_stop_on_upgrade: !config.stop_on_upgrade.unwrap_or(true),
39        }
40    }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
44enum ArchSpec {
45    /// e.g. [armhf]
46    Require(String),
47    /// e.g. [!armhf]
48    NegRequire(String),
49}
50
51fn get_architecture_specification(depend: &str) -> CDResult<(String, Option<ArchSpec>)> {
52    use ArchSpec::{NegRequire, Require};
53    let re = regex::Regex::new(r"(.*)\[(!?)(.*)\]").map_err(|_| CargoDebError::Str("internal"))?;
54    match re.captures(depend) {
55        Some(caps) => {
56            let spec = if &caps[2] == "!" {
57                NegRequire(caps[3].to_string())
58            } else {
59                assert_eq!(&caps[2], "");
60                Require(caps[3].to_string())
61            };
62            Ok((caps[1].trim().to_string(), Some(spec)))
63        },
64        None => Ok((depend.to_string(), None)),
65    }
66}
67
68/// Architecture specification strings
69/// <https://www.debian.org/doc/debian-policy/ch-customized-programs.html#s-arch-spec>
70fn match_architecture(spec: ArchSpec, target_arch: &str) -> CDResult<bool> {
71    let (neg, spec) = match spec {
72        ArchSpec::NegRequire(pkg) => (true, pkg),
73        ArchSpec::Require(pkg) => (false, pkg),
74    };
75    let output = Command::new("dpkg-architecture")
76        .args(["-a", target_arch, "-i", &spec])
77        .output()
78        .map_err(|e| CargoDebError::CommandFailed(e, "dpkg-architecture".into()))?;
79    if neg {
80        Ok(!output.status.success())
81    } else {
82        Ok(output.status.success())
83    }
84}
85
86#[derive(Debug)]
87#[non_exhaustive]
88/// Cargo deb configuration read from the manifest and cargo metadata
89pub struct BuildEnvironment {
90    /// Directory where `Cargo.toml` is located. It's a subdirectory in workspaces.
91    pub package_manifest_dir: PathBuf,
92    /// Run `cargo` commands from this dir, or things may subtly break
93    pub cargo_run_current_dir: PathBuf,
94    /// `CARGO_TARGET_DIR`, without target?/profile
95    pub target_dir_base: PathBuf,
96    /// Either derived from target_dir or `-Zbuild-dir`
97    pub build_dir_base: Option<PathBuf>,
98    /// List of Cargo features to use during build
99    pub features: Vec<String>,
100    pub default_features: bool,
101    pub all_features: bool,
102    /// Should the binary be stripped from debug symbols?
103    pub debug_symbols: DebugSymbols,
104    /// try to be deterministic
105    pub reproducible: bool,
106
107    pub(crate) build_profile: BuildProfile,
108    cargo_build_cmd: String,
109    cargo_build_flags: Vec<String>,
110
111    /// Products available in the package
112    build_targets: Vec<CargoMetadataTarget>,
113    cargo_locking_flags: CargoLockingFlags,
114}
115
116#[derive(Debug)]
117pub enum ExtendedDescription {
118    None,
119    File(PathBuf),
120    String(String),
121    ReadmeFallback(PathBuf),
122}
123
124#[derive(Debug)]
125#[non_exhaustive]
126pub struct PackageConfig {
127    /// The name of the project to build
128    pub cargo_crate_name: String,
129    /// The name to give the Debian package; usually the same as the Cargo project name
130    pub deb_name: String,
131    /// The version to give the Debian package; usually the same as the Cargo version
132    pub deb_version: String,
133    /// The software license of the project (SPDX format).
134    pub license_identifier: Option<String>,
135    /// The location of the license file
136    pub license_file_rel_path: Option<PathBuf>,
137    /// number of lines to skip when reading `license_file`
138    pub license_file_skip_lines: usize,
139    /// Names of copyright owners (credit in `Copyright` metadata)
140    /// Used in Debian's `copyright` file, which is *required* by Debian.
141    pub copyright: Option<String>,
142    pub changelog: Option<String>,
143    /// The homepage URL of the project.
144    pub homepage: Option<String>,
145    /// Documentation URL from `Cargo.toml`. Fallback if `homepage` is missing.
146    pub documentation: Option<String>,
147    /// The URL of the software repository. Fallback if both `homepage` and `documentation` are missing.
148    pub repository: Option<String>,
149    /// A short description of the project.
150    pub description: String,
151    /// An extended description of the project.
152    pub extended_description: ExtendedDescription,
153    /// The maintainer of the Debian package.
154    /// In Debian `control` file `Maintainer` field format.
155    pub maintainer: Option<String>,
156    /// Deps including `$auto`
157    pub wildcard_depends: String,
158    /// The Debian dependencies required to run the project.
159    pub resolved_depends: Option<String>,
160    /// The Debian pre-dependencies.
161    pub pre_depends: Option<String>,
162    /// The Debian recommended dependencies.
163    pub recommends: Option<String>,
164    /// The Debian suggested dependencies.
165    pub suggests: Option<String>,
166    /// The list of packages this package can enhance.
167    pub enhances: Option<String>,
168    /// The Debian software category to which the package belongs.
169    pub section: Option<String>,
170    /// The Debian priority of the project. Typically 'optional'.
171    pub priority: String,
172
173    /// `Conflicts` Debian control field.
174    ///
175    /// See [PackageTransition](https://wiki.debian.org/PackageTransition).
176    pub conflicts: Option<String>,
177    /// `Breaks` Debian control field.
178    ///
179    /// See [PackageTransition](https://wiki.debian.org/PackageTransition).
180    pub breaks: Option<String>,
181    /// `Replaces` Debian control field.
182    ///
183    /// See [PackageTransition](https://wiki.debian.org/PackageTransition).
184    pub replaces: Option<String>,
185    /// `Provides` Debian control field.
186    ///
187    /// See [PackageTransition](https://wiki.debian.org/PackageTransition).
188    pub provides: Option<String>,
189
190    /// The Debian architecture of the target system.
191    pub architecture: String,
192    /// Rust's name for the arch. `None` means `DEFAULT_TARGET`
193    pub(crate) rust_target_triple: Option<String>,
194    /// Support Debian's multiarch, which puts libs in `/usr/lib/$tuple/`
195    pub multiarch: Multiarch,
196    /// A list of configuration files installed by the package.
197    /// Automatically includes all files in `/etc`
198    pub conf_files: Vec<String>,
199    /// All of the files that are to be packaged.
200    pub(crate) assets: Assets,
201
202    /// Added to usr/share/doc as a fallback
203    pub readme_rel_path: Option<PathBuf>,
204    /// The location of the triggers file
205    pub triggers_file_rel_path: Option<PathBuf>,
206    /// The path where possible maintainer scripts live
207    pub maintainer_scripts_rel_path: Option<PathBuf>,
208    /// Should symlinks be preserved in the assets
209    pub preserve_symlinks: bool,
210    /// Details of how to install any systemd units
211    pub(crate) systemd_units: Option<Vec<SystemdUnitsConfig>>,
212    /// unix timestamp for generated files
213    pub default_timestamp: u64,
214    /// Save it under a different path
215    pub is_split_dbgsym_package: bool,
216}
217
218#[derive(Debug, Copy, Clone, Eq, PartialEq)]
219pub enum DebugSymbols {
220    /// No change (also used if Cargo already stripped the symbols
221    Keep,
222    Strip,
223    /// Should the debug symbols be moved to a separate file included in the package? (implies `strip:true`)
224    Separate {
225        /// Should the debug symbols be compressed
226        compress: CompressDebugSymbols,
227        /// Generate dbgsym.ddeb package
228        generate_dbgsym_package: bool,
229    },
230}
231
232#[derive(Debug, Copy, Clone, Eq, PartialEq)]
233pub enum CompressDebugSymbols {
234    No,
235    Zstd,
236    Zlib,
237    Auto,
238}
239
240/// Replace config values via command-line
241#[derive(Debug, Clone, Default)]
242#[non_exhaustive]
243pub struct DebConfigOverrides {
244    pub deb_version: Option<String>,
245    pub deb_revision: Option<String>,
246    pub maintainer: Option<String>,
247    pub section: Option<String>,
248    pub features: Vec<String>,
249    pub no_default_features: bool,
250    pub all_features: bool,
251    pub(crate) systemd_units: Option<Vec<SystemdUnitsConfig>>,
252    pub(crate) maintainer_scripts_rel_path: Option<PathBuf>,
253}
254
255#[derive(Debug, Clone, Default)]
256pub struct BuildProfile {
257    /// "release" by default
258    pub profile_name: Option<String>,
259    /// Cargo setting
260    pub override_debug: Option<String>,
261    pub override_lto: Option<String>,
262}
263
264impl BuildProfile {
265    #[must_use]
266    pub fn profile_name(&self) -> &str {
267        self.profile_name.as_deref().unwrap_or("release")
268    }
269
270    #[must_use]
271    pub fn example_profile_name(&self) -> &str {
272        self.profile_name.as_deref().filter(|&p| p != "dev" && p != "debug").unwrap_or("release")
273    }
274
275    #[must_use]
276    fn profile_dir_name(&self) -> &Path {
277        Path::new(self.profile_name.as_deref().map(|p| match p {
278            "dev" => "debug",
279            p => p,
280        }).unwrap_or("release"))
281    }
282}
283
284#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
285pub enum Multiarch {
286    /// Not supported
287    #[default]
288    None,
289    /// Architecture-dependent, but more than one arch can be installed at the same time
290    Same,
291    /// For architecture-independent tools
292    Foreign,
293}
294
295#[derive(Debug, Clone, Default)]
296pub struct DebugSymbolOptions {
297    pub generate_dbgsym_package: Option<bool>,
298    pub separate_debug_symbols: Option<bool>,
299    pub compress_debug_symbols: Option<CompressDebugSymbols>,
300    pub strip_override: Option<bool>,
301}
302
303#[derive(Debug, Clone, Default)]
304pub struct BuildOptions<'a> {
305    pub manifest_path: Option<&'a Path>,
306    pub selected_package_name: Option<&'a str>,
307    pub rust_target_triples: Vec<&'a str>,
308    pub config_variant: Option<&'a str>,
309    pub overrides: DebConfigOverrides,
310    pub build_profile: BuildProfile,
311    pub debug: DebugSymbolOptions,
312    pub cargo_locking_flags: CargoLockingFlags,
313    pub multiarch: Multiarch,
314    pub cargo_build_cmd: Option<String>,
315    pub cargo_build_flags: Vec<String>,
316}
317
318impl BuildEnvironment {
319    /// Makes a new config from `Cargo.toml` in the `manifest_path`
320    ///
321    /// `None` target means the host machine's architecture.
322    pub fn from_manifest(
323        BuildOptions {
324            manifest_path,
325            selected_package_name,
326            rust_target_triples,
327            config_variant,
328            overrides,
329            mut build_profile,
330            debug,
331            cargo_locking_flags,
332            multiarch,
333            cargo_build_cmd,
334            cargo_build_flags,
335        }: BuildOptions<'_>,
336        listener: &dyn Listener,
337    ) -> CDResult<(Self, Vec<PackageConfig>)> {
338        // **IMPORTANT**: This function must not create or expect to see any asset files on disk!
339        // It's run before destination directory is cleaned up, and before the build start!
340
341        let ManifestFound {
342            build_targets,
343            root_manifest,
344            workspace_root_manifest_path,
345            mut manifest_path,
346            build_dir: build_dir_base,
347            target_dir: target_dir_base,
348            mut manifest,
349        } = cargo_metadata(manifest_path, selected_package_name, cargo_locking_flags)?;
350
351        let mut reproducible = false;
352        let default_timestamp = if let Ok(source_date_epoch) = std::env::var("SOURCE_DATE_EPOCH") {
353            reproducible = true;
354            source_date_epoch.parse().map_err(|e| CargoDebError::NumParse("SOURCE_DATE_EPOCH", e))?
355        } else {
356            let manifest_mdate = fs::metadata(&manifest_path).and_then(|m| m.modified()).unwrap_or_else(|_| SystemTime::now());
357            let mut timestamp = manifest_mdate.duration_since(SystemTime::UNIX_EPOCH).map_err(CargoDebError::SystemTime)?.as_secs();
358            timestamp -= timestamp % (24 * 3600);
359            timestamp
360        };
361
362        // Cargo cross-compiles to a dir
363        for rust_target_triple in &rust_target_triples {
364            if !is_valid_target(rust_target_triple) {
365                listener.warning(format!("specified invalid target: '{rust_target_triple}'"));
366                return Err(CargoDebError::Str("invalid build target triple"));
367            }
368        }
369
370        let cargo_package = manifest.package.as_mut().ok_or("Cargo.toml is a workspace, not a package")?;
371
372        // If we build against a variant use that config and change the package name
373        let mut deb = if let Some(variant) = config_variant {
374            let mut deb = cargo_package.metadata.take()
375                .and_then(|m| m.deb).unwrap_or_default();
376            if deb.name.is_none() {
377                deb.name = Some(debian_package_name(&format!("{}-{variant}", cargo_package.name)));
378            }
379            deb.variants
380                .as_mut()
381                .and_then(|v| v.remove(variant))
382                .ok_or_else(|| CargoDebError::VariantNotFound(variant.to_string()))?
383                .inherit_from(deb, listener)
384        } else {
385            cargo_package.metadata.take().and_then(|m| m.deb).unwrap_or_default()
386        };
387
388        if build_profile.profile_name.is_none() {
389            build_profile.profile_name = deb.profile.take();
390        }
391
392        let selected_profile = build_profile.profile_name();
393        let package_profile = find_profile(&manifest, selected_profile);
394        let root_profile = root_manifest.as_ref().and_then(|m| find_profile(m, selected_profile));
395        if package_profile.is_some() && workspace_root_manifest_path != manifest_path {
396            let rel_path = workspace_root_manifest_path.parent().and_then(|base| manifest_path.strip_prefix(base).ok()).unwrap_or(&manifest_path);
397            let profile_name = build_profile.example_profile_name();
398            if root_profile.is_some() {
399                listener.warning(format!("The [profile.{profile_name}] is in both the package and the root workspace.\n\
400                    Picking root ({}) over the package ({}) for compatibility with Cargo", workspace_root_manifest_path.display(), rel_path.display()));
401            } else if root_manifest.is_some() {
402                listener.warning(format!("The [profile.{profile_name}] should be defined in {}, not in {}\n\
403                    Cargo only uses profiles from the workspace root. See --override-debug and --override-lto options.",
404                    workspace_root_manifest_path.display(), rel_path.display()));
405            }
406        }
407        drop(workspace_root_manifest_path);
408
409        let manifest_debug = debug_flags(root_profile.or(package_profile), &build_profile);
410        drop(root_manifest);
411
412        let debug_symbols = Self::configure_debug_symbols(&mut build_profile, debug, &deb, manifest_debug, listener);
413
414        let mut features = deb.features.take().unwrap_or_default();
415        features.extend(overrides.features.iter().cloned());
416
417        manifest_path.pop();
418        let manifest_dir = manifest_path;
419
420        let config = Self {
421            reproducible,
422            package_manifest_dir: manifest_dir,
423            build_dir_base,
424            target_dir_base,
425            features,
426            all_features: overrides.all_features,
427            default_features: if overrides.no_default_features { false } else { deb.default_features.unwrap_or(true) },
428            debug_symbols,
429            build_profile,
430            build_targets,
431            cargo_build_cmd: cargo_build_cmd.unwrap_or_else(|| "build".into()),
432            cargo_build_flags,
433            cargo_locking_flags,
434            cargo_run_current_dir: std::env::current_dir().unwrap_or_default(),
435        };
436
437        let targets = rust_target_triples.iter().copied().map(Some)
438            .chain(rust_target_triples.is_empty().then_some(None));
439        let packages = targets.map(|rust_target_triple| {
440            let assets = deb.assets.as_deref().unwrap_or(&[RawAssetOrAuto::Auto]);
441            let cargo_package = manifest.package.as_mut().ok_or("Cargo.toml is a workspace, not a package")?;
442            let mut package_deb = PackageConfig::new(&deb, cargo_package, listener, default_timestamp, &overrides, rust_target_triple, multiarch)?;
443
444            config.add_assets(&mut package_deb, assets, listener)?;
445            Ok(package_deb)
446        }).collect::<CDResult<Vec<_>>>()?;
447
448        Ok((config, packages))
449    }
450
451    fn configure_debug_symbols(build_profile: &mut BuildProfile, debug: DebugSymbolOptions, deb: &CargoDeb, manifest_debug: ManifestDebugFlags, listener: &dyn Listener) -> DebugSymbols {
452        let DebugSymbolOptions { generate_dbgsym_package, separate_debug_symbols, compress_debug_symbols, strip_override } = debug;
453        let allows_strip = strip_override != Some(false);
454        let allows_separate_debug_symbols = separate_debug_symbols != Some(false);
455
456        let generate_dbgsym_package = generate_dbgsym_package.inspect(|v| log::debug!("--dbgsym={v}"))
457            .or((!allows_strip).then_some(false)) // --no-strip means not running the strip command, even to separate symbols
458            .or(deb.dbgsym).inspect(|v| log::debug!("deb.dbgsym={v}"))
459            .unwrap_or(allows_separate_debug_symbols && crate::DBGSYM_DEFAULT);
460        log::debug!("dbgsym? {generate_dbgsym_package} default={}", crate::DBGSYM_DEFAULT);
461        let explicit_wants_separate_debug_symbols = separate_debug_symbols.inspect(|v| log::debug!("--separate-debug-symbols={v}"))
462            .or((!allows_strip).then_some(false)) // --no-strip means not running the strip command, even to separate symbols
463            .or(deb.separate_debug_symbols).inspect(|v| log::debug!("deb.separate-debug-symbols={v}"));
464        let wants_separate_debug_symbols = explicit_wants_separate_debug_symbols
465            .unwrap_or(generate_dbgsym_package || (allows_separate_debug_symbols && crate::SEPARATE_DEBUG_SYMBOLS_DEFAULT));
466        let separate_debug_symbols = generate_dbgsym_package || wants_separate_debug_symbols;
467        log::debug!("separate? {separate_debug_symbols} default={}", crate::SEPARATE_DEBUG_SYMBOLS_DEFAULT);
468
469        let compress_debug_symbols = compress_debug_symbols.unwrap_or_else(|| {
470            let v = deb.compress_debug_symbols.inspect(|v| log::debug!("deb.compress-debug-symbols={v}"))
471                .unwrap_or(separate_debug_symbols && allows_strip && crate::COMPRESS_DEBUG_SYMBOLS_DEFAULT);
472            if v { CompressDebugSymbols::Auto } else { CompressDebugSymbols::No }
473        });
474        log::debug!("compress? {compress_debug_symbols:?} default={}", crate::COMPRESS_DEBUG_SYMBOLS_DEFAULT);
475
476        let separate_option_name = if generate_dbgsym_package { "dbgsym" } else { "separate-debug-symbols" };
477        let suggested_debug_symbols_setting = if generate_dbgsym_package { "1" } else { "\"line-tables-only\"" };
478
479        if !allows_strip && separate_debug_symbols {
480            listener.warning(format!("--no-strip has no effect when using {separate_option_name}"));
481        }
482        else if generate_dbgsym_package && !wants_separate_debug_symbols {
483            listener.warning("separate-debug-symbols can't be disabled when generating dbgsym".into());
484        }
485        else if !separate_debug_symbols && compress_debug_symbols != CompressDebugSymbols::No {
486            listener.warning("--separate-debug-symbols or --dbgsym is required to compresss symbols".into());
487        }
488
489        let strip_override_default = strip_override.map(|s| if s { DebugSymbols::Strip } else { DebugSymbols::Keep });
490
491        let keep_debug_symbols_default = if separate_debug_symbols {
492            DebugSymbols::Separate {
493                compress: if compress_debug_symbols != CompressDebugSymbols::Auto { compress_debug_symbols }
494                    else if manifest_debug == ManifestDebugFlags::FullSymbolsAdded { CompressDebugSymbols::Zstd } // assuming it's for lldb, not gimli
495                    else { CompressDebugSymbols::Zlib }, // panics in Rust can decompress zlib, but not zstd
496                generate_dbgsym_package,
497            }
498        } else {
499            strip_override_default.unwrap_or(DebugSymbols::Keep)
500        };
501
502        let debug_symbols = match manifest_debug {
503            ManifestDebugFlags::SomeSymbolsAdded => keep_debug_symbols_default,
504            ManifestDebugFlags::FullSymbolsAdded => {
505                if !separate_debug_symbols {
506                    listener.warning(format!("the debug symbols may be bloated\n\
507                        Use `[profile.{}] debug = {suggested_debug_symbols_setting}` or --separate-debug-symbols or --dbgsym options",
508                        build_profile.example_profile_name()));
509                }
510                keep_debug_symbols_default
511            },
512            ManifestDebugFlags::Default if separate_debug_symbols => {
513                listener.warning(format!("debug info hasn't been explicitly enabled\n\
514                    Add `[profile.{}] debug = {suggested_debug_symbols_setting}` to Cargo.toml", build_profile.example_profile_name()));
515
516                if strip_override != Some(true) && (generate_dbgsym_package || explicit_wants_separate_debug_symbols.unwrap_or(false)) {
517                    if generate_dbgsym_package {
518                        build_profile.override_debug = Some("1".into());
519                    }
520                    log::debug!("adding some debug symbols {:?}", build_profile.override_debug);
521                    keep_debug_symbols_default
522                } else {
523                    DebugSymbols::Strip
524                }
525            },
526            ManifestDebugFlags::FullyStrippedByCargo => {
527                if separate_debug_symbols || compress_debug_symbols != CompressDebugSymbols::No {
528                    listener.warning(format!("{separate_option_name} won't have any effect when Cargo is configured to strip the symbols first.\n\
529                        Remove `strip` from `[profile.{}]`", build_profile.example_profile_name()));
530                }
531                strip_override_default.unwrap_or(DebugSymbols::Keep) // no need to launch strip
532            },
533            ManifestDebugFlags::SymbolsDisabled => {
534                if separate_debug_symbols || generate_dbgsym_package {
535                    listener.warning(format!("{separate_option_name} won't have any effect when debug symbols are disabled\n\
536                        Add `[profile.{}] debug = {suggested_debug_symbols_setting}` to Cargo.toml", build_profile.example_profile_name()));
537                }
538                // Rust still adds debug bloat from the libstd
539                strip_override_default.unwrap_or(DebugSymbols::Strip)
540            },
541            ManifestDebugFlags::Default => {
542                // Rust still adds debug bloat from the libstd
543                strip_override_default.unwrap_or(DebugSymbols::Strip)
544            },
545            ManifestDebugFlags::SymbolsPackedExternally => {
546                listener.warning("Cargo's split-debuginfo option (.dwp/.dwo) is not supported; the symbols may be incomplete".into());
547                keep_debug_symbols_default
548            },
549        };
550        log::debug!("manifest debug setting = {manifest_debug:?}; using {debug_symbols:?}");
551        debug_symbols
552    }
553
554    fn add_assets(&self, package_deb: &mut PackageConfig, assets: &[RawAssetOrAuto], listener: &dyn Listener) -> CDResult<()> {
555        package_deb.assets = self.explicit_assets(package_deb, assets, listener)?;
556
557        // https://wiki.debian.org/Multiarch/Implementation
558        if package_deb.multiarch != Multiarch::None {
559            let mut has_bin = None;
560            let mut has_lib = None;
561            let multiarch_lib_dir_prefix = &package_deb.multiarch_lib_dirs()[0];
562            debug_assert!(!multiarch_lib_dir_prefix.is_absolute());
563            for c in package_deb.assets.iter() {
564                let p = c.target_path.as_path();
565                if has_bin.is_none() && (p.starts_with("bin") || p.starts_with("usr/bin") || p.starts_with("usr/sbin")) {
566                    has_bin = Some(p);
567                } else if has_lib.is_none() && p.starts_with(multiarch_lib_dir_prefix) {
568                    has_lib = Some(p);
569                }
570                if let Some((lib, bin)) = has_lib.zip(has_bin) {
571                    listener.warning(format!("Multiarch packages are not allowed to contain both libs and binaries.\n'{}' and '{}' can't be in the same package.", lib.display(), bin.display()));
572                    break;
573                }
574            }
575        }
576
577        self.add_copyright_asset(package_deb, listener)?;
578        self.add_changelog_asset(package_deb)?;
579        self.add_systemd_assets(package_deb, listener)?;
580
581        self.reset_deb_temp_directory(package_deb)
582            .map_err(|e| CargoDebError::Io(e).context("Error while clearing temp directory"))?;
583        Ok(())
584    }
585
586    pub(crate) fn cargo_build(&self, package_debs: &[PackageConfig], verbose: bool, verbose_cargo: bool, listener: &dyn Listener) -> CDResult<()> {
587        let mut cmd = Command::new("cargo");
588        cmd.current_dir(&self.cargo_run_current_dir);
589        cmd.args(self.cargo_build_cmd.split(' ')
590            .filter(|cmd| if !cmd.starts_with('-') { true } else {
591                log::error!("unexpected flag in build command name: {cmd}");
592                false
593            }));
594
595        self.set_cargo_build_flags_for_packages(package_debs, &mut cmd);
596
597        if verbose_cargo && !self.cargo_build_flags.iter().any(|f| f == "--quiet" || f == "-q") {
598            cmd.arg("--verbose");
599        }
600        if verbose {
601            listener.progress("Running", format!("cargo {}{}",
602                cmd.get_args().map(|arg| {
603                    let arg = arg.to_string_lossy();
604                    if arg.as_bytes().iter().any(|b| b.is_ascii_whitespace()) {
605                        format!("'{}'", arg.escape_default()).into()
606                    } else {
607                        arg
608                    }
609                }).join(" "),
610                cmd.get_envs().map(|(k, v)| {
611                    format!(" {}='{}'", k.to_string_lossy(), v.map(|v| v.to_string_lossy()).as_deref().unwrap_or(""))
612                }).join(" "),
613            ));
614        } else {
615            log::debug!("cargo {:?} {:?}", cmd.get_args(), cmd.get_envs());
616        }
617
618        let status = cmd.status()
619            .map_err(|e| CargoDebError::CommandFailed(e, "cargo".into()))?;
620        if !status.success() {
621            return Err(CargoDebError::BuildFailed);
622        }
623        Ok(())
624    }
625
626    pub fn set_cargo_build_flags_for_packages(&self, package_debs: &[PackageConfig], cmd: &mut Command) {
627        let manifest_path = self.manifest_path();
628        debug_assert!(manifest_path.exists());
629        cmd.arg("--manifest-path").arg(manifest_path);
630
631        let profile_name = self.build_profile.profile_name();
632
633        for (name, val) in [("DEBUG", &self.build_profile.override_debug), ("LTO", &self.build_profile.override_lto)] {
634            if let Some(val) = val {
635                cmd.env(format!("CARGO_PROFILE_{}_{name}", profile_name.to_ascii_uppercase()), val);
636            }
637        }
638
639        if profile_name == "release" {
640            cmd.arg("--release");
641        } else {
642            log::debug!("building profile {profile_name}");
643            cmd.arg(format!("--profile={profile_name}"));
644        }
645        cmd.args(self.cargo_locking_flags.flags());
646
647        for package_deb in package_debs {
648            if let Some(rust_target_triple) = package_deb.rust_target_triple.as_deref() {
649                cmd.args(["--target", (rust_target_triple)]);
650                // Set helpful defaults for cross-compiling
651                if std::env::var_os("PKG_CONFIG_PATH").is_none() {
652                    let pkg_config_path = format!("/usr/lib/{}/pkgconfig", debian_triple_from_rust_triple(rust_target_triple));
653                    if Path::new(&pkg_config_path).exists() {
654                        cmd.env(format!("PKG_CONFIG_PATH_{rust_target_triple}"), pkg_config_path);
655                    }
656                }
657            }
658        }
659
660        if self.all_features {
661            cmd.arg("--all-features");
662        } else if !self.default_features {
663            cmd.arg("--no-default-features");
664        }
665        if !self.features.is_empty() {
666            cmd.arg("--features").arg(self.features.join(","));
667        }
668
669        cmd.args(&self.cargo_build_flags);
670        let flags_already_build_a_workspace = self.cargo_build_flags.iter().any(|f| f == "--workspace" || f == "--all");
671
672        if flags_already_build_a_workspace {
673            return;
674        }
675
676        // Assumes all package_debs are same Rust package, only different architectures
677        let Some(package_deb) = package_debs.first() else {
678            return;
679        };
680
681        for a in package_deb.assets.unresolved.iter().filter(|a| a.c.is_built()) {
682            if is_glob_pattern(&a.source_path) {
683                log::debug!("building entire workspace because of glob {}", a.source_path.display());
684                cmd.arg("--workspace");
685                return;
686            }
687        }
688
689        let mut build_bins = vec![];
690        let mut build_examples = vec![];
691        let mut build_libs = false;
692        let mut same_package = true;
693        let resolved = package_deb.assets.resolved.iter().map(|a| (&a.c, a.source.path()));
694        let unresolved = package_deb.assets.unresolved.iter().map(|a| (&a.c, Some(a.source_path.as_ref())));
695        for (asset_target, source_path) in resolved.chain(unresolved).filter(|(c, _)| c.is_built()) {
696            if !asset_target.is_same_package() {
697                log::debug!("building workspace because {} is from another package", source_path.unwrap_or(&asset_target.target_path).display());
698                same_package = false;
699            }
700            if asset_target.is_dynamic_library() || source_path.is_some_and(is_dynamic_library_filename) {
701                log::debug!("building libs for {}", source_path.unwrap_or(&asset_target.target_path).display());
702                build_libs = true;
703            } else if asset_target.is_executable() {
704                if let Some(source_path) = source_path {
705                    let name = source_path.file_name().unwrap().to_str().expect("utf-8 target name");
706                    let name = name.strip_suffix(EXE_SUFFIX).unwrap_or(name);
707                    if asset_target.asset_kind == AssetKind::CargoExampleBinary {
708                        build_examples.push(name);
709                    } else {
710                        build_bins.push(name);
711                    }
712                }
713            }
714        }
715
716        if !same_package {
717            cmd.arg("--workspace");
718        }
719        cmd.args(build_bins.iter().map(|&name| {
720            log::debug!("building bin for {name}");
721            format!("--bin={name}")
722        }));
723        cmd.args(build_examples.iter().map(|&name| {
724            log::debug!("building example for {name}");
725            format!("--example={name}")
726        }));
727        if build_libs {
728            cmd.arg("--lib");
729        }
730    }
731
732    fn add_copyright_asset(&self, package_deb: &mut PackageConfig, listener: &dyn Listener) -> CDResult<()> {
733        let destination_path = Path::new("usr/share/doc").join(&package_deb.deb_name).join("copyright");
734        if package_deb.assets.iter().any(|a| a.target_path == destination_path) {
735            listener.info(format!("Not generating a default copyright, because asset for {} exists", destination_path.display()));
736            return Ok(());
737        }
738
739        let (source_path, (copyright_file, incomplete)) = self.generate_copyright_asset(package_deb)?;
740        if incomplete {
741            listener.warning("Debian requires copyright information, but the Cargo package doesn't have it.\n\
742                Use --maintainer flag to skip this warning.\n\
743                Otherwise, edit Cargo.toml to add `[package] authors = [\"...\"]`, or \n\
744                `[package.metadata.deb] copyright = \"© copyright owner's name\"`.\n\
745                If the package is proprietary, add `[package] license = \"UNLICENSED\"` or `publish = false`.\n\
746                You can also specify `license-file = \"path\"` to a Debian-formatted `copyright` file.".into());
747        }
748        log::debug!("added copyright via {}", source_path.display());
749        package_deb.assets.resolved.push(Asset::new(
750            AssetSource::Data(copyright_file.into()),
751            destination_path,
752            0o644,
753            IsBuilt::No,
754            AssetKind::Any,
755        ).processed("generated", source_path));
756        Ok(())
757    }
758
759    /// Generates the copyright file from the license file and adds that to the tar archive.
760    fn generate_copyright_asset(&self, package_deb: &PackageConfig) -> CDResult<(PathBuf, (String, bool))> {
761        Ok(if let Some(path) = &package_deb.license_file_rel_path {
762            let source_path = self.path_in_cargo_crate(path);
763            let license_string = fs::read_to_string(&source_path)
764                .map_err(|e| CargoDebError::IoFile("Unable to read license file", e, path.clone()))?;
765
766            let (mut copyright, incomplete) = if has_copyright_metadata(&license_string) {
767                (String::new(), false)
768            } else {
769                package_deb.write_copyright_metadata(true)?
770            };
771
772            // Skip the first `A` number of lines and then iterate each line after that.
773            for line in license_string.lines().skip(package_deb.license_file_skip_lines) {
774                // If the line is a space, add a dot, else write the line.
775                if line == " " {
776                    copyright.push_str(" .\n");
777                } else {
778                    copyright.push_str(line);
779                    copyright.push('\n');
780                }
781            }
782            (source_path, (copyright, incomplete))
783        } else {
784            ("Cargo.toml".into(), package_deb.write_copyright_metadata(false)?)
785        })
786    }
787
788    fn add_changelog_asset(&self, package_deb: &mut PackageConfig) -> CDResult<()> {
789        if package_deb.changelog.is_some() {
790            if let Some((source_path, changelog_file)) = self.generate_changelog_asset(package_deb)? {
791                log::debug!("added changelog via {}", source_path.display());
792                package_deb.assets.resolved.push(Asset::new(
793                    AssetSource::Data(changelog_file),
794                    Path::new("usr/share/doc").join(&package_deb.deb_name).join("changelog.Debian.gz"),
795                    0o644,
796                    IsBuilt::No,
797                    AssetKind::Any,
798                ).processed("generated", source_path));
799            }
800        }
801        Ok(())
802    }
803
804    /// Generates compressed changelog file
805    fn generate_changelog_asset(&self, package_deb: &PackageConfig) -> CDResult<Option<(PathBuf, Vec<u8>)>> {
806        if let Some(ref path) = package_deb.changelog {
807            let source_path = self.path_in_cargo_crate(path);
808            let changelog = fs::read(&source_path)
809                .map_err(|e| CargoDebError::IoFile("Unable to read changelog file", e, source_path.clone()))
810                .and_then(|content| {
811                    // allow pre-compressed
812                    if source_path.extension().is_some_and(|e| e == "gz") {
813                        return Ok(content);
814                    }
815                    // The input is plaintext, but the debian package should contain gzipped one.
816                    gzipped(&content).map_err(|e| CargoDebError::Io(e).context("error gzipping changelog"))
817                })?;
818            Ok(Some((source_path, changelog)))
819        } else {
820            Ok(None)
821        }
822    }
823
824    fn add_systemd_assets(&self, package_deb: &mut PackageConfig, listener: &dyn Listener) -> CDResult<()> {
825        let default_units_dir = package_deb.maintainer_scripts_rel_path.as_ref()
826            .map(|dir| self.path_in_cargo_crate(dir))
827            .inspect(|dir| {
828                if !dir.is_dir() {
829                    listener.warning(format!("maintainer-scripts directory not found: {}", dir.display()));
830                }
831            })
832            .unwrap_or_else(|| self.path_in_cargo_crate("systemd"));
833
834        let Some(ref config_vec) = package_deb.systemd_units else {
835            log::debug!("no systemd units to generate");
836            return Ok(());
837        };
838
839        for config in config_vec {
840            let units_dir_option = config.unit_scripts.as_ref().map(|dir| self.path_in_cargo_crate(dir));
841            let search_path = units_dir_option.as_ref().unwrap_or(&default_units_dir);
842            log::debug!("searching for systemd units in {}", search_path.display());
843            let unit_name = config.unit_name.as_deref();
844
845            let mut units = dh_installsystemd::find_units(search_path, &package_deb.deb_name, unit_name);
846            if package_deb.deb_name != package_deb.cargo_crate_name {
847                let fallback_units = dh_installsystemd::find_units(search_path, &package_deb.cargo_crate_name, unit_name);
848                if !fallback_units.is_empty() && fallback_units != units {
849                    let unit_name_info = unit_name.unwrap_or("<unit_name unspecified>");
850                    if units.is_empty() {
851                        units = fallback_units;
852                        listener.warning(format!("Systemd unit {unit_name_info} found for Cargo package name ({}), but Debian package name was expected ({}). Used Cargo package name as a fallback.", package_deb.cargo_crate_name, package_deb.deb_name));
853                    } else {
854                        listener.warning(format!("Cargo package name and Debian package name are different ({} !=  {}) and both have systemd units. Used Debian package name for the systemd unit {unit_name_info}.", package_deb.cargo_crate_name, package_deb.deb_name));
855                    }
856                }
857            }
858
859            if units.is_empty() {
860                listener.warning(format!("No usable systemd units found for `{}` in `{}`", package_deb.deb_name, search_path.display()));
861            }
862
863            for (source, target) in units {
864                package_deb.assets.resolved.push(Asset::new(
865                    AssetSource::from_path(source, package_deb.preserve_symlinks), // should this even support symlinks at all?
866                    target.path,
867                    target.mode,
868                    IsBuilt::No,
869                    AssetKind::Any,
870                ).processed("systemd", search_path.clone()));
871            }
872        }
873        Ok(())
874    }
875
876    /// Based on target dir, not build dir
877    pub(crate) fn path_in_build_products<P: AsRef<Path>>(&self, rel_path: P, package_deb: &PackageConfig) -> PathBuf {
878        self.path_in_target_dir(rel_path.as_ref(), package_deb.rust_target_triple.as_deref())
879    }
880
881    fn target_dependent_path(base: &PathBuf, rust_target_triple: Option<&str>, capacity: usize) -> PathBuf {
882        let mut path = PathBuf::with_capacity(
883            base.as_os_str().len() +
884            rust_target_triple.map(|t| 1 + t.len()).unwrap_or(0) +
885            capacity
886        );
887        path.clone_from(base);
888        if let Some(target) = rust_target_triple {
889            path.push(target);
890        }
891        path
892    }
893
894    fn path_in_target_dir(&self, rel_path: &Path, rust_target_triple: Option<&str>) -> PathBuf {
895        let profile = self.build_profile.profile_dir_name();
896        let mut path = Self::target_dependent_path(
897            &self.target_dir_base,
898            rust_target_triple,
899            1 + profile.as_os_str().len() +
900            1 + rel_path.as_os_str().len()
901        );
902        path.push(profile);
903        path.push(rel_path);
904        path
905    }
906
907    pub(crate) fn path_in_cargo_crate<P: AsRef<Path>>(&self, rel_path: P) -> PathBuf {
908        self.package_manifest_dir.join(rel_path)
909    }
910
911    fn manifest_path(&self) -> PathBuf {
912        self.package_manifest_dir.join("Cargo.toml")
913    }
914
915    /// Store intermediate files here
916    pub(crate) fn deb_temp_dir(&self, package_deb: &PackageConfig) -> PathBuf {
917        let build_dir = self.build_dir_base.as_ref().unwrap_or(&self.target_dir_base);
918        let mut temp_dir = Self::target_dependent_path(
919            build_dir,
920            package_deb.rust_target_triple.as_deref(),
921            1 + package_deb.cargo_crate_name.len(),
922        );
923        temp_dir.push(&package_deb.cargo_crate_name);
924        temp_dir
925    }
926
927    pub(crate) fn default_deb_output_dir(&self) -> PathBuf {
928        self.target_dir_base.join("debian")
929    }
930
931    pub(crate) fn cargo_config(&self) -> CDResult<Option<CargoConfig>> {
932        CargoConfig::new(&self.cargo_run_current_dir)
933    }
934
935    /// Creates empty (removes files if needed) target/debian/foo directory so that we can start fresh.
936    fn reset_deb_temp_directory(&self, package_deb: &PackageConfig) -> io::Result<()> {
937        let deb_temp_dir = self.deb_temp_dir(package_deb);
938        // Delete previous .deb from target/debian, but only other versions of the same package
939        let deb_dir = self.default_deb_output_dir();
940        log::debug!("clearing build dir {}; dest {}/*.deb", deb_temp_dir.display(), deb_dir.display());
941        let _ = fs::remove_dir(&deb_temp_dir);
942        for base_name in [
943            format!("{}_*_{}.deb", package_deb.deb_name, package_deb.architecture),
944            format!("{}-dbgsym_*_{}.ddeb", package_deb.deb_name, package_deb.architecture),
945        ] {
946            if let Ok(old_files) = glob::glob(deb_dir.join(base_name).to_str().ok_or(io::ErrorKind::InvalidInput)?) {
947                for old_file in old_files.flatten() {
948                    let _ = fs::remove_file(old_file);
949                }
950            }
951        }
952        fs::create_dir_all(deb_temp_dir)
953    }
954
955}
956
957fn is_valid_target(rust_target_triple: &str) -> bool {
958    !rust_target_triple.is_empty() &&
959    !rust_target_triple.starts_with('.') &&
960    !rust_target_triple.as_bytes().iter().any(|&b| b == b'/' || b.is_ascii_whitespace()) &&
961    rust_target_triple.contains('-')
962}
963
964impl PackageConfig {
965    pub(crate) fn new(
966        deb: &CargoDeb, cargo_package: &cargo_toml::Package<CargoPackageMetadata>, listener: &dyn Listener, default_timestamp: u64,
967        overrides: &DebConfigOverrides, rust_target_triple: Option<&str>, multiarch: Multiarch,
968    ) -> Result<Self, CargoDebError> {
969        let architecture = debian_architecture_from_rust_triple(rust_target_triple.unwrap_or(DEFAULT_TARGET));
970        let (license_file_rel_path, license_file_skip_lines) = parse_license_file(cargo_package, deb.license_file.as_ref())?;
971        let mut license_identifier = cargo_package.license();
972
973        if license_identifier.is_none() && license_file_rel_path.is_none() {
974            if cargo_package.publish() == false {
975                license_identifier = Some("UNLICENSED");
976                listener.info("license field defaulted to UNLICENSED".into());
977            } else {
978                listener.warning("license field is missing in Cargo.toml".into());
979            }
980        }
981        let deb_version = overrides.deb_version.as_deref().map(Cow::Borrowed)
982            .unwrap_or_else(|| manifest_version_string(cargo_package, overrides.deb_revision.as_deref().or(deb.revision.as_deref())))
983            .into_owned();
984        if let Err(why) = check_debian_version(&deb_version) {
985            return Err(CargoDebError::InvalidVersion(why, deb_version));
986        }
987        Ok(Self {
988            deb_version,
989            default_timestamp,
990            cargo_crate_name: cargo_package.name.clone(),
991            deb_name: deb.name.clone().unwrap_or_else(|| debian_package_name(&cargo_package.name)),
992            license_identifier: license_identifier.map(From::from),
993            license_file_rel_path,
994            license_file_skip_lines,
995            maintainer: overrides.maintainer.as_deref().or(deb.maintainer.as_deref())
996                .or_else(|| Some(cargo_package.authors().first()?.as_str()))
997                .map(From::from),
998            copyright: deb.copyright.clone().or_else(|| (!cargo_package.authors().is_empty()).then_some(cargo_package.authors().join(", "))),
999            homepage: cargo_package.homepage().map(From::from),
1000            documentation: cargo_package.documentation().map(From::from),
1001            repository: cargo_package.repository().map(From::from),
1002            description: cargo_package.description().map(From::from).unwrap_or_else(|| {
1003                listener.warning("description field is missing in Cargo.toml".to_owned());
1004                format!("[generated from Rust crate {}]", cargo_package.name)
1005            }),
1006            extended_description: if let Some(path) = deb.extended_description_file.as_ref() {
1007                if deb.extended_description.is_some() {
1008                    listener.warning("extended-description and extended-description-file are both set".into());
1009                }
1010                ExtendedDescription::File(path.into())
1011            } else if let Some(desc) = &deb.extended_description {
1012                ExtendedDescription::String(desc.into())
1013            } else if let Some(readme_rel_path) = cargo_package.readme().as_path() {
1014                if readme_rel_path.extension().is_some_and(|ext| ext == "md" || ext == "markdown") {
1015                    listener.info(format!("extended-description field missing. Using {}, but markdown may not render well.", readme_rel_path.display()));
1016                }
1017                ExtendedDescription::ReadmeFallback(readme_rel_path.into())
1018            } else {
1019                ExtendedDescription::None
1020            },
1021            readme_rel_path: cargo_package.readme().as_path().map(|p| p.to_path_buf()),
1022            wildcard_depends: deb.depends.as_ref().map_or_else(|| "$auto".to_owned(), DependencyList::to_depends_string),
1023            resolved_depends: None,
1024            pre_depends: deb.pre_depends.as_ref().map(DependencyList::to_depends_string),
1025            recommends: deb.recommends.as_ref().map(DependencyList::to_depends_string),
1026            suggests: deb.suggests.as_ref().map(DependencyList::to_depends_string),
1027            enhances: deb.enhances.as_ref().map(DependencyList::to_depends_string),
1028            conflicts: deb.conflicts.as_ref().map(DependencyList::to_depends_string),
1029            breaks: deb.breaks.as_ref().map(DependencyList::to_depends_string),
1030            replaces: deb.replaces.as_ref().map(DependencyList::to_depends_string),
1031            provides: deb.provides.as_ref().map(DependencyList::to_depends_string),
1032            section: overrides.section.as_deref().or(deb.section.as_deref()).map(From::from),
1033            priority: deb.priority.as_deref().unwrap_or("optional").into(),
1034            architecture: architecture.to_owned(),
1035            conf_files: deb.conf_files.clone().unwrap_or_default(),
1036            rust_target_triple: rust_target_triple.map(|v| v.to_owned()),
1037            assets: Assets::new(vec![], vec![]),
1038            triggers_file_rel_path: deb.triggers_file.as_deref().map(PathBuf::from),
1039            changelog: deb.changelog.clone(),
1040            maintainer_scripts_rel_path: overrides.maintainer_scripts_rel_path.clone()
1041                .or_else(|| deb.maintainer_scripts.as_deref().map(PathBuf::from)),
1042            preserve_symlinks: deb.preserve_symlinks.unwrap_or(false),
1043            systemd_units: overrides.systemd_units.clone().or_else(|| match &deb.systemd_units {
1044                None => None,
1045                Some(SystemUnitsSingleOrMultiple::Single(s)) => Some(vec![s.clone()]),
1046                Some(SystemUnitsSingleOrMultiple::Multi(v)) => Some(v.clone()),
1047            }),
1048            multiarch,
1049            is_split_dbgsym_package: false,
1050        })
1051    }
1052
1053    /// Use `/usr/lib/arch-linux-gnu` dir for libraries
1054    pub fn set_multiarch(&mut self, enable: Multiarch) {
1055        self.multiarch = enable;
1056    }
1057
1058    pub(crate) fn library_install_dir(&self) -> Cow<'static, Path> {
1059        if self.multiarch == Multiarch::None {
1060            Path::new("usr/lib").into()
1061        } else {
1062            let [p, _] = self.multiarch_lib_dirs();
1063            p.into()
1064        }
1065    }
1066
1067    /// Apparently, Debian uses both! The first one is preferred?
1068    ///
1069    /// The paths are without leading /
1070    pub(crate) fn multiarch_lib_dirs(&self) -> [PathBuf; 2] {
1071        let triple = debian_triple_from_rust_triple(self.rust_target_triple.as_deref().unwrap_or(DEFAULT_TARGET));
1072        let debian_multiarch = PathBuf::from(format!("usr/lib/{triple}"));
1073        let gcc_crossbuild = PathBuf::from(format!("usr/{triple}/lib"));
1074        [debian_multiarch, gcc_crossbuild]
1075    }
1076
1077    pub fn resolve_assets(&mut self, listener: &dyn Listener) -> CDResult<()> {
1078        let cwd = std::env::current_dir().unwrap_or_default();
1079
1080        let unresolved = std::mem::take(&mut self.assets.unresolved);
1081        let matched = unresolved.into_par_iter().map(|asset| {
1082            asset.resolve(self.preserve_symlinks).map_err(|e| e.context(format_args!("Can't resolve asset: {}", AssetFmt::unresolved(&asset, &cwd))))
1083        }).collect_vec_list();
1084        for res in matched.into_iter().flatten() {
1085            self.assets.resolved.extend(res?);
1086        }
1087
1088        let mut target_paths = HashMap::new();
1089        let mut indices_to_remove = Vec::new();
1090        for (idx, asset) in self.assets.resolved.iter().enumerate() {
1091            target_paths.entry(asset.c.target_path.as_path()).and_modify(|&mut old_asset| {
1092                listener.warning(format!("Duplicate assets: [{}] and [{}] have the same target path; first one wins", AssetFmt::new(old_asset, &cwd), AssetFmt::new(asset, &cwd)));
1093                indices_to_remove.push(idx);
1094            }).or_insert(asset);
1095        }
1096        for idx in indices_to_remove.into_iter().rev() {
1097            self.assets.resolved.swap_remove(idx);
1098        }
1099
1100        self.add_conf_files();
1101        Ok(())
1102    }
1103
1104    /// Debian defaults all /etc files to be conf files
1105    /// <https://www.debian.org/doc/manuals/maint-guide/dother.en.html#conffiles>
1106    fn add_conf_files(&mut self) {
1107        let existing_conf_files = self.conf_files.iter()
1108            .map(|c| c.trim_start_matches('/')).collect::<HashSet<_>>();
1109
1110        let mut new_conf = Vec::new();
1111        for a in &self.assets.resolved {
1112            if a.c.target_path.starts_with("etc") {
1113                let Some(path_str) = a.c.target_path.to_str() else { continue };
1114                if existing_conf_files.contains(path_str) {
1115                    continue;
1116                }
1117                log::debug!("automatically adding /{path_str} to conffiles");
1118                new_conf.push(format!("/{path_str}"));
1119            }
1120        }
1121        self.conf_files.append(&mut new_conf);
1122    }
1123
1124    /// run dpkg/ldd to check deps of libs
1125    pub fn resolved_binary_dependencies(&self, listener: &dyn Listener) -> CDResult<String> {
1126        // When cross-compiling, resolve dependencies using libs for the target platform (where multiarch is supported)
1127        let lib_search_paths = self.rust_target_triple.is_some()
1128            // the paths are without leading /
1129            .then(|| self.multiarch_lib_dirs().map(|dir| Path::new("/").join(dir)));
1130        let lib_search_paths: Vec<_> = lib_search_paths.iter().flatten().enumerate()
1131            .filter_map(|(i, dir)| {
1132                if dir.exists() {
1133                    Some(dir.as_path())
1134                } else {
1135                    if i == 0 { // report only the preferred one
1136                        log::debug!("lib dir doesn't exist: {}", dir.display());
1137                    }
1138                    None
1139                }
1140            })
1141            .collect();
1142
1143        let mut deps = BTreeSet::new();
1144        let mut used_auto_deps = false;
1145        for word in self.wildcard_depends.split(',') {
1146            let word = word.trim();
1147            if word == "$auto" {
1148                used_auto_deps = true;
1149                let bin = self.all_binaries();
1150                let resolved = bin.par_iter()
1151                    .filter(|bin| !bin.source.archive_as_symlink_only())
1152                    .filter_map(|&bin| {
1153                        let bname = bin.source.path()?;
1154                        match resolve_with_dpkg(bname, &self.architecture, &lib_search_paths) {
1155                            Ok(bindeps) => {
1156                                log::debug!("$auto depends for '{}': {bindeps:?}", bin.c.target_path.display());
1157                                Some(bindeps)
1158                            },
1159                            Err(err) => {
1160                                listener.warning(format!("{err}\nNo $auto deps for {}", bname.display()));
1161                                None
1162                            },
1163                        }
1164                    })
1165                    .collect_vec_list();
1166                deps.extend(resolved.into_iter().flatten().flatten());
1167            } else {
1168                let (dep, arch_spec) = get_architecture_specification(word)?;
1169                if let Some(spec) = arch_spec {
1170                    let matches = match_architecture(spec, &self.architecture)
1171                        .inspect_err(|e| listener.warning(format!("Can't get arch spec for '{word}'\n{e}")));
1172                    if matches.unwrap_or(true) {
1173                        deps.insert(dep);
1174                    }
1175                } else {
1176                    deps.insert(dep);
1177                }
1178            }
1179        }
1180
1181        let deps_str = itertools::Itertools::join(&mut deps.into_iter(), ", ");
1182        if used_auto_deps {
1183            listener.progress("Depends", if deps_str.is_empty() { "(none)" } else { deps_str.as_str() }.into());
1184        }
1185        Ok(deps_str)
1186    }
1187
1188    /// Executables AND dynamic libraries. May include symlinks.
1189    fn all_binaries(&self) -> Vec<&Asset> {
1190        self.assets.resolved.iter()
1191            .filter(|asset| {
1192                // Assumes files in build dir which have executable flag set are binaries
1193                asset.c.is_dynamic_library() || asset.is_binary_executable()
1194            })
1195            .collect()
1196    }
1197
1198    /// Executables AND dynamic libraries, but only in `target/release`
1199    pub(crate) fn built_binaries_mut(&mut self) -> Vec<&mut Asset> {
1200        self.assets.resolved.iter_mut()
1201            .filter(move |asset| {
1202                // Assumes files in build dir which have executable flag set are binaries
1203                asset.c.is_built() && (asset.c.is_dynamic_library() || asset.c.is_executable())
1204            })
1205            .collect()
1206    }
1207
1208    /// similar files next to each other improve tarball compression
1209    pub fn sort_assets_by_type(&mut self) {
1210        self.assets.resolved.sort_by(|a,b| {
1211            a.c.is_executable().cmp(&b.c.is_executable())
1212            .then(a.c.is_dynamic_library().cmp(&b.c.is_dynamic_library()))
1213            .then(a.processed_from.as_ref().map(|p| p.action).cmp(&b.processed_from.as_ref().map(|p| p.action)))
1214            .then(a.c.target_path.extension().cmp(&b.c.target_path.extension()))
1215            .then(a.c.target_path.cmp(&b.c.target_path))
1216        });
1217    }
1218
1219    fn extended_description(&self, config: &BuildEnvironment) -> CDResult<Option<Cow<'_, str>>> {
1220        let path = match &self.extended_description {
1221            ExtendedDescription::None => return Ok(None),
1222            ExtendedDescription::String(s) => return Ok(Some(s.as_str().into())),
1223            ExtendedDescription::File(p) => Cow::Borrowed(p.as_path()),
1224            ExtendedDescription::ReadmeFallback(p) => Cow::Owned(config.path_in_cargo_crate(p)),
1225        };
1226        let desc = fs::read_to_string(&path)
1227            .map_err(|err| CargoDebError::IoFile("Unable to read extended description from file", err, path.into_owned()))?;
1228        Ok(Some(desc.into()))
1229    }
1230
1231    /// Generates the control file that obtains all the important information about the package.
1232    pub fn generate_control(&self, config: &BuildEnvironment) -> CDResult<String> {
1233        use fmt::Write;
1234
1235        // Create and return the handle to the control file with write access.
1236        let mut control = String::with_capacity(1024);
1237
1238        // Write all of the lines required by the control file.
1239        writeln!(control, "Package: {}", self.deb_name)?;
1240        writeln!(control, "Version: {}", self.deb_version)?;
1241        writeln!(control, "Architecture: {}", self.architecture)?;
1242        let ma = match self.multiarch {
1243            Multiarch::None => "",
1244            Multiarch::Same => "same",
1245            Multiarch::Foreign => "foreign",
1246        };
1247        if !ma.is_empty() {
1248            writeln!(control, "Multi-Arch: {ma}")?;
1249        }
1250        if self.is_split_dbgsym_package {
1251            writeln!(control, "Auto-Built-Package: debug-symbols")?;
1252        }
1253        if let Some(homepage) = self.homepage.as_deref().or(self.documentation.as_deref()).or(self.repository.as_deref()) {
1254            writeln!(control, "Homepage: {homepage}")?;
1255        }
1256        if let Some(ref section) = self.section {
1257            writeln!(control, "Section: {section}")?;
1258        }
1259        writeln!(control, "Priority: {}", self.priority)?;
1260        if let Some(maintainer) = self.maintainer.as_deref() {
1261            writeln!(control, "Maintainer: {maintainer}")?;
1262        }
1263
1264        let installed_size = self.assets.resolved
1265            .iter()
1266            .map(|m| (m.source.file_size().unwrap_or(0) + 2047) / 1024) // assume 1KB of fs overhead per file
1267            .sum::<u64>();
1268
1269        writeln!(control, "Installed-Size: {installed_size}")?;
1270
1271        if let Some(deps) = &self.resolved_depends {
1272            writeln!(control, "Depends: {deps}")?;
1273        }
1274
1275        if let Some(ref pre_depends) = self.pre_depends {
1276            let pre_depends_normalized = pre_depends.trim();
1277
1278            if !pre_depends_normalized.is_empty() {
1279                writeln!(control, "Pre-Depends: {pre_depends_normalized}")?;
1280            }
1281        }
1282
1283        if let Some(ref recommends) = self.recommends {
1284            let recommends_normalized = recommends.trim();
1285
1286            if !recommends_normalized.is_empty() {
1287                writeln!(control, "Recommends: {recommends_normalized}")?;
1288            }
1289        }
1290
1291        if let Some(ref suggests) = self.suggests {
1292            let suggests_normalized = suggests.trim();
1293
1294            if !suggests_normalized.is_empty() {
1295                writeln!(control, "Suggests: {suggests_normalized}")?;
1296            }
1297        }
1298
1299        if let Some(ref enhances) = self.enhances {
1300            let enhances_normalized = enhances.trim();
1301
1302            if !enhances_normalized.is_empty() {
1303                writeln!(control, "Enhances: {enhances_normalized}")?;
1304            }
1305        }
1306
1307        if let Some(ref conflicts) = self.conflicts {
1308            writeln!(control, "Conflicts: {conflicts}")?;
1309        }
1310        if let Some(ref breaks) = self.breaks {
1311            writeln!(control, "Breaks: {breaks}")?;
1312        }
1313        if let Some(ref replaces) = self.replaces {
1314            writeln!(control, "Replaces: {replaces}")?;
1315        }
1316        if let Some(ref provides) = self.provides {
1317            writeln!(control, "Provides: {provides}")?;
1318        }
1319
1320        write!(&mut control, "Description:")?;
1321        for line in self.description.split_by_chars(79) {
1322            writeln!(control, " {line}")?;
1323        }
1324
1325        if let Some(desc) = self.extended_description(config)? {
1326            for line in desc.split_by_chars(79) {
1327                writeln!(control, " {line}")?;
1328            }
1329        }
1330        control.push('\n');
1331
1332        Ok(control)
1333    }
1334
1335    pub(crate) fn write_copyright_metadata(&self, has_full_text: bool) -> Result<(String, bool), fmt::Error> {
1336        let mut copyright = String::new();
1337        let mut incomplete = false;
1338        use std::fmt::Write;
1339
1340        writeln!(copyright, "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/")?;
1341        writeln!(copyright, "Upstream-Name: {}", self.cargo_crate_name)?;
1342        if let Some(source) = self.repository.as_deref().or(self.homepage.as_deref()) {
1343            writeln!(copyright, "Source: {source}")?;
1344        }
1345        if let Some(c) = self.copyright.as_deref() {
1346            writeln!(copyright, "Copyright: {c}")?;
1347        } else if let Some(m) = self.maintainer.as_deref() {
1348            writeln!(copyright, "Comment: Copyright information missing (maintainer: {m})")?;
1349        } else if let Some(l) = self.license_identifier.as_deref().filter(|l| license_doesnt_need_author_info(l)) {
1350            log::debug!("assuming the license {l} doesn't require copyright owner info");
1351        } else {
1352            incomplete = true;
1353        }
1354        if let Some(license) = self.license_identifier.as_deref().or(has_full_text.then_some("")) {
1355            writeln!(copyright, "License: {license}")?;
1356        }
1357        Ok((copyright, incomplete))
1358    }
1359
1360    pub(crate) fn conf_files(&self) -> Option<String> {
1361        if self.conf_files.is_empty() {
1362            return None;
1363        }
1364        Some(format_conffiles(&self.conf_files))
1365    }
1366
1367    /// Save final .deb here
1368    pub(crate) fn deb_output_path(&self, path: &OutputPath<'_>) -> PathBuf {
1369        if path.is_dir {
1370            path.path.join(format!(
1371                "{}_{}_{}.{}",
1372                self.deb_name,
1373                self.deb_version,
1374                self.architecture,
1375                if self.is_split_dbgsym_package { "ddeb" } else { "deb" }
1376            ))
1377        } else if self.is_split_dbgsym_package {
1378            path.path.with_extension("ddeb")
1379        } else {
1380            path.path.to_owned()
1381        }
1382    }
1383
1384    pub(crate) fn split_dbgsym(&mut self) -> Option<Self> {
1385        debug_assert!(self.assets.unresolved.is_empty());
1386        let (debug_assets, regular): (Vec<_>, Vec<_>) = self.assets.resolved.drain(..).partition(|asset| {
1387            asset.c.asset_kind == AssetKind::SeparateDebugSymbols
1388        });
1389        self.assets.resolved = regular;
1390        if debug_assets.is_empty() {
1391            return None;
1392        }
1393
1394        let mut recommends = Some(format!("{} (= {})", self.deb_name, self.deb_version));
1395
1396        // if the debug paths are ambiguous, it has to require exact dep
1397        let using_build_id = debug_assets.iter().all(|asset| asset.c.target_path.components().any(|c| c.as_os_str() == ".build-id"));
1398        let resolved_depends = if !using_build_id { recommends.take() } else { None };
1399
1400        Some(Self {
1401            cargo_crate_name: self.cargo_crate_name.clone(),
1402            deb_name: format!("{}-dbgsym", self.deb_name),
1403            deb_version: self.deb_version.clone(),
1404            license_identifier: self.license_identifier.clone(),
1405            license_file_rel_path: None,
1406            license_file_skip_lines: 0,
1407            copyright: None,
1408            changelog: None,
1409            homepage: self.homepage.clone(),
1410            documentation: self.documentation.clone(),
1411            repository: self.repository.clone(),
1412            description: format!("Debug symbols for {} v{} ({})", self.deb_name, self.deb_version, self.architecture),
1413            extended_description: ExtendedDescription::None,
1414            maintainer: self.maintainer.clone(),
1415            wildcard_depends: String::new(),
1416            resolved_depends,
1417            pre_depends: None,
1418            recommends,
1419            suggests: None,
1420            enhances: None,
1421            section: Some("debug".into()),
1422            priority: "extra".into(),
1423            conflicts: None,
1424            breaks: None,
1425            replaces: None,
1426            provides: None,
1427            architecture: self.architecture.clone(),
1428            rust_target_triple: self.rust_target_triple.clone(),
1429            multiarch: if self.multiarch == Multiarch::Same { Multiarch::Same } else { Multiarch::None },
1430            conf_files: Vec::new(),
1431            assets: Assets::new(Vec::new(), debug_assets),
1432            readme_rel_path: None,
1433            triggers_file_rel_path: None,
1434            maintainer_scripts_rel_path: None,
1435            preserve_symlinks: self.preserve_symlinks,
1436            systemd_units: None,
1437            default_timestamp: self.default_timestamp,
1438            is_split_dbgsym_package: true,
1439        })
1440    }
1441}
1442
1443fn license_doesnt_need_author_info(license_identifier: &str) -> bool {
1444    ["UNLICENSED", "PROPRIETARY", "CC-PDDC", "CC0-1.0"].iter()
1445        .any(|l| l.eq_ignore_ascii_case(license_identifier))
1446}
1447
1448const EXPECTED: &str = "Expected items in `assets` to be either `[source, dest, mode]` array, or `{source, dest, mode}` object, or `\"$auto\"`";
1449
1450impl TryFrom<CargoDebAssetArrayOrTable> for RawAssetOrAuto {
1451    type Error = String;
1452
1453    fn try_from(toml: CargoDebAssetArrayOrTable) -> Result<Self, Self::Error> {
1454        fn parse_chmod(mode: &str) -> Result<u32, String> {
1455            u32::from_str_radix(mode, 8).map_err(|e| format!("Unable to parse mode argument (third array element) as an octal number in an asset: {e}"))
1456        }
1457        let raw_asset = match toml {
1458            CargoDebAssetArrayOrTable::Table(a) => Self::RawAsset(RawAsset {
1459                source_path: a.source.into(),
1460                target_path: a.dest.into(),
1461                chmod: parse_chmod(&a.mode)?,
1462            }),
1463            CargoDebAssetArrayOrTable::Array(a) => {
1464                let mut a = a.into_iter();
1465                Self::RawAsset(RawAsset {
1466                    source_path: PathBuf::from(a.next().ok_or("Missing source path (first array element) in an asset in Cargo.toml")?),
1467                    target_path: PathBuf::from(a.next().ok_or("missing dest path (second array entry) for asset in Cargo.toml. Use something like \"usr/local/bin/\".")?),
1468                    chmod: parse_chmod(&a.next().ok_or("Missing mode (third array element) in an asset")?)?
1469                })
1470            },
1471            CargoDebAssetArrayOrTable::Auto(s) if s == "$auto" => Self::Auto,
1472            CargoDebAssetArrayOrTable::Auto(bad) => {
1473                return Err(format!("{EXPECTED}, but found a string: '{bad}'"));
1474            },
1475            CargoDebAssetArrayOrTable::Invalid(bad) => {
1476                return Err(format!("{EXPECTED}, but found {}: {bad}", bad.type_str()));
1477            },
1478        };
1479        if let Self::RawAsset(a) = &raw_asset {
1480            if let Some(msg) = is_trying_to_customize_target_path(&a.source_path) {
1481                return Err(format!("Please only use `target/release` path prefix for built products, not `{}`.
1482    {msg}
1483    The `target/release` is treated as a special prefix, and will be replaced dynamically by cargo-deb with the actual target directory path used by the build.
1484    ", a.source_path.display()));
1485            }
1486        }
1487        Ok(raw_asset)
1488    }
1489}
1490
1491fn is_trying_to_customize_target_path(p: &Path) -> Option<&'static str> {
1492    let mut p = p.components().skip_while(|p| matches!(p, Component::ParentDir | Component::CurDir));
1493    if p.next() != Some(Component::Normal("target".as_ref())) {
1494        return None;
1495    }
1496    let Some(Component::Normal(subdir)) = p.next() else {
1497        return None;
1498    };
1499    if subdir == "debug" {
1500        return Some("Packaging of development-only binaries is intentionally unsupported in cargo-deb.\n\
1501            To add debug information or additional assertions use `[profile.release]` in Cargo.toml instead.");
1502    }
1503    if subdir.to_str().unwrap_or_default().contains('-')
1504            && p.next() == Some(Component::Normal("release".as_ref())) {
1505        return Some("Hardcoding of cross-compilation paths in the configuration is unnecessary, and counter-productive. cargo-deb understands cross-compilation natively and adjusts the path when you use --target.");
1506    }
1507    None
1508}
1509
1510fn parse_license_file(package: &cargo_toml::Package<CargoPackageMetadata>, license_file: Option<&LicenseFile>) -> CDResult<(Option<PathBuf>, usize)> {
1511    Ok(match license_file {
1512        Some(LicenseFile::Vec(args)) => {
1513            let mut args = args.iter();
1514            let file = args.next().map(PathBuf::from);
1515            let lines = args.next().map(|n| n.parse().map_err(|e| CargoDebError::NumParse("invalid number of lines", e))).transpose()?.unwrap_or(0);
1516            (file, lines)
1517        },
1518        Some(LicenseFile::String(s)) => (Some(s.into()), 0),
1519        None => (package.license_file().map(PathBuf::from), 0),
1520    })
1521}
1522
1523fn has_copyright_metadata(file: &str) -> bool {
1524    file.lines().take(10)
1525        .any(|l| ["Copyright: ", "License: ", "Source: ", "Upstream-Name: ", "Format: "].into_iter().any(|f| l.starts_with(f)))
1526}
1527
1528/// Debian doesn't like `_` in names
1529fn debian_package_name(crate_name: &str) -> String {
1530    // crate names are ASCII only
1531    crate_name.bytes().map(|c| {
1532        if c != b'_' {c.to_ascii_lowercase() as char} else {'-'}
1533    }).collect()
1534}
1535
1536impl BuildEnvironment {
1537    fn explicit_assets(&self, package_deb: &PackageConfig, assets: &[RawAssetOrAuto], listener: &dyn Listener) -> CDResult<Assets> {
1538        let custom_profile_dir = self.build_profile.profile_dir_name();
1539        let custom_profile_target_dir = (custom_profile_dir.as_os_str() != "release")
1540            .then(|| Path::new("target").join(custom_profile_dir));
1541
1542        let mut has_auto = false;
1543
1544        // Treat all explicit assets as unresolved until after the build step
1545        let unresolved_assets = assets.iter().filter_map(|asset_or_auto| {
1546            match asset_or_auto {
1547                RawAssetOrAuto::Auto => {
1548                    has_auto = true;
1549                    None
1550                },
1551                RawAssetOrAuto::RawAsset(asset) => Some(asset),
1552            }
1553        }).map(|&RawAsset { ref source_path, ref target_path, chmod }| {
1554            // target/release is treated as a magic prefix that resolves to any profile
1555            let target_artifact_rel_path = source_path.strip_prefix("target/release").ok()
1556                .or_else(|| source_path.strip_prefix(custom_profile_target_dir.as_deref()?).ok());
1557            let (is_built, source_path, is_example) = if let Some(rel_path) = target_artifact_rel_path {
1558                let is_example = rel_path.starts_with("examples");
1559                (self.find_is_built_file_in_package(rel_path, if is_example { "example" } else { "bin" }), self.path_in_build_products(rel_path, package_deb), is_example)
1560            } else {
1561                if source_path.to_str().is_some_and(|s| s.starts_with(['/','.']) && s.contains("/target/")) {
1562                    listener.warning(format!("Only source paths starting with exactly 'target/release/' are detected as Cargo target dir. '{}' does not match the pattern, and will not be built", source_path.display()));
1563                }
1564                (IsBuilt::No, self.path_in_cargo_crate(source_path), false)
1565            };
1566
1567            let mut target_path = target_path.to_owned();
1568            if package_deb.multiarch != Multiarch::None {
1569                if let Ok(lib_file_name) = target_path.strip_prefix("usr/lib") {
1570                    let lib_dir = package_deb.library_install_dir();
1571                    if !target_path.starts_with(&lib_dir) {
1572                        let new_path = lib_dir.join(lib_file_name);
1573                        log::debug!("multiarch: changed {} to {}", target_path.display(), new_path.display());
1574                        target_path = new_path;
1575                    }
1576                }
1577            }
1578            UnresolvedAsset::new(source_path, target_path, chmod, is_built, if is_example { AssetKind::CargoExampleBinary } else { AssetKind::Any })
1579        }).collect::<Vec<_>>();
1580        let resolved = if has_auto { self.implicit_assets(package_deb)? } else { vec![] };
1581        Ok(Assets::new(unresolved_assets, resolved))
1582    }
1583
1584    fn implicit_assets(&self, package_deb: &PackageConfig) -> CDResult<Vec<Asset>> {
1585        let mut implied_assets: Vec<_> = self.build_targets.iter()
1586            .filter_map(|t| {
1587                if t.crate_types.iter().any(|ty| ty == "bin") && t.kind.iter().any(|k| k == "bin") {
1588                    Some(Asset::new(
1589                        AssetSource::Path(self.path_in_build_products(&t.name, package_deb)),
1590                        Path::new("usr/bin").join(&t.name),
1591                        0o755,
1592                        self.is_built_file_in_package(t),
1593                        AssetKind::Any,
1594                    ).processed("$auto", t.src_path.clone()))
1595                } else if t.crate_types.iter().any(|ty| ty == "cdylib") && t.kind.iter().any(|k| k == "cdylib") {
1596                    let (prefix, suffix) = if package_deb.rust_target_triple.is_none() { (DLL_PREFIX, DLL_SUFFIX) } else { ("lib", ".so") };
1597                    let lib_name = format!("{prefix}{}{suffix}", t.name);
1598                    let lib_dir = package_deb.library_install_dir();
1599                    Some(Asset::new(
1600                        AssetSource::Path(self.path_in_build_products(&lib_name, package_deb)),
1601                        lib_dir.join(lib_name),
1602                        0o644,
1603                        self.is_built_file_in_package(t),
1604                        AssetKind::Any,
1605                    ).processed("$auto", t.src_path.clone()))
1606                } else {
1607                    None
1608                }
1609            })
1610            .collect();
1611        if implied_assets.is_empty() {
1612            return Err(CargoDebError::BinariesNotFound(package_deb.cargo_crate_name.clone()));
1613        }
1614        if let Some(readme_rel_path) = package_deb.readme_rel_path.as_deref() {
1615            let path = self.path_in_cargo_crate(readme_rel_path);
1616            let target_path = Path::new("usr/share/doc")
1617                .join(&package_deb.deb_name)
1618                .join(path.file_name().ok_or("bad README path")?);
1619            implied_assets.push(Asset::new(AssetSource::Path(path), target_path, 0o644, IsBuilt::No, AssetKind::Any)
1620                .processed("$auto", readme_rel_path.to_path_buf()));
1621        }
1622        Ok(implied_assets)
1623    }
1624
1625    fn find_is_built_file_in_package(&self, rel_path: &Path, expected_kind: &str) -> IsBuilt {
1626        let source_name = rel_path.file_name().expect("asset filename").to_str().expect("utf-8 names");
1627        let source_name = source_name.strip_suffix(EXE_SUFFIX).unwrap_or(source_name);
1628
1629        if self.build_targets.iter()
1630            .filter(|t| t.name == source_name && t.kind.iter().any(|k| k == expected_kind))
1631            .any(|t| self.is_built_file_in_package(t) == IsBuilt::SamePackage)
1632        {
1633            IsBuilt::SamePackage
1634        } else {
1635            IsBuilt::Workspace
1636        }
1637    }
1638
1639    fn is_built_file_in_package(&self, build_target: &CargoMetadataTarget) -> IsBuilt {
1640        if build_target.src_path.starts_with(&self.package_manifest_dir) {
1641            IsBuilt::SamePackage
1642        } else {
1643            IsBuilt::Workspace
1644        }
1645    }
1646}
1647
1648/// Format conffiles section, ensuring each path has a leading slash
1649///
1650/// Starting with [dpkg 1.20.1](https://github.com/guillemj/dpkg/blob/68ab722604217d3ab836276acfc0ae1260b28f5f/debian/changelog#L393),
1651/// which is what Ubuntu 21.04 uses, relative conf-files are no longer
1652/// accepted (the deb-conffiles man page states that "they should be listed as
1653/// absolute pathnames"). So we prepend a leading slash to the given strings
1654/// as needed
1655fn format_conffiles<S: AsRef<str>>(files: &[S]) -> String {
1656    files.iter().fold(String::new(), |mut acc, x| {
1657        let pth = x.as_ref();
1658        if !pth.starts_with('/') {
1659            acc.push('/');
1660        }
1661        acc + pth + "\n"
1662    })
1663}
1664
1665fn check_debian_version(mut ver: &str) -> Result<(), &'static str> {
1666    if ver.trim_start().is_empty() {
1667        return Err("empty string");
1668    }
1669
1670    if let Some((epoch, ver_rest)) = ver.split_once(':') {
1671        ver = ver_rest;
1672        if epoch.is_empty() || epoch.as_bytes().iter().any(|c| !c.is_ascii_digit()) {
1673            return Err("version has unexpected ':' char");
1674        }
1675    }
1676
1677    if !ver.starts_with(|c: char| c.is_ascii_digit()) {
1678        return Err("version must start with a digit");
1679    }
1680
1681    if ver.as_bytes().iter().any(|&c| !c.is_ascii_alphanumeric() && !matches!(c, b'.' | b'+' | b'-' | b'~')) {
1682        return Err("contains characters other than a-z 0-9 . + - ~");
1683    }
1684    Ok(())
1685}
1686
1687#[cfg(test)]
1688mod tests {
1689    use super::*;
1690
1691    #[test]
1692    fn match_arm_arch() {
1693        assert_eq!("armhf", debian_architecture_from_rust_triple("arm-unknown-linux-gnueabihf"));
1694    }
1695
1696    #[test]
1697    fn arch_spec() {
1698        use ArchSpec::*;
1699        // req
1700        assert_eq!(
1701            get_architecture_specification("libjpeg64-turbo [armhf]").expect("arch"),
1702            ("libjpeg64-turbo".to_owned(), Some(Require("armhf".to_owned())))
1703        );
1704        // neg
1705        assert_eq!(
1706            get_architecture_specification("libjpeg64-turbo [!amd64]").expect("arch"),
1707            ("libjpeg64-turbo".to_owned(), Some(NegRequire("amd64".to_owned())))
1708        );
1709    }
1710
1711    #[test]
1712    fn format_conffiles_empty() {
1713        let actual = format_conffiles::<String>(&[]);
1714        assert_eq!("", actual);
1715    }
1716
1717    #[test]
1718    fn format_conffiles_one() {
1719        let actual = format_conffiles(&["/etc/my-pkg/conf.toml"]);
1720        assert_eq!("/etc/my-pkg/conf.toml\n", actual);
1721    }
1722
1723    #[test]
1724    fn format_conffiles_multiple() {
1725        let actual = format_conffiles(&["/etc/my-pkg/conf.toml", "etc/my-pkg/conf2.toml"]);
1726
1727        assert_eq!("/etc/my-pkg/conf.toml\n/etc/my-pkg/conf2.toml\n", actual);
1728    }
1729}