1use crate::config::{BuildEnvironment, PackageConfig};
2use crate::deb::tar::Tarball;
3use crate::dh::{dh_installsystemd, dh_lib};
4use crate::error::{CDResult, CargoDebError};
5use crate::listener::Listener;
6use crate::util::{is_path_file, read_file_to_string};
7use dh_lib::ScriptFragments;
8use std::fs;
9use std::io::Write;
10use std::path::Path;
11
12pub struct ControlArchiveBuilder<'l, W: Write> {
13 archive: Tarball<W>,
14 listener: &'l dyn Listener,
15}
16
17impl<'l, W: Write> ControlArchiveBuilder<'l, W> {
18 pub fn new(dest: W, time: u64, listener: &'l dyn Listener) -> Self {
19 Self {
20 archive: Tarball::new(dest, time),
21 listener,
22 }
23 }
24
25 pub fn generate_archive(&mut self, config: &BuildEnvironment, package_deb: &PackageConfig) -> CDResult<()> {
27 self.add_control(package_deb.generate_control(config)?.as_bytes())?;
28
29 if let Some(files) = package_deb.conf_files() {
30 self.add_conf_files(&files)?;
31 }
32
33 self.generate_scripts(config, package_deb)?;
34 if let Some(rel_path) = &package_deb.triggers_file_rel_path {
35 self.add_triggers_file(config, rel_path)?;
36 }
37 Ok(())
38 }
39
40 pub fn finish(self) -> CDResult<W> {
41 self.archive.into_inner().map_err(|e| CargoDebError::Io(e).context("error while finalizing control archive"))
42 }
43
44 fn generate_scripts(&mut self, config: &BuildEnvironment, package_deb: &PackageConfig) -> CDResult<()> {
62 let Some(maintainer_scripts_dir) = &package_deb.maintainer_scripts_rel_path else {
63 return Ok(());
64 };
65
66 let maintainer_scripts_dir = config.path_in_cargo_crate(maintainer_scripts_dir);
67 let mut scripts = ScriptFragments::new();
68
69 if let Some(systemd_units_config_vec) = &package_deb.systemd_units {
70 for systemd_units_config in systemd_units_config_vec {
71 scripts = dh_installsystemd::generate(
74 &package_deb.deb_name,
75 &package_deb.assets.resolved,
76 &dh_installsystemd::Options::from(systemd_units_config),
77 self.listener,
78 )?;
79
80 let unit_name = systemd_units_config.unit_name.as_deref();
82
83 dh_lib::apply(
86 &maintainer_scripts_dir,
87 &mut scripts,
88 &package_deb.deb_name,
89 unit_name,
90 self.listener,
91 )?;
92 }
93 }
94
95 let mut found_any = !scripts.is_empty();
96
97 for name in ["config", "preinst", "postinst", "prerm", "postrm", "templates"] {
100 let script_path = maintainer_scripts_dir.join(name);
101 let script_path_exists = is_path_file(&script_path);
102 let (contents, source_path) = if let Some(script) = scripts.remove(name) {
103 if script_path_exists {
104 log::info!("maintainer script replaced by autogenerated systemd script {}", script_path.display());
105 }
106 (script, Some(Path::new("systemd_units")))
107 } else {
108 if !script_path_exists {
109 log::info!("maintainer script {} not found", script_path.display());
110 continue;
111 }
112 let file = read_file_to_string(&script_path)
113 .map_err(|e| CargoDebError::IoFile("Can't read script", e, script_path.clone()))?;
114 (file, Some(script_path.as_path()))
115 };
116
117 found_any = true;
118
119 let permissions = if name == "templates" { 0o644 } else { 0o755 };
124 self.add_file_with_log(name.as_ref(), contents.as_bytes(), permissions, source_path)?;
125 }
126
127 if !found_any {
128 self.listener.warning(format!("no maintainer scripts found in {}", maintainer_scripts_dir.display()));
129 }
130 Ok(())
131 }
132
133 fn add_file_with_log(&mut self, name: &Path, contents: &[u8], permissions: u32, source_path: Option<&Path>) -> CDResult<()> {
134 let source_path = source_path.and_then(|s| s.to_str()).unwrap_or("-");
135 self.listener.progress("Adding", format!("'{}' control-> {}", source_path, name.display()));
136 self.archive.file(name, contents, permissions)
137 }
138
139 fn add_control(&mut self, control: &[u8]) -> CDResult<()> {
141 self.archive.file("control", control, 0o644)?;
142 Ok(())
143 }
144
145 fn add_conf_files(&mut self, list: &str) -> CDResult<()> {
147 self.add_file_with_log("conffiles".as_ref(), list.as_bytes(), 0o644, None)
148 }
149
150 fn add_triggers_file(&mut self, config: &BuildEnvironment, rel_path: &Path) -> CDResult<()> {
151 let path = config.path_in_cargo_crate(rel_path);
152 let content = match fs::read(&path) {
153 Ok(p) => p,
154 Err(e) => return Err(CargoDebError::IoFile("Triggers file", e, path)),
155 };
156 self.add_file_with_log("triggers".as_ref(), &content, 0o644, Some(&path))
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
186 use crate::assets::{Asset, AssetSource, IsBuilt};
187 use crate::config::DebugSymbolOptions;
188 use crate::listener::MockListener;
189 use crate::parse::manifest::SystemdUnitsConfig;
190 use crate::util::tests::{add_test_fs_paths, set_test_fs_path_content};
191 use std::collections::HashMap;
192 use std::io::prelude::Read;
193 use std::path::PathBuf;
194
195 fn filename_from_path_str(path: &str) -> String {
196 let filename = Path::new(path).file_name().unwrap();
197 Path::new(".").join(filename).to_string_lossy().to_string()
198 }
199
200 fn decode_name<R>(entry: &tar::Entry<'_, R>) -> String where R: Read {
201 std::str::from_utf8(&entry.path_bytes()).unwrap().to_string()
202 }
203
204 fn decode_names<R>(ar: &mut tar::Archive<R>) -> Vec<String> where R: Read {
205 ar.entries().unwrap().map(|e| decode_name(&e.unwrap())).collect()
206 }
207
208 fn extract_contents<R>(ar: &mut tar::Archive<R>) -> HashMap<String, String> where R: Read {
209 let mut out = HashMap::new();
210 for entry in ar.entries().unwrap() {
211 let mut unwrapped = entry.unwrap();
212 let name = decode_name(&unwrapped);
213 let mut buf = Vec::new();
214 unwrapped.read_to_end(&mut buf).unwrap();
215 let content = String::from_utf8(buf).unwrap();
216 out.insert(name, content);
217 }
218 out
219 }
220
221 #[track_caller]
222 #[cfg(test)]
223 fn prepare<'l, W: Write>(dest: W, package_name: Option<&str>, mock_listener: &'l mut MockListener) -> (BuildEnvironment, PackageConfig, ControlArchiveBuilder<'l, W>) {
224 use crate::config::BuildOptions;
225
226 mock_listener.expect_progress().return_const(());
227
228 let (mut config, mut package_debs) = BuildEnvironment::from_manifest(
229 BuildOptions {
230 manifest_path: Some(Path::new("test-resources/testroot/Cargo.toml")),
231 selected_package_name: package_name,
232 debug: DebugSymbolOptions {
233 #[cfg(feature = "default_enable_dbgsym")]
234 generate_dbgsym_package: Some(false),
235 #[cfg(feature = "default_enable_separate_debug_symbols")]
236 separate_debug_symbols: Some(false),
237 ..Default::default()
238 },
239 ..Default::default()
240 },
241 mock_listener,
242 ).unwrap();
243 let package_deb = package_debs.pop().unwrap();
244
245 config.package_manifest_dir = config.package_manifest_dir.strip_prefix(env!("CARGO_MANIFEST_DIR")).unwrap().to_path_buf();
251
252 let ar = ControlArchiveBuilder::new(dest, 0, mock_listener);
253
254 (config, package_deb, ar)
255 }
256
257 #[test]
258 fn generate_scripts_does_nothing_if_maintainer_scripts_is_not_set() {
259 let mut listener = MockListener::new();
260 let (config, package_deb, mut in_ar) = prepare(vec![], None, &mut listener);
261
262 let _g = add_test_fs_paths(&["debian/postinst"]);
264
265 in_ar.generate_scripts(&config, &package_deb).unwrap();
267
268 let archive_bytes = in_ar.finish().unwrap();
270
271 let mut out_ar = tar::Archive::new(&archive_bytes[..]);
273
274 let archived_file_names = decode_names(&mut out_ar);
276 assert!(archived_file_names.is_empty());
277 }
278
279 #[test]
280 fn generate_scripts_archives_user_supplied_maintainer_scripts_in_root_package() {
281 let maintainer_script_paths = vec![
282 "test-resources/testroot/debian/config",
283 "test-resources/testroot/debian/preinst",
284 "test-resources/testroot/debian/postinst",
285 "test-resources/testroot/debian/prerm",
286 "test-resources/testroot/debian/postrm",
287 "test-resources/testroot/debian/templates",
288 ];
289 generate_scripts_for_package_without_systemd_unit(None, &maintainer_script_paths);
290 }
291
292 #[test]
293 fn generate_scripts_archives_user_supplied_maintainer_scripts_in_workspace_package() {
294 let maintainer_script_paths = vec![
295 "test-resources/testroot/testchild/debian/config",
296 "test-resources/testroot/testchild/debian/preinst",
297 "test-resources/testroot/testchild/debian/postinst",
298 "test-resources/testroot/testchild/debian/prerm",
299 "test-resources/testroot/testchild/debian/postrm",
300 "test-resources/testroot/testchild/debian/templates",
301 ];
302 generate_scripts_for_package_without_systemd_unit(Some("test_child"), &maintainer_script_paths);
303 }
304
305 #[track_caller]
306 fn generate_scripts_for_package_without_systemd_unit(package_name: Option<&str>, maintainer_script_paths: &[&'static str]) {
307 let mut listener = MockListener::new();
308 let (config, mut package_deb, mut in_ar) = prepare(vec![], package_name, &mut listener);
309
310 for script in maintainer_script_paths {
313 let content = format!("some contents: {script}");
314 set_test_fs_path_content(script, content.clone());
315 }
316
317 package_deb
319 .maintainer_scripts_rel_path
320 .get_or_insert(PathBuf::from("debian"));
321
322 in_ar.generate_scripts(&config, &package_deb).unwrap();
324
325 let archive_bytes = in_ar.finish().unwrap();
327
328 let mut out_ar = tar::Archive::new(&archive_bytes[..]);
330
331 let archived_content = extract_contents(&mut out_ar);
333
334 assert_eq!(maintainer_script_paths.len(), archived_content.len());
335
336 for script in maintainer_script_paths {
338 let expected_content = &format!("some contents: {script}");
339 let filename = filename_from_path_str(script);
340 let actual_content = archived_content.get(&filename).unwrap();
341 assert_eq!(expected_content, actual_content);
342 }
343 }
344
345 #[test]
346 fn generate_scripts_augments_maintainer_scripts_for_unit_in_root_package() {
347 let maintainer_scripts = vec![
348 ("test-resources/testroot/debian/config", Some("dummy content")),
349 ("test-resources/testroot/debian/preinst", Some("dummy content\n#DEBHELPER#")),
350 ("test-resources/testroot/debian/postinst", Some("dummy content\n#DEBHELPER#")),
351 ("test-resources/testroot/debian/prerm", Some("dummy content\n#DEBHELPER#")),
352 ("test-resources/testroot/debian/postrm", Some("dummy content\n#DEBHELPER#")),
353 ("test-resources/testroot/debian/templates", Some("dummy content")),
354 ];
355 generate_scripts_for_package_with_systemd_unit(None, &maintainer_scripts, "test-resources/testroot/debian/some.service");
356 }
357
358 #[test]
359 fn generate_scripts_augments_maintainer_scripts_for_unit_in_workspace_package() {
360 let maintainer_scripts = vec![
361 ("test-resources/testroot/testchild/debian/config", Some("dummy content")),
362 ("test-resources/testroot/testchild/debian/preinst", Some("dummy content\n#DEBHELPER#")),
363 ("test-resources/testroot/testchild/debian/postinst", Some("dummy content\n#DEBHELPER#")),
364 ("test-resources/testroot/testchild/debian/prerm", Some("dummy content\n#DEBHELPER#")),
365 ("test-resources/testroot/testchild/debian/postrm", Some("dummy content\n#DEBHELPER#")),
366 ("test-resources/testroot/testchild/debian/templates", Some("dummy content")),
367 ];
368 generate_scripts_for_package_with_systemd_unit(
369 Some("test_child"),
370 &maintainer_scripts,
371 "test-resources/testroot/testchild/debian/some.service",
372 );
373 }
374
375 #[test]
376 fn generate_scripts_generates_missing_maintainer_scripts_for_unit_in_root_package() {
377 let maintainer_scripts = vec![
378 ("test-resources/testroot/debian/postinst", None),
379 ("test-resources/testroot/debian/prerm", None),
380 ("test-resources/testroot/debian/postrm", None),
381 ];
382 generate_scripts_for_package_with_systemd_unit(None, &maintainer_scripts, "test-resources/testroot/debian/some.service");
383 }
384
385 #[test]
386 fn generate_scripts_generates_missing_maintainer_scripts_for_unit_in_workspace_package() {
387 let maintainer_scripts = vec![
388 ("test-resources/testroot/testchild/debian/postinst", None),
389 ("test-resources/testroot/testchild/debian/prerm", None),
390 ("test-resources/testroot/testchild/debian/postrm", None),
391 ];
392 generate_scripts_for_package_with_systemd_unit(
393 Some("test_child"),
394 &maintainer_scripts,
395 "test-resources/testroot/testchild/debian/some.service",
396 );
397 }
398
399 #[track_caller]
404 fn generate_scripts_for_package_with_systemd_unit(
405 package_name: Option<&str>,
406 maintainer_scripts: &[(&'static str, Option<&'static str>)],
407 service_file: &'static str,
408 ) {
409 let mut listener = MockListener::new();
410 let (config, mut package_deb, mut in_ar) = prepare(vec![], package_name, &mut listener);
411
412 for &(script, content) in maintainer_scripts {
415 if let Some(content) = content {
416 set_test_fs_path_content(script, content.to_string());
417 }
418 }
419
420 set_test_fs_path_content(service_file, "mock service file".to_string());
421
422 let source = AssetSource::Path(PathBuf::from(service_file));
424 let target_path = PathBuf::from(format!("usr/lib/systemd/system/{}", filename_from_path_str(service_file)));
425 package_deb.assets.resolved.push(Asset::new(source, target_path, 0o000, IsBuilt::No, crate::assets::AssetKind::Any));
426
427 package_deb.maintainer_scripts_rel_path.get_or_insert(PathBuf::from("debian"));
430
431 package_deb.systemd_units.get_or_insert(vec![SystemdUnitsConfig::default()]);
433
434 in_ar.generate_scripts(&config, &package_deb).unwrap();
436
437 let archive_bytes = in_ar.finish().unwrap();
439
440 let mut out_ar = tar::Archive::new(&archive_bytes[..]);
442
443 let mut archived_file_names = decode_names(&mut out_ar);
444 archived_file_names.sort();
445
446 let mut expected_maintainer_scripts = maintainer_scripts
447 .iter()
448 .map(|(script, _)| filename_from_path_str(script))
449 .collect::<Vec<String>>();
450 expected_maintainer_scripts.sort();
451
452 assert_eq!(expected_maintainer_scripts, archived_file_names);
453
454 let mut out_ar = tar::Archive::new(&archive_bytes[..]);
458
459 let unreplaced_placeholders = out_ar
460 .entries()
461 .unwrap()
462 .map(Result::unwrap)
463 .map(|mut entry| {
464 let mut v = String::new();
465 entry.read_to_string(&mut v).unwrap();
466 v
467 })
468 .any(|v| v.contains("#DEBHELPER#"));
469
470 assert!(!unreplaced_placeholders);
471 }
472}