Skip to main content

cargo_deb/
lib.rs

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