cargo_deb/deb/
control.rs

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    /// 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                // Select and populate autoscript templates relevant to the unit
72                // file(s) in this package and the configuration settings chosen.
73                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                // Get Option<&str> from Option<String>
81                let unit_name = systemd_units_config.unit_name.as_deref();
82
83                // Replace the #DEBHELPER# token in the users maintainer scripts
84                // and/or generate maintainer scripts from scratch as needed.
85                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        // Add maintainer scripts to the archive, either those supplied by the
98        // user or if available prefer modified versions generated above.
99        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            // The config, postinst, postrm, preinst, and prerm
120            // control files should use mode 0755; all other control files should use 0644.
121            // See Debian Policy Manual section 10.9
122            // and lintian tag control-file-has-bad-permissions
123            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    // Add the control file to the tar archive.
140    fn add_control(&mut self, control: &[u8]) -> CDResult<()> {
141        self.archive.file("control", control, 0o644)?;
142        Ok(())
143    }
144
145    /// If configuration files are required, the conffiles file will be created.
146    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    // The following test suite verifies that `fn generate_scripts()` correctly
163    // copies "maintainer scripts" (files with the name config, preinst, postinst,
164    // prerm, postrm, and/or templates) from the `maintainer_scripts` directory
165    // into the generated archive, and in the case that a systemd config is
166    // provided, that a service file when present causes #DEBHELPER# placeholders
167    // in the maintainer scripts to be replaced and missing maintainer scripts to
168    // be generated.
169    //
170    // The exact details of maintainer script replacement is tested
171    // in `dh_installsystemd.rs`, here we are more interested in testing that
172    // `fn generate_scripts()` correctly looks for maintainer script and unit
173    // script files relative to the crate root, whether processing the root crate
174    // or a workspace member crate.
175    //
176    // This test depends on the existence of two test crates organized such that
177    // one is a Cargo workspace member and the other is a root crate.
178    //
179    //   test-resources/
180    //     testroot/         <-- root crate
181    //       Cargo.toml
182    //       testchild/      <-- workspace member crate
183    //         Cargo.toml
184
185    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        // make the absolute manifest dir relative to our crate root dir
246        // as the static paths we receive from the caller cannot be set
247        // to the absolute path we find ourselves in at test run time, but
248        // instead have to match exactly the paths looked up based on the
249        // value of the manifest dir.
250        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        // supply a maintainer script as if it were available on disk
263        let _g = add_test_fs_paths(&["debian/postinst"]);
264
265        // generate scripts and store them in the given archive
266        in_ar.generate_scripts(&config, &package_deb).unwrap();
267
268        // finish the archive and unwrap it as a byte vector
269        let archive_bytes = in_ar.finish().unwrap();
270
271        // parse the archive bytes
272        let mut out_ar = tar::Archive::new(&archive_bytes[..]);
273
274        // compare the file names in the archive to what we expect
275        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        // supply a maintainer script as if it were available on disk
311        // provide file content that we can easily verify
312        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        // specify a path relative to the (root or workspace child) package
318        package_deb
319            .maintainer_scripts_rel_path
320            .get_or_insert(PathBuf::from("debian"));
321
322        // generate scripts and store them in the given archive
323        in_ar.generate_scripts(&config, &package_deb).unwrap();
324
325        // finish the archive and unwrap it as a byte vector
326        let archive_bytes = in_ar.finish().unwrap();
327
328        // parse the archive bytes
329        let mut out_ar = tar::Archive::new(&archive_bytes[..]);
330
331        // compare the file contents in the archive to what we expect
332        let archived_content = extract_contents(&mut out_ar);
333
334        assert_eq!(maintainer_script_paths.len(), archived_content.len());
335
336        // verify that the content we supplied was faithfully archived
337        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    // `maintainer_scripts` is a collection of file system paths for which:
400    //   - each file should be in the same directory
401    //   - the generated archive should contain a file with each of the given filenames
402    //   - if Some(...) then pretend when creating the archive that a file at that path exists with the given content
403    #[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        // supply a maintainer script as if it were available on disk
413        // provide file content that we can easily verify
414        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        // make the unit file available for systemd unit processing
423        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        // look in the current dir for maintainer scripts (none, but the systemd
428        // unit processing will be skipped if we don't set this)
429        package_deb.maintainer_scripts_rel_path.get_or_insert(PathBuf::from("debian"));
430
431        // enable systemd unit processing
432        package_deb.systemd_units.get_or_insert(vec![SystemdUnitsConfig::default()]);
433
434        // generate scripts and store them in the given archive
435        in_ar.generate_scripts(&config, &package_deb).unwrap();
436
437        // finish the archive and unwrap it as a byte vector
438        let archive_bytes = in_ar.finish().unwrap();
439
440        // check that the expected files were included in the archive
441        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        // check the content of the archived files for any unreplaced placeholders.
455        // create a new tar wrapper around the bytes as you cannot seek the same
456        // Archive more than once.
457        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}