Skip to main content

cargo_deb/deb/
control.rs

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    /// Generates an uncompressed tar archive with `control`, and others
26    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    /// Append Debian maintainer script files (control, preinst, postinst, prerm,
45    /// postrm and templates) present in the `maintainer_scripts` path to the
46    /// archive, if `maintainer_scripts` is configured.
47    ///
48    /// Additionally, when `systemd_units` is configured, shell script fragments
49    /// "for enabling, disabling, starting, stopping and restarting systemd unit
50    /// files" (quoting `man 1 dh_installsystemd`) will replace the `#DEBHELPER#`
51    /// token in the provided maintainer scripts.
52    ///
53    /// If a shell fragment cannot be inserted because the target script is missing
54    /// then the entire script will be generated and appended to the archive.
55    ///
56    /// # Requirements
57    ///
58    /// When `systemd_units` is configured, user supplied `maintainer_scripts` must
59    /// contain a `#DEBHELPER#` token at the point where shell script fragments
60    /// should be inserted.
61    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                // Ordering dependency: sysusers.d hook must run before tmpfiles.d
72                dh_installsysusers::generate(
73                    &package_deb.deb_name,
74                    &package_deb.assets.resolved,
75                    &mut scripts,
76                    self.listener,
77                )?;
78
79                // Select and populate autoscript templates relevant to the unit
80                // file(s) belonging to this config entry (or all unit files in this package when no unit name is set)
81                // and the configuration settings chosen, accumulating the fragments across entries.
82                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            // Replace the #DEBHELPER# token in the users maintainer scripts
93            // and/or generate maintainer scripts from scratch as needed,
94            // now that the fragments of every systemd-units entry are known.
95            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        // Add maintainer scripts to the archive, either those supplied by the
109        // user or if available prefer modified versions generated above.
110        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            // The config, postinst, postrm, preinst, and prerm
131            // control files should use mode 0755; all other control files should use 0644.
132            // See Debian Policy Manual section 10.9
133            // and lintian tag control-file-has-bad-permissions
134            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    // Add the control file to the tar archive.
151    fn add_control(&mut self, control: &[u8]) -> CDResult<()> {
152        self.archive.file("control", control, 0o644)?;
153        Ok(())
154    }
155
156    /// If configuration files are required, the conffiles file will be created.
157    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    // The following test suite verifies that `fn generate_scripts()` correctly
174    // copies "maintainer scripts" (files with the name config, preinst, postinst,
175    // prerm, postrm, and/or templates) from the `maintainer_scripts` directory
176    // into the generated archive, and in the case that a systemd config is
177    // provided, that a service file when present causes #DEBHELPER# placeholders
178    // in the maintainer scripts to be replaced and missing maintainer scripts to
179    // be generated.
180    //
181    // The exact details of maintainer script replacement is tested
182    // in `dh_installsystemd.rs`, here we are more interested in testing that
183    // `fn generate_scripts()` correctly looks for maintainer script and unit
184    // script files relative to the crate root, whether processing the root crate
185    // or a workspace member crate.
186    //
187    // This test depends on the existence of two test crates organized such that
188    // one is a Cargo workspace member and the other is a root crate.
189    //
190    //   test-resources/
191    //     testroot/         <-- root crate
192    //       Cargo.toml
193    //       testchild/      <-- workspace member crate
194    //         Cargo.toml
195
196    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        // make the absolute manifest dir relative to our crate root dir
257        // as the static paths we receive from the caller cannot be set
258        // to the absolute path we find ourselves in at test run time, but
259        // instead have to match exactly the paths looked up based on the
260        // value of the manifest dir.
261        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        // supply a maintainer script as if it were available on disk
274        let _g = add_test_fs_paths(&["debian/postinst"]);
275
276        // generate scripts and store them in the given archive
277        in_ar.generate_scripts(&config, &package_deb).unwrap();
278
279        // finish the archive and unwrap it as a byte vector
280        let archive_bytes = in_ar.finish().unwrap();
281
282        // parse the archive bytes
283        let mut out_ar = tar::Archive::new(&archive_bytes[..]);
284
285        // compare the file names in the archive to what we expect
286        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        // supply a maintainer script as if it were available on disk
322        // provide file content that we can easily verify
323        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        // specify a path relative to the (root or workspace child) package
329        package_deb
330            .maintainer_scripts_rel_path
331            .get_or_insert(PathBuf::from("debian"));
332
333        // generate scripts and store them in the given archive
334        in_ar.generate_scripts(&config, &package_deb).unwrap();
335
336        // finish the archive and unwrap it as a byte vector
337        let archive_bytes = in_ar.finish().unwrap();
338
339        // parse the archive bytes
340        let mut out_ar = tar::Archive::new(&archive_bytes[..]);
341
342        // compare the file contents in the archive to what we expect
343        let archived_content = extract_contents(&mut out_ar);
344
345        assert_eq!(maintainer_script_paths.len(), archived_content.len());
346
347        // verify that the content we supplied was faithfully archived
348        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    // `maintainer_scripts` is a collection of file system paths for which:
411    //   - each file should be in the same directory
412    //   - the generated archive should contain a file with each of the given filenames
413    //   - if Some(...) then pretend when creating the archive that a file at that path exists with the given content
414    #[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        // supply a maintainer script as if it were available on disk
424        // provide file content that we can easily verify
425        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        // make the unit file available for systemd unit processing
434        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        // look in the current dir for maintainer scripts (none, but the systemd
439        // unit processing will be skipped if we don't set this)
440        package_deb.maintainer_scripts_rel_path.get_or_insert(PathBuf::from("debian"));
441
442        // enable systemd unit processing
443        package_deb.systemd_units.get_or_insert(vec![SystemdUnitsConfig::default()]);
444
445        // generate scripts and store them in the given archive
446        in_ar.generate_scripts(&config, &package_deb).unwrap();
447
448        // finish the archive and unwrap it as a byte vector
449        let archive_bytes = in_ar.finish().unwrap();
450
451        // check that the expected files were included in the archive
452        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        // check the content of the archived files for any unreplaced placeholders.
466        // create a new tar wrapper around the bytes as you cannot seek the same
467        // Archive more than once.
468        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}