cargo_deb/
debuginfo.rs

1use crate::assets::{Asset, AssetSource, IsBuilt, ProcessedFrom};
2use crate::config::{BuildEnvironment, CompressDebugSymbols, DebugSymbols, PackageConfig};
3use crate::error::{CDResult, CargoDebError};
4use crate::listener::Listener;
5use crate::parse::cargo::CargoConfig;
6use rayon::prelude::*;
7use std::borrow::Cow;
8use std::path::{Path, PathBuf};
9use std::process::{Command, ExitStatus};
10use std::{env, fs, io};
11
12fn ensure_success(status: ExitStatus) -> io::Result<()> {
13    if status.success() {
14        Ok(())
15    } else {
16        Err(io::Error::other(status.to_string()))
17    }
18}
19
20/// Strips the binary that was created with cargo
21pub fn strip_binaries(config: &BuildEnvironment, package_deb: &mut PackageConfig, asked_for_dbgsym_package: bool, listener: &dyn Listener) -> CDResult<()> {
22    let (separate_debug_symbols, compress_debug_symbols) = match config.debug_symbols {
23        DebugSymbols::Keep => return Ok(()),
24        DebugSymbols::Strip => (false, CompressDebugSymbols::No),
25        DebugSymbols::Separate { compress, .. } => (true, compress),
26    };
27
28    let mut cargo_config = None;
29    let objcopy_tmp;
30    let strip_tmp;
31    let mut objcopy_cmd = Path::new("objcopy");
32    let mut strip_cmd = Path::new("strip");
33
34    if let Some(rust_target_triple) = &package_deb.rust_target_triple {
35        cargo_config = config.cargo_config().ok().flatten();
36        if let Some(cmd) = target_specific_command(cargo_config.as_ref(), "objcopy", rust_target_triple) {
37            listener.info(format!("Using '{}' for '{rust_target_triple}'", cmd.display()));
38            objcopy_tmp = cmd;
39            objcopy_cmd = &objcopy_tmp;
40        }
41
42        if let Some(cmd) = target_specific_command(cargo_config.as_ref(), "strip", rust_target_triple) {
43            listener.info(format!("Using '{}' for '{rust_target_triple}'", cmd.display()));
44            strip_tmp = cmd;
45            strip_cmd = &strip_tmp;
46        }
47    }
48
49    let stripped_binaries_output_dir = config.deb_temp_dir(package_deb);
50    debug_assert!(stripped_binaries_output_dir.is_dir());
51
52    let lib_dir_base = package_deb.library_install_dir();
53    let target_triple = package_deb.rust_target_triple.clone();
54    let added_debug_assets = package_deb.built_binaries_mut().into_par_iter().enumerate()
55        .filter(|(_, asset)| !asset.source.archive_as_symlink_only()) // data won't be included, so nothing to strip
56        .map(|(i, asset)| {
57        let (new_source, new_debug_asset) = if let Some(path) = asset.source.path() {
58            if !path.exists() {
59                return Err(CargoDebError::StripFailed(path.to_owned(), format!("The file doesn't exist\nnote: needed for {}", asset.c.target_path.display())));
60            }
61
62            let cargo_config_path = cargo_config.as_ref().map(|c| c.path()).unwrap_or(".cargo/config.toml".as_ref());
63            let file_name = path.file_stem().and_then(|f| f.to_str()).ok_or(CargoDebError::Str("bad path"))?;
64            let stripped_temp_path = stripped_binaries_output_dir.join(format!("{file_name}.stripped-{i}.tmp"));
65            let _ = fs::remove_file(&stripped_temp_path);
66
67            run_strip(strip_cmd, &stripped_temp_path, path, &["--strip-unneeded", "--remove-section=.comment", "--remove-section=.note"])
68                .or_else(|err| {
69                    use std::fmt::Write;
70                    let mut help_text = String::new();
71                    if let Some(target) = &target_triple {
72                        write!(&mut help_text, "\nnote: Target-specific strip commands are configured in {}: `[target.{target}] strip = {{ path = \"{}\" }}`", cargo_config_path.display(), strip_cmd.display()).unwrap();
73                    }
74                    if !separate_debug_symbols {
75                        write!(&mut help_text, "\nnote: You can add `[profile.{}] strip=true` or run with --no-strip",
76                            config.build_profile.example_profile_name()).unwrap();
77                    }
78
79                    let msg = match err {
80                        Some(err) if err.kind() == io::ErrorKind::NotFound => {
81                            return Err(CargoDebError::CommandFailed(err, strip_cmd.display().to_string().into())
82                                .context(format!("can't separate debug symbols{help_text}")));
83                        },
84                        Some(err) => format!("{}: {err}{help_text}", strip_cmd.display()),
85                        None => format!("{} command failed to create output '{}'{help_text}", strip_cmd.display(), stripped_temp_path.display()),
86                    };
87
88                    match run_strip(strip_cmd, &stripped_temp_path, path, &[]) {
89                        Ok(()) => {
90                            listener.warning(format!("strip didn't support additional arguments: {msg}"));
91                            Ok(())
92                        },
93                        Err(_) => Err(CargoDebError::StripFailed(path.to_owned(), msg)),
94                    }
95                })?;
96
97            let new_debug_asset = if separate_debug_symbols {
98                log::debug!("extracting debug info with {} from {}", objcopy_cmd.display(), path.display());
99
100                // parse the ELF and use debug-id-based path if available
101                let debug_target_path = get_target_debug_path(asset, path, &lib_dir_base)?;
102
103                // --add-gnu-debuglink reads the file path given, so it can't get to-be-installed target path
104                // and the recommended fallback solution is to give it relative path in the same dir
105                let debug_temp_path = stripped_temp_path.with_file_name(debug_target_path.file_name().ok_or("bad .debug")?);
106                let _ = fs::remove_file(&debug_temp_path);
107
108                let mut cmd = Command::new(objcopy_cmd);
109                cmd.arg("--only-keep-debug");
110
111                if config.reproducible {
112                    cmd.arg("--enable-deterministic-archives");
113                }
114                match compress_debug_symbols {
115                    CompressDebugSymbols::No => {},
116                    CompressDebugSymbols::Zstd => { cmd.arg("--compress-debug-sections=zstd"); },
117                    CompressDebugSymbols::Zlib | CompressDebugSymbols::Auto => { cmd.arg("--compress-debug-sections=zlib"); },
118                }
119
120                cmd.arg(path).arg(&debug_temp_path)
121                    .status()
122                    .and_then(ensure_success)
123                    .map_err(|err| {
124                        use std::fmt::Write;
125                        let mut help_text = String::new();
126
127                        if let Some(target) = &target_triple {
128                            write!(&mut help_text, "\nnote: Target-specific objcopy commands are configured in {}: `[target.{target}] objcopy = {{ path =\"{}\" }}`", cargo_config_path.display(), objcopy_cmd.display()).unwrap();
129                        }
130                        help_text.push_str("\nnote: Use --no-separate-debug-symbols if you don't have objcopy");
131                        if err.kind() == io::ErrorKind::NotFound {
132                            CargoDebError::CommandFailed(err, objcopy_cmd.display().to_string().into())
133                                .context(format!("can't separate debug symbols{help_text}"))
134                        } else {
135                            CargoDebError::StripFailed(path.to_owned(), format!("{}: {err}{help_text}", objcopy_cmd.display()))
136                        }
137                    })?;
138
139                let relative_debug_temp_path = debug_temp_path.file_name().ok_or(CargoDebError::Str("bad path"))?;
140                log::debug!("linking debug info with {} from {} into {:?}", objcopy_cmd.display(), stripped_temp_path.display(), relative_debug_temp_path);
141                Command::new(objcopy_cmd)
142                    .current_dir(debug_temp_path.parent().ok_or(CargoDebError::Str("bad path"))?)
143                    .arg("--add-gnu-debuglink")
144                    // intentionally relative - the file name must match debug_target_path
145                    .arg(relative_debug_temp_path)
146                    .arg(&stripped_temp_path)
147                    .status()
148                    .and_then(ensure_success)
149                    .map_err(|err| CargoDebError::CommandFailed(err, "objcopy".into()))?;
150
151                Some(Asset::new(
152                    AssetSource::Path(debug_temp_path),
153                    debug_target_path,
154                    0o666 & asset.c.chmod,
155                    IsBuilt::No,
156                    crate::assets::AssetKind::SeparateDebugSymbols,
157                ).processed(if compress_debug_symbols != CompressDebugSymbols::No {"compress"} else {"separate"}, path.to_path_buf()))
158            } else {
159                None // no new asset
160            };
161
162            if separate_debug_symbols && new_debug_asset.is_some() {
163                listener.progress("Split", format!("debug info from '{}'", path.display()));
164            } else if !separate_debug_symbols && asked_for_dbgsym_package {
165                listener.info(format!("No debug info in '{}'", path.display()));
166            } else {
167                listener.progress("Stripped", format!("'{}'", path.display()));
168            }
169
170            (AssetSource::Path(stripped_temp_path), new_debug_asset)
171        } else {
172            // This is unexpected - emit a warning if we come across it
173            listener.warning(format!("Found built asset with non-path source '{asset:?}'"));
174            return Ok(None);
175        };
176        log::debug!("Replacing asset {} with stripped asset {}", asset.source.path().unwrap().display(), new_source.path().unwrap().display());
177        let old_source = std::mem::replace(&mut asset.source, new_source);
178        asset.processed_from = Some(ProcessedFrom {
179            original_path: old_source.into_path(),
180            action: "strip",
181        });
182        Ok::<_, CargoDebError>(new_debug_asset)
183    }).collect::<Result<Vec<_>, _>>()?;
184
185    package_deb.assets.resolved
186        .extend(added_debug_assets.into_iter().flatten());
187
188    Ok(())
189}
190
191fn run_strip(strip_cmd: &Path, stripped_temp_path: &PathBuf, path: &Path, args: &[&str]) -> Result<(), Option<io::Error>> {
192    log::debug!("stripping with {} from {} into {}; {args:?}", strip_cmd.display(), path.display(), stripped_temp_path.display());
193    Command::new(strip_cmd)
194       // same as dh_strip
195       .args(args)
196       .arg("-o").arg(stripped_temp_path)
197       .arg(path)
198       .status()
199       .and_then(ensure_success)
200       .map_err(|err| {
201            Some(err)
202        })?;
203    if !stripped_temp_path.exists() {
204        return Err(None);
205    }
206    Ok(())
207}
208
209fn target_specific_command<'a>(cargo_config: Option<&'a CargoConfig>, command_name: &str, rust_target_triple: &str) -> Option<Cow<'a, Path>> {
210    if let Some(cmd) = cargo_config.and_then(|c| c.explicit_target_specific_command(command_name, rust_target_triple)) {
211        return Some(cmd.into());
212    }
213
214    let debian_target_triple = crate::debian_triple_from_rust_triple(rust_target_triple);
215    let env_prefix = command_name.to_ascii_uppercase();
216    for env_name in &[format!("{env_prefix}_{debian_target_triple}"), format!("{env_prefix}_{rust_target_triple}")] {
217        if let Some(path) = env::var_os(env_name).filter(|p| !p.is_empty()).map(PathBuf::from) {
218            log::debug!("found env {env_name}={}", path.display());
219            return Some(path.into());
220        }
221    }
222
223    if let Some(linker) = cargo_config.and_then(|c| c.explicit_linker_command(rust_target_triple)) {
224        if linker.parent().is_some() {
225            let linker_file_name = linker.file_name()?.to_str()?;
226            // checks whether it's `/usr/bin/triple-ld` or `/custom-toolchain/ld`
227            let strip_path = if linker_file_name.starts_with(&debian_target_triple) {
228                linker.with_file_name(format!("{debian_target_triple}-{command_name}"))
229            } else {
230                linker.with_file_name(command_name)
231            };
232            if strip_path.exists() {
233                return Some(strip_path.into());
234            }
235        }
236    }
237
238    let path = PathBuf::from(format!("/usr/bin/{debian_target_triple}-{command_name}"));
239    if path.exists() {
240        return Some(path.into());
241    }
242    None
243}
244
245fn get_target_debug_path(asset: &Asset, asset_path: &Path, lib_dir_base: &Path) -> Result<PathBuf, CargoDebError> {
246    let target_debug_path = match elf_gnu_debug_id(asset_path, lib_dir_base) {
247        Ok(Some(path)) => {
248            log::debug!("got gnu debug-id: {} for {}", path.display(), asset_path.display());
249            path
250        },
251        Ok(None) => {
252            log::debug!("debug-id not found in {}", asset_path.display());
253            asset.c.default_debug_target_path(lib_dir_base)
254        },
255        Err(e) => {
256            log::debug!("elf: {e} in {}", asset_path.display());
257            asset.c.default_debug_target_path(lib_dir_base)
258        },
259    };
260    Ok(target_debug_path)
261}
262
263#[cfg(not(feature = "debug-id"))]
264fn elf_gnu_debug_id(_: &Path, _: &Path) -> io::Result<Option<PathBuf>> {
265    Ok(None)
266}
267
268#[cfg(feature = "debug-id")]
269fn elf_gnu_debug_id(elf_file_path: &Path, lib_dir_base: &Path) -> Result<Option<PathBuf>, elf::ParseError> {
270    use elf::endian::AnyEndian;
271    use elf::note::Note;
272    use elf::ElfStream;
273
274    let mut stream = ElfStream::<AnyEndian, _>::open_stream(fs::File::open(elf_file_path)?)?;
275    let Some(abi_shdr) = stream.section_header_by_name(".note.gnu.build-id")?
276        else { return Ok(None) };
277
278    let abi_shdr = *abi_shdr;
279    for note in stream.section_data_as_notes(&abi_shdr)? {
280        if let Note::GnuBuildId(note) = note {
281            if let Some((byte, rest)) = note.0.split_first() {
282                let mut s = format!("debug/.build-id/{byte:02x}/");
283                for b in rest {
284                    use std::fmt::Write;
285                    write!(&mut s, "{b:02x}").unwrap();
286                }
287                s.push_str(".debug");
288                return Ok(Some(lib_dir_base.join(s)));
289            }
290        }
291    }
292    Ok(None)
293}