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