1use crate::config::{BuildEnvironment, PackageConfig};
2use crate::deb::tar::Tarball;
3use crate::dh::{dh_installsystemd, dh_installsysusers, 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 dh_installsysusers::generate(
73 &package_deb.deb_name,
74 &package_deb.assets.resolved,
75 &mut scripts,
76 self.listener,
77 )?;
78
79 dh_installsystemd::generate(
83 &package_deb.deb_name,
84 &package_deb.assets.resolved,
85 systemd_units_config.unit_name.as_deref(),
86 &dh_installsystemd::Options::from(systemd_units_config),
87 &mut scripts,
88 self.listener,
89 )?;
90 }
91
92 for systemd_units_config in systemd_units_config_vec {
96 dh_lib::apply(
97 &maintainer_scripts_dir,
98 &mut scripts,
99 &package_deb.deb_name,
100 systemd_units_config.unit_name.as_deref(),
101 self.listener,
102 )?;
103 }
104 }
105
106 let mut found_any = !scripts.is_empty();
107
108 for name in ["config", "preinst", "postinst", "prerm", "postrm", "templates"] {
111 let script_path = maintainer_scripts_dir.join(name);
112 let script_path_exists = is_path_file(&script_path);
113 let (contents, source_path) = if let Some(script) = scripts.remove(name) {
114 if script_path_exists {
115 log::info!("maintainer script replaced by autogenerated systemd script {}", script_path.display());
116 }
117 (script, Some(Path::new("systemd_units")))
118 } else {
119 if !script_path_exists {
120 log::info!("maintainer script {} not found", script_path.display());
121 continue;
122 }
123 let file = read_file_to_string(&script_path)
124 .map_err(|e| CargoDebError::IoFile("Can't read script", e, script_path.clone()))?;
125 (file, Some(script_path.as_path()))
126 };
127
128 found_any = true;
129
130 let permissions = if name == "templates" { 0o644 } else { 0o755 };
135 self.add_file_with_log(name.as_ref(), contents.as_bytes(), permissions, source_path)?;
136 }
137
138 if !found_any {
139 self.listener.warning(format!("no maintainer scripts found in {}", maintainer_scripts_dir.display()));
140 }
141 Ok(())
142 }
143
144 fn add_file_with_log(&mut self, name: &Path, contents: &[u8], permissions: u32, source_path: Option<&Path>) -> CDResult<()> {
145 let source_path = source_path.and_then(|s| s.to_str()).unwrap_or("-");
146 self.listener.progress("Adding", format!("'{}' control-> {}", source_path, name.display()));
147 self.archive.file(name, contents, permissions)
148 }
149
150 fn add_control(&mut self, control: &[u8]) -> CDResult<()> {
152 self.archive.file("control", control, 0o644)?;
153 Ok(())
154 }
155
156 fn add_conf_files(&mut self, list: &str) -> CDResult<()> {
158 self.add_file_with_log("conffiles".as_ref(), list.as_bytes(), 0o644, None)
159 }
160
161 fn add_triggers_file(&mut self, config: &BuildEnvironment, rel_path: &Path) -> CDResult<()> {
162 let path = config.path_in_cargo_crate(rel_path);
163 let content = match fs::read(&path) {
164 Ok(p) => p,
165 Err(e) => return Err(CargoDebError::IoFile("Triggers file", e, path)),
166 };
167 self.add_file_with_log("triggers".as_ref(), &content, 0o644, Some(&path))
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
197 use crate::assets::{Asset, AssetSource, IsBuilt};
198 use crate::config::DebugSymbolOptions;
199 use crate::listener::MockListener;
200 use crate::parse::manifest::SystemdUnitsConfig;
201 use crate::util::tests::{add_test_fs_paths, set_test_fs_path_content};
202 use std::collections::HashMap;
203 use std::io::prelude::Read;
204 use std::path::PathBuf;
205
206 fn filename_from_path_str(path: &str) -> String {
207 let filename = Path::new(path).file_name().unwrap();
208 Path::new(".").join(filename).to_string_lossy().to_string()
209 }
210
211 fn decode_name<R>(entry: &tar::Entry<'_, R>) -> String where R: Read {
212 std::str::from_utf8(&entry.path_bytes()).unwrap().to_string()
213 }
214
215 fn decode_names<R>(ar: &mut tar::Archive<R>) -> Vec<String> where R: Read {
216 ar.entries().unwrap().map(|e| decode_name(&e.unwrap())).collect()
217 }
218
219 fn extract_contents<R>(ar: &mut tar::Archive<R>) -> HashMap<String, String> where R: Read {
220 let mut out = HashMap::new();
221 for entry in ar.entries().unwrap() {
222 let mut unwrapped = entry.unwrap();
223 let name = decode_name(&unwrapped);
224 let mut buf = Vec::new();
225 unwrapped.read_to_end(&mut buf).unwrap();
226 let content = String::from_utf8(buf).unwrap();
227 out.insert(name, content);
228 }
229 out
230 }
231
232 #[track_caller]
233 #[cfg(test)]
234 fn prepare<'l, W: Write>(dest: W, package_name: Option<&str>, mock_listener: &'l mut MockListener) -> (BuildEnvironment, PackageConfig, ControlArchiveBuilder<'l, W>) {
235 use crate::config::BuildOptions;
236
237 mock_listener.expect_progress().return_const(());
238
239 let (mut config, mut package_debs) = BuildEnvironment::from_manifest(
240 BuildOptions {
241 manifest_path: Some(Path::new("test-resources/testroot/Cargo.toml")),
242 selected_package_name: package_name,
243 debug: DebugSymbolOptions {
244 #[cfg(feature = "default_enable_dbgsym")]
245 generate_dbgsym_package: Some(false),
246 #[cfg(feature = "default_enable_separate_debug_symbols")]
247 separate_debug_symbols: Some(false),
248 ..Default::default()
249 },
250 ..Default::default()
251 },
252 mock_listener,
253 ).unwrap();
254 let package_deb = package_debs.pop().unwrap();
255
256 config.package_manifest_dir = config.package_manifest_dir.strip_prefix(env!("CARGO_MANIFEST_DIR")).unwrap().to_path_buf();
262
263 let ar = ControlArchiveBuilder::new(dest, 0, mock_listener);
264
265 (config, package_deb, ar)
266 }
267
268 #[test]
269 fn generate_scripts_does_nothing_if_maintainer_scripts_is_not_set() {
270 let mut listener = MockListener::new();
271 let (config, package_deb, mut in_ar) = prepare(vec![], None, &mut listener);
272
273 let _g = add_test_fs_paths(&["debian/postinst"]);
275
276 in_ar.generate_scripts(&config, &package_deb).unwrap();
278
279 let archive_bytes = in_ar.finish().unwrap();
281
282 let mut out_ar = tar::Archive::new(&archive_bytes[..]);
284
285 let archived_file_names = decode_names(&mut out_ar);
287 assert!(archived_file_names.is_empty());
288 }
289
290 #[test]
291 fn generate_scripts_archives_user_supplied_maintainer_scripts_in_root_package() {
292 let maintainer_script_paths = vec![
293 "test-resources/testroot/debian/config",
294 "test-resources/testroot/debian/preinst",
295 "test-resources/testroot/debian/postinst",
296 "test-resources/testroot/debian/prerm",
297 "test-resources/testroot/debian/postrm",
298 "test-resources/testroot/debian/templates",
299 ];
300 generate_scripts_for_package_without_systemd_unit(None, &maintainer_script_paths);
301 }
302
303 #[test]
304 fn generate_scripts_archives_user_supplied_maintainer_scripts_in_workspace_package() {
305 let maintainer_script_paths = vec![
306 "test-resources/testroot/testchild/debian/config",
307 "test-resources/testroot/testchild/debian/preinst",
308 "test-resources/testroot/testchild/debian/postinst",
309 "test-resources/testroot/testchild/debian/prerm",
310 "test-resources/testroot/testchild/debian/postrm",
311 "test-resources/testroot/testchild/debian/templates",
312 ];
313 generate_scripts_for_package_without_systemd_unit(Some("test_child"), &maintainer_script_paths);
314 }
315
316 #[track_caller]
317 fn generate_scripts_for_package_without_systemd_unit(package_name: Option<&str>, maintainer_script_paths: &[&'static str]) {
318 let mut listener = MockListener::new();
319 let (config, mut package_deb, mut in_ar) = prepare(vec![], package_name, &mut listener);
320
321 for script in maintainer_script_paths {
324 let content = format!("some contents: {script}");
325 set_test_fs_path_content(script, content.clone());
326 }
327
328 package_deb
330 .maintainer_scripts_rel_path
331 .get_or_insert(PathBuf::from("debian"));
332
333 in_ar.generate_scripts(&config, &package_deb).unwrap();
335
336 let archive_bytes = in_ar.finish().unwrap();
338
339 let mut out_ar = tar::Archive::new(&archive_bytes[..]);
341
342 let archived_content = extract_contents(&mut out_ar);
344
345 assert_eq!(maintainer_script_paths.len(), archived_content.len());
346
347 for script in maintainer_script_paths {
349 let expected_content = &format!("some contents: {script}");
350 let filename = filename_from_path_str(script);
351 let actual_content = archived_content.get(&filename).unwrap();
352 assert_eq!(expected_content, actual_content);
353 }
354 }
355
356 #[test]
357 fn generate_scripts_augments_maintainer_scripts_for_unit_in_root_package() {
358 let maintainer_scripts = vec![
359 ("test-resources/testroot/debian/config", Some("dummy content")),
360 ("test-resources/testroot/debian/preinst", Some("dummy content\n#DEBHELPER#")),
361 ("test-resources/testroot/debian/postinst", Some("dummy content\n#DEBHELPER#")),
362 ("test-resources/testroot/debian/prerm", Some("dummy content\n#DEBHELPER#")),
363 ("test-resources/testroot/debian/postrm", Some("dummy content\n#DEBHELPER#")),
364 ("test-resources/testroot/debian/templates", Some("dummy content")),
365 ];
366 generate_scripts_for_package_with_systemd_unit(None, &maintainer_scripts, "test-resources/testroot/debian/some.service");
367 }
368
369 #[test]
370 fn generate_scripts_augments_maintainer_scripts_for_unit_in_workspace_package() {
371 let maintainer_scripts = vec![
372 ("test-resources/testroot/testchild/debian/config", Some("dummy content")),
373 ("test-resources/testroot/testchild/debian/preinst", Some("dummy content\n#DEBHELPER#")),
374 ("test-resources/testroot/testchild/debian/postinst", Some("dummy content\n#DEBHELPER#")),
375 ("test-resources/testroot/testchild/debian/prerm", Some("dummy content\n#DEBHELPER#")),
376 ("test-resources/testroot/testchild/debian/postrm", Some("dummy content\n#DEBHELPER#")),
377 ("test-resources/testroot/testchild/debian/templates", Some("dummy content")),
378 ];
379 generate_scripts_for_package_with_systemd_unit(
380 Some("test_child"),
381 &maintainer_scripts,
382 "test-resources/testroot/testchild/debian/some.service",
383 );
384 }
385
386 #[test]
387 fn generate_scripts_generates_missing_maintainer_scripts_for_unit_in_root_package() {
388 let maintainer_scripts = vec![
389 ("test-resources/testroot/debian/postinst", None),
390 ("test-resources/testroot/debian/prerm", None),
391 ("test-resources/testroot/debian/postrm", None),
392 ];
393 generate_scripts_for_package_with_systemd_unit(None, &maintainer_scripts, "test-resources/testroot/debian/some.service");
394 }
395
396 #[test]
397 fn generate_scripts_generates_missing_maintainer_scripts_for_unit_in_workspace_package() {
398 let maintainer_scripts = vec![
399 ("test-resources/testroot/testchild/debian/postinst", None),
400 ("test-resources/testroot/testchild/debian/prerm", None),
401 ("test-resources/testroot/testchild/debian/postrm", None),
402 ];
403 generate_scripts_for_package_with_systemd_unit(
404 Some("test_child"),
405 &maintainer_scripts,
406 "test-resources/testroot/testchild/debian/some.service",
407 );
408 }
409
410 #[track_caller]
415 fn generate_scripts_for_package_with_systemd_unit(
416 package_name: Option<&str>,
417 maintainer_scripts: &[(&'static str, Option<&'static str>)],
418 service_file: &'static str,
419 ) {
420 let mut listener = MockListener::new();
421 let (config, mut package_deb, mut in_ar) = prepare(vec![], package_name, &mut listener);
422
423 for &(script, content) in maintainer_scripts {
426 if let Some(content) = content {
427 set_test_fs_path_content(script, content.to_string());
428 }
429 }
430
431 set_test_fs_path_content(service_file, "mock service file".to_string());
432
433 let source = AssetSource::Path(PathBuf::from(service_file));
435 let target_path = PathBuf::from(format!("usr/lib/systemd/system/{}", filename_from_path_str(service_file)));
436 package_deb.assets.resolved.push(Asset::new(source, target_path, Some(0o000), IsBuilt::No, crate::assets::AssetKind::Any));
437
438 package_deb.maintainer_scripts_rel_path.get_or_insert(PathBuf::from("debian"));
441
442 package_deb.systemd_units.get_or_insert(vec![SystemdUnitsConfig::default()]);
444
445 in_ar.generate_scripts(&config, &package_deb).unwrap();
447
448 let archive_bytes = in_ar.finish().unwrap();
450
451 let mut out_ar = tar::Archive::new(&archive_bytes[..]);
453
454 let mut archived_file_names = decode_names(&mut out_ar);
455 archived_file_names.sort();
456
457 let mut expected_maintainer_scripts = maintainer_scripts
458 .iter()
459 .map(|(script, _)| filename_from_path_str(script))
460 .collect::<Vec<String>>();
461 expected_maintainer_scripts.sort();
462
463 assert_eq!(expected_maintainer_scripts, archived_file_names);
464
465 let mut out_ar = tar::Archive::new(&archive_bytes[..]);
469
470 let unreplaced_placeholders = out_ar
471 .entries()
472 .unwrap()
473 .map(Result::unwrap)
474 .map(|mut entry| {
475 let mut v = String::new();
476 entry.read_to_string(&mut v).unwrap();
477 v
478 })
479 .any(|v| v.contains("#DEBHELPER#"));
480
481 assert!(!unreplaced_placeholders);
482 }
483}