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
20pub 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()) .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 let debug_target_path = get_target_debug_path(asset, path, &lib_dir_base)?;
102
103 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 .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 };
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 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 .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 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}