1#![recursion_limit = "128"]
2#![allow(clippy::case_sensitive_file_extension_comparisons)]
3#![allow(clippy::if_not_else)]
4#![allow(clippy::missing_errors_doc)]
5#![allow(clippy::missing_panics_doc)]
6#![allow(clippy::module_name_repetitions)]
7#![allow(clippy::redundant_closure_for_method_calls)]
8#![allow(clippy::similar_names)]
9#![allow(clippy::assigning_clones)] pub mod deb {
30 pub mod ar;
31 pub mod control;
32 pub mod tar;
33}
34#[macro_use]
35mod util;
36mod dh {
37 pub(crate) mod dh_installsystemd;
38 pub(crate) mod dh_lib;
39}
40pub mod listener;
41pub(crate) mod parse {
42 pub(crate) mod cargo;
43 pub(crate) mod manifest;
44}
45pub use crate::config::{BuildEnvironment, BuildProfile, DebugSymbols, PackageConfig};
46pub use crate::deb::ar::DebArchive;
47pub use crate::error::*;
48pub use crate::util::compress;
49use crate::util::compress::{CompressConfig, Format};
50
51pub mod assets;
52pub mod config;
53mod debuginfo;
54mod dependencies;
55mod error;
56pub use debuginfo::strip_binaries;
57
58use crate::assets::{apply_compressed_assets, compressed_assets};
59use crate::deb::control::ControlArchiveBuilder;
60use crate::deb::tar::Tarball;
61use crate::listener::{Listener, PrefixedListener};
62use config::BuildOptions;
63use rayon::prelude::*;
64use std::path::{Path, PathBuf};
65use std::process::Command;
66use std::{env, fs};
67
68const DEFAULT_TARGET: &str = env!("CARGO_DEB_DEFAULT_TARGET");
70
71pub const DBGSYM_DEFAULT: bool = cfg!(feature = "default_enable_dbgsym");
72pub const SEPARATE_DEBUG_SYMBOLS_DEFAULT: bool = cfg!(feature = "default_enable_separate_debug_symbols");
73pub const COMPRESS_DEBUG_SYMBOLS_DEFAULT: bool = cfg!(feature = "default_enable_compress_debug_symbols");
74
75pub struct CargoDeb<'tmp> {
76 pub options: BuildOptions<'tmp>,
77 pub no_build: bool,
78 pub verbose_cargo_build: bool,
80 pub verbose: bool,
82 pub compress_config: CompressConfig,
83 pub deb_output: Option<OutputPath<'tmp>>,
85 pub install: (bool, bool),
87}
88
89pub struct OutputPath<'tmp> {
90 pub path: &'tmp Path,
91 pub is_dir: bool,
92}
93
94impl CargoDeb<'_> {
95 pub fn process(mut self, listener: &dyn Listener) -> CDResult<()> {
96 if self.install.0 || self.options.rust_target_triples.is_empty() {
97 warn_if_not_linux(listener); }
99
100 if self.options.debug.generate_dbgsym_package == Some(true) {
101 let _ = self.options.debug.separate_debug_symbols.get_or_insert(true);
102 }
103 let asked_for_dbgsym_package = self.options.debug.generate_dbgsym_package.unwrap_or(false);
104 let single_target_needs_back_compat = self.deb_output.is_none() && self.options.rust_target_triples.len() == 1;
105
106 if matches!(self.options.build_profile.profile_name(), "debug" | "dev") {
111 listener.warning("dev profile is not supported and will be a hard error in the future. \
112 cargo-deb is for making releases, and it doesn't make sense to use it with dev profiles.\n\
113 To enable debug symbols set `[profile.release] debug = 1` instead, or use --debug-override. \
114 Cargo also supports custom profiles, you can make `[profile.dist]`, etc.".into());
115 }
116
117 let (config, package_debs) = BuildEnvironment::from_manifest(self.options, listener)?;
118
119 if !self.no_build {
120 config.cargo_build(&package_debs, self.verbose, self.verbose_cargo_build, listener)?;
121 }
122
123 let common_suffix_len = Self::rust_target_triple_common_suffix_len(&package_debs);
124
125 let tmp_dir;
126 let output = if let Some(d) = self.deb_output { d } else {
127 tmp_dir = config.default_deb_output_dir();
128 OutputPath { path: &tmp_dir, is_dir: true }
129 };
130
131 package_debs.into_par_iter().try_for_each(|package_deb| {
132 let tmp_prefix;
133 let tmp_listener;
134 let mut listener = listener;
135 if common_suffix_len != 0 {
136 let target = package_deb.rust_target_triple.as_deref().unwrap_or(DEFAULT_TARGET);
137 let target = target.get(..target.len().saturating_sub(common_suffix_len)).unwrap_or(target);
138 tmp_prefix = format!("{target}: ");
139 tmp_listener = PrefixedListener(&tmp_prefix, listener);
140 listener = &tmp_listener;
141 }
142
143 Self::process_package(package_deb, &config, listener, &self.compress_config, &output, self.install, asked_for_dbgsym_package, single_target_needs_back_compat)
144 })
145 }
146
147 fn process_package(mut package_deb: PackageConfig, config: &BuildEnvironment, listener: &dyn Listener, compress_config: &CompressConfig, output: &OutputPath<'_>, (install, install_dbgsym): (bool, bool), asked_for_dbgsym_package: bool, needs_back_compat: bool) -> CDResult<()> {
148 package_deb.resolve_assets(listener)?;
149
150 let (depends, compressed_assets) = rayon::join(
151 || package_deb.resolved_binary_dependencies(listener),
152 || compressed_assets(&package_deb, listener),
153 );
154
155 debug_assert!(package_deb.resolved_depends.is_none());
156 package_deb.resolved_depends = Some(depends?);
157 apply_compressed_assets(&mut package_deb, compressed_assets?);
158
159 strip_binaries(config, &mut package_deb, asked_for_dbgsym_package, listener)?;
160
161 let generate_dbgsym_package = matches!(config.debug_symbols, DebugSymbols::Separate { generate_dbgsym_package: true, .. });
162 let package_dbgsym_ddeb = generate_dbgsym_package.then(|| package_deb.split_dbgsym()).flatten();
163
164 if package_dbgsym_ddeb.is_none() && generate_dbgsym_package {
165 listener.warning("No debug symbols found. Skipping dbgsym.ddeb".into());
166 }
167
168 let (generated_deb, generated_dbgsym_ddeb) = rayon::join(
169 || {
170 package_deb.sort_assets_by_type();
171 write_deb(
172 config,
173 package_deb.deb_output_path(output),
174 &package_deb,
175 compress_config,
176 listener,
177 )
178 },
179 || package_dbgsym_ddeb.map(|mut ddeb| {
180 ddeb.sort_assets_by_type();
181 write_deb(
182 config,
183 ddeb.deb_output_path(output),
184 &ddeb,
185 compress_config,
186 &PrefixedListener("ddeb: ", listener),
187 )
188 }),
189 );
190 let generated_deb = generated_deb?;
191 let generated_dbgsym_ddeb = generated_dbgsym_ddeb.transpose()?;
192
193 if let Some(generated) = &generated_dbgsym_ddeb {
194 let _ = back_compat_copy(generated, &package_deb, needs_back_compat);
195 listener.generated_archive(generated);
196 }
197 let _ = back_compat_copy(&generated_deb, &package_deb, needs_back_compat);
198 listener.generated_archive(&generated_deb);
199
200 if install {
201 if let Some(dbgsym_ddeb) = generated_dbgsym_ddeb.as_deref().filter(|_| install_dbgsym) {
202 install_debs(&[&generated_deb, dbgsym_ddeb])?;
203 } else {
204 install_debs(&[&generated_deb])?;
205 }
206 }
207 Ok(())
208 }
209
210 fn rust_target_triple_common_suffix_len(package_debs: &[PackageConfig]) -> usize {
212 if package_debs.len() < 2 {
213 return 0;
214 }
215 let targets = package_debs.iter()
216 .map(|p| p.rust_target_triple.as_deref().unwrap_or(DEFAULT_TARGET))
217 .collect::<Vec<_>>();
218 let Some((&(mut common_suffix), rest)) = targets.split_first() else {
219 return 0;
220 };
221
222 for &label in rest {
223 let common_len = common_suffix.split('-').rev()
224 .zip(label.split('-').rev())
225 .take_while(|(a, b)| a == b)
226 .map(|(a, _)| a.len() + 1)
227 .sum::<usize>();
228 common_suffix = &common_suffix[common_suffix.len().saturating_sub(common_len)..];
229 }
230 common_suffix.len()
231 }
232}
233
234#[derive(Copy, Clone, Default, Debug)]
235pub struct CargoLockingFlags {
236 pub offline: bool,
238 pub frozen: bool,
240 pub locked: bool,
242}
243
244impl CargoLockingFlags {
245 #[inline]
246 pub(crate) fn flags(self) -> impl Iterator<Item = &'static str> {
247 [
248 self.offline.then_some("--offline"),
249 self.frozen.then_some("--frozen"),
250 self.locked.then_some("--locked"),
251 ].into_iter().flatten()
252 }
253}
254
255impl Default for CargoDeb<'_> {
256 fn default() -> Self {
257 Self {
258 options: BuildOptions::default(),
259 no_build: false,
260 deb_output: None,
261 verbose: false,
262 verbose_cargo_build: false,
263 install: (false, false),
264 compress_config: CompressConfig {
265 fast: false,
266 compress_type: Format::Xz,
267 compress_system: false,
268 rsyncable: false,
269 },
270 }
271 }
272}
273
274pub fn install_debs(paths: &[&Path]) -> CDResult<()> {
276 let no_sudo = std::env::var_os("EUID").or_else(|| std::env::var_os("UID")).is_some_and(|v| v == "0");
277 match install_debs_inner(paths, no_sudo) {
278 Err(CargoDebError::CommandFailed(_, cmd)) if cmd == "sudo" => {
279 install_debs_inner(paths, true)
280 },
281 res => res,
282 }
283}
284
285fn install_debs_inner(paths: &[&Path], no_sudo: bool) -> CDResult<()> {
286 let args = ["dpkg", "-i", "--"];
287 let (exe, args) = if no_sudo {
288 ("dpkg", &args[1..])
289 } else {
290 ("sudo", &args[..])
291 };
292 let mut cmd = Command::new(exe);
293 cmd.args(args);
294 cmd.args(paths);
295 log::debug!("{exe} {:?}", cmd.get_args());
296 let status = cmd.status()
297 .map_err(|e| CargoDebError::CommandFailed(e, exe.into()))?;
298 if !status.success() {
299 return Err(CargoDebError::InstallFailed(status));
300 }
301 Ok(())
302}
303
304pub fn write_deb(config: &BuildEnvironment, deb_output_path: PathBuf, package_deb: &PackageConfig, &CompressConfig { fast, compress_type, compress_system, rsyncable }: &CompressConfig, listener: &dyn Listener) -> Result<PathBuf, CargoDebError> {
305 let (deb_contents, data_result) = rayon::join(
306 move || {
307 let mut control_builder = ControlArchiveBuilder::new(util::compress::select_compressor(fast, compress_type, compress_system)?, package_deb.default_timestamp, listener);
309 control_builder.generate_archive(config, package_deb)?;
310 let control_compressed = control_builder.finish()?.finish()?;
311
312 let mut deb_contents = DebArchive::new(deb_output_path, package_deb.default_timestamp)?;
313 let compressed_control_size = control_compressed.len();
314 deb_contents.add_control(control_compressed)?;
315 Ok::<_, CargoDebError>((deb_contents, compressed_control_size))
316 },
317 move || {
318 let dest = util::compress::select_compressor(fast, compress_type, compress_system)?;
320 let archive = Tarball::new(dest, package_deb.default_timestamp);
321 let compressed = archive.archive_files(package_deb, rsyncable, listener)?;
322 let original_data_size = compressed.uncompressed_size;
323 Ok::<_, CargoDebError>((compressed.finish()?, original_data_size))
324 },
325 );
326 let (mut deb_contents, compressed_control_size) = deb_contents?;
327 let (data_compressed, original_data_size) = data_result?;
328
329 let compressed_size = data_compressed.len() + compressed_control_size;
330 let original_size = original_data_size + compressed_control_size; listener.progress("Compressed", format!(
332 "{}KB to {}KB (by {}%)",
333 original_data_size / 1000,
334 compressed_size / 1000,
335 (original_size.saturating_sub(compressed_size)) * 100 / original_size,
336 ));
337 deb_contents.add_data(data_compressed)?;
338 let generated = deb_contents.finish()?;
339
340 let deb_temp_dir = config.deb_temp_dir(package_deb);
341 let _ = fs::remove_dir(&deb_temp_dir);
342
343 Ok(generated)
344}
345
346fn debian_triple_from_rust_triple(rust_target_triple: &str) -> String {
348 let mut p = rust_target_triple.split('-');
349 let arch = p.next().unwrap();
350 let abi = p.next_back().unwrap_or("gnu");
351
352 let (darch, dabi) = match (arch, abi) {
353 ("i586" | "i686", _) => ("i386", "gnu"),
354 ("x86_64", _) => ("x86_64", "gnu"),
355 ("aarch64", _) => ("aarch64", "gnu"),
356 (arm, abi) if arm.starts_with("arm") || arm.starts_with("thumb") => {
357 ("arm", if abi.ends_with("hf") {"gnueabihf"} else {"gnueabi"})
358 },
359 ("mipsel", _) => ("mipsel", "gnu"),
360 (mips @ ("mips64" | "mips64el"), "musl" | "muslabi64") => (mips, "gnuabi64"),
361 ("loongarch64", _) => ("loongarch64", "gnu"), (risc, _) if risc.starts_with("riscv64") => ("riscv64", "gnu"),
363 (risc, _) if risc.starts_with("riscv32") => ("riscv32", "gnu"),
364 (arch, "muslspe") => (arch, "gnuspe"),
365 (arch, "musl" | "uclibc") => (arch, "gnu"),
366 (arch, abi) => (arch, abi),
367 };
368 format!("{darch}-linux-{dabi}")
369}
370
371pub(crate) fn debian_architecture_from_rust_triple(rust_target_triple: &str) -> &str {
373 let mut parts = rust_target_triple.split('-');
374 let arch = parts.next().unwrap();
375 let abi = parts.next_back().unwrap_or("");
376 match (arch, abi) {
377 ("aarch64" | "aarch64_be", _) => "arm64",
381 ("mips64", "gnuabi32") => "mipsn32",
382 ("mips64el", "gnuabi32") => "mipsn32el",
383 ("mipsisa32r6", _) => "mipsr6",
384 ("mipsisa32r6el", _) => "mipsr6el",
385 ("mipsisa64r6", "gnuabi64") => "mips64r6",
386 ("mipsisa64r6", "gnuabi32") => "mipsn32r6",
387 ("mipsisa64r6el", "gnuabi64") => "mips64r6el",
388 ("mipsisa64r6el", "gnuabi32") => "mipsn32r6el",
389 ("powerpc", "gnuspe" | "muslspe") => "powerpcspe",
390 ("powerpc64", _) => "ppc64",
391 ("powerpc64le", _) => "ppc64el",
392 ("riscv32gc", _) => "riscv32",
393 ("i586" | "i686" | "x86", _) => "i386",
394 ("x86_64", "gnux32") => "x32",
395 ("x86_64", _) => "amd64",
396 ("loongarch64", _) => "loong64",
397 (risc, _) if risc.starts_with("riscv64") => "riscv64",
398 (arm, gnueabi) if arm.starts_with("arm") && gnueabi.ends_with("hf") => "armhf",
399 (arm, _) if arm.starts_with("arm") || arm.starts_with("thumb") => "armel",
400 (other_arch, _) => other_arch,
401 }
402}
403
404#[test]
405fn ensure_all_rust_targets_map_to_debian_targets() {
406 assert_eq!(debian_triple_from_rust_triple("armv7-unknown-linux-gnueabihf"), "arm-linux-gnueabihf");
407
408 const DEB_ARCHS: &[&str] = &["alpha", "amd64", "arc", "arm", "arm64", "arm64ilp32", "armel",
409 "armhf", "hppa", "hurd-i386", "hurd-amd64", "i386", "ia64", "kfreebsd-amd64",
410 "kfreebsd-i386", "loong64", "m68k", "mips", "mipsel", "mips64", "mips64el",
411 "mipsn32", "mipsn32el", "mipsr6", "mipsr6el", "mips64r6", "mips64r6el", "mipsn32r6",
412 "mipsn32r6el", "powerpc", "powerpcspe", "ppc64", "ppc64el", "riscv64", "riscv32", "s390",
413 "s390x", "sh4", "sparc", "sparc64", "uefi-amd6437", "uefi-arm6437", "uefi-armhf37",
414 "uefi-i38637", "x32"];
415
416 const DEB_TUPLES: &[&str] = &["aarch64-linux-gnu", "aarch64-linux-gnu_ilp32", "aarch64-uefi",
417 "aarch64_be-linux-gnu", "aarch64_be-linux-gnu_ilp32", "alpha-linux-gnu", "arc-linux-gnu",
418 "arm-linux-gnu", "arm-linux-gnueabi", "arm-linux-gnueabihf", "arm-uefi", "armeb-linux-gnueabi",
419 "armeb-linux-gnueabihf", "hppa-linux-gnu", "i386-gnu", "i386-kfreebsd-gnu",
420 "i386-linux-gnu", "i386-uefi", "ia64-linux-gnu", "loongarch64-linux-gnu",
421 "m68k-linux-gnu", "mips-linux-gnu", "mips64-linux-gnuabi64", "mips64-linux-gnuabin32",
422 "mips64el-linux-gnuabi64", "mips64el-linux-gnuabin32", "mipsel-linux-gnu",
423 "mipsisa32r6-linux-gnu", "mipsisa32r6el-linux-gnu", "mipsisa64r6-linux-gnuabi64",
424 "mipsisa64r6-linux-gnuabin32", "mipsisa64r6el-linux-gnuabi64", "mipsisa64r6el-linux-gnuabin32",
425 "powerpc-linux-gnu", "powerpc-linux-gnuspe", "powerpc64-linux-gnu", "powerpc64le-linux-gnu",
426 "riscv64-linux-gnu", "s390-linux-gnu", "s390x-linux-gnu", "sh4-linux-gnu",
427 "sparc-linux-gnu", "sparc64-linux-gnu", "x86_64-gnu", "x86_64-kfreebsd-gnu",
428 "x86_64-linux-gnu", "x86_64-linux-gnux32", "x86_64-uefi", "riscv32-linux-gnu"];
429
430 let list = std::process::Command::new("rustc").arg("--print=target-list").output().unwrap().stdout;
431 for rust_target in std::str::from_utf8(&list).unwrap().lines().filter(|a| a.contains("linux")) {
432 if ["csky", "hexagon", "wasm32"].contains(&rust_target.split_once('-').unwrap().0) {
433 continue; }
435 let deb_arch = debian_architecture_from_rust_triple(rust_target);
436 assert!(DEB_ARCHS.contains(&deb_arch), "{rust_target} => {deb_arch}");
437 let deb_tuple = debian_triple_from_rust_triple(rust_target);
438 assert!(DEB_TUPLES.contains(&deb_tuple.as_str()), "{rust_target} => {deb_tuple}");
439 }
440}
441
442#[cfg(target_os = "linux")]
443fn warn_if_not_linux(_: &dyn Listener) {
444}
445
446#[cfg(not(target_os = "linux"))]
447fn warn_if_not_linux(listener: &dyn Listener) {
448 listener.warning(format!("You're creating a package only for {}, and not for Linux.\nUse --target if you want to cross-compile.", std::env::consts::OS));
449}
450
451#[cold]
453fn back_compat_copy(path: &Path, package_deb: &PackageConfig, enable: bool) -> Option<()> {
454 if !enable {
455 return None;
456 }
457 let previous_path = path.parent()?.parent()?
458 .join(package_deb.rust_target_triple.as_deref()?)
459 .join("debian")
460 .join(path.file_name()?);
461 let _ = fs::create_dir_all(previous_path.parent()?);
462 fs::hard_link(path, &previous_path)
463 .or_else(|_| fs::copy(path, &previous_path).map(drop))
464 .inspect_err(|e| log::warn!("can't copy {} to {}: {e}", path.display(), previous_path.display()))
465 .ok()
466}