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 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
29impl 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 Require(String),
47 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
68fn 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]
88pub struct BuildEnvironment {
90 pub package_manifest_dir: PathBuf,
92 pub cargo_run_current_dir: PathBuf,
94 pub target_dir_base: PathBuf,
96 pub build_dir_base: Option<PathBuf>,
98 pub features: Vec<String>,
100 pub default_features: bool,
101 pub all_features: bool,
102 pub debug_symbols: DebugSymbols,
104 pub reproducible: bool,
106
107 pub(crate) build_profile: BuildProfile,
108 cargo_build_cmd: String,
109 cargo_build_flags: Vec<String>,
110
111 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 pub cargo_crate_name: String,
129 pub deb_name: String,
131 pub deb_source: Option<String>,
133 pub deb_version: String,
135 pub license_identifier: Option<String>,
137 pub license_file_rel_path: Option<PathBuf>,
139 pub license_file_skip_lines: usize,
141 pub copyright: Option<String>,
144 pub changelog: Option<String>,
145 pub homepage: Option<String>,
147 pub documentation: Option<String>,
149 pub repository: Option<String>,
151 pub description: String,
153 pub extended_description: ExtendedDescription,
155 pub maintainer: Option<String>,
158 pub wildcard_depends: String,
160 pub resolved_depends: Option<String>,
162 pub pre_depends: Option<String>,
164 pub recommends: Option<String>,
166 pub suggests: Option<String>,
168 pub enhances: Option<String>,
170 pub section: Option<String>,
172 pub priority: String,
174
175 pub conflicts: Option<String>,
179 pub breaks: Option<String>,
183 pub replaces: Option<String>,
187 pub provides: Option<String>,
191
192 pub architecture: String,
194 pub(crate) rust_target_triple: Option<String>,
196 pub multiarch: Multiarch,
198 pub conf_files: Vec<String>,
201 pub(crate) assets: Assets,
203
204 pub readme_rel_path: Option<PathBuf>,
206 pub triggers_file_rel_path: Option<PathBuf>,
208 pub maintainer_scripts_rel_path: Option<PathBuf>,
210 pub preserve_symlinks: bool,
212 pub(crate) systemd_units: Option<Vec<SystemdUnitsConfig>>,
214 pub default_timestamp: u64,
216 pub is_split_dbgsym_package: bool,
218}
219
220#[derive(Debug, Copy, Clone, Eq, PartialEq)]
221pub enum DebugSymbols {
222 Keep,
224 Strip,
225 Separate {
227 compress: CompressDebugSymbols,
229 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#[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 pub profile_name: Option<String>,
261 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 #[default]
290 None,
291 Same,
293 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 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 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 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 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)) .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)) .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 } else { CompressDebugSymbols::Zlib }, 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) },
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 strip_override_default.unwrap_or(DebugSymbols::Strip)
542 },
543 ManifestDebugFlags::Default => {
544 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 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 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 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 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 for line in license_string.lines().skip(package_deb.license_file_skip_lines) {
776 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 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 if source_path.extension().is_some_and(|e| e == "gz") {
815 return Ok(content);
816 }
817 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), 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 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 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 fn reset_deb_temp_directory(&self, package_deb: &PackageConfig) -> io::Result<()> {
951 let deb_temp_dir = self.deb_temp_dir(package_deb);
952 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 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 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 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 pub fn resolved_binary_dependencies(&self, listener: &dyn Listener) -> CDResult<String> {
1140 let lib_search_paths = self.rust_target_triple.is_some()
1142 .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 { 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 fn all_binaries(&self) -> Vec<&Asset> {
1204 self.assets.resolved.iter()
1205 .filter(|asset| {
1206 asset.c.is_dynamic_library() || asset.is_binary_executable()
1208 })
1209 .collect()
1210 }
1211
1212 pub(crate) fn built_binaries_mut(&mut self) -> Vec<&mut Asset> {
1214 self.assets.resolved.iter_mut()
1215 .filter(move |asset| {
1216 asset.c.is_built() && (asset.c.is_dynamic_library() || asset.c.is_executable())
1218 })
1219 .collect()
1220 }
1221
1222 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 pub fn generate_control(&self, config: &BuildEnvironment) -> CDResult<String> {
1247 use fmt::Write;
1248
1249 let mut control = String::with_capacity(1024);
1251
1252 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) .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 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 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
1555fn debian_package_name(crate_name: &str) -> String {
1557 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 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 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
1689fn 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 assert_eq!(
1742 get_architecture_specification("libjpeg64-turbo [armhf]").expect("arch"),
1743 ("libjpeg64-turbo".to_owned(), Some(Require("armhf".to_owned())))
1744 );
1745 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}