Skip to main content

cargo_deb/
config.rs

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