Skip to main content

debian_workbench/
abstract_control.rs

1//! Abstract interface for editing debian packages, whether backed by real control files or
2//! debcargo files.
3use crate::relations::ensure_relation;
4use debian_control::lossless::relations::{Entry, Relations};
5use std::path::Path;
6
7/// Interface for editing debian packages, whether backed by real control files or debcargo files.
8pub trait AbstractControlEditor {
9    /// Get the source package.
10    fn source<'a>(&'a mut self) -> Option<Box<dyn AbstractSource<'a> + 'a>>;
11
12    /// Get the binary packages.
13    fn binaries<'a>(&'a mut self) -> Vec<Box<dyn AbstractBinary + 'a>>;
14
15    /// Commit the changes.
16    fn commit(&mut self) -> bool;
17
18    /// Wrap and sort the control file.
19    fn wrap_and_sort(&mut self);
20}
21
22/// An abstract source package.
23pub trait AbstractSource<'a> {
24    /// Get the name of the source package.
25    fn name(&self) -> Option<String>;
26
27    /// Ensure that a build dependency is present.
28    fn ensure_build_dep(&mut self, dep: Entry);
29
30    /// Set the maintainer of the source package.
31    fn set_maintainer(&mut self, maintainer: &str);
32
33    /// Set the uploaders of the source package.
34    fn set_uploaders(&mut self, uploaders: &[&str]);
35
36    /// Set the VCS URL for the source package.
37    fn set_vcs_url(&mut self, vcs_type: &str, url: &str);
38
39    /// Get the VCS URL for the source package.
40    fn get_vcs_url(&self, vcs_type: &str) -> Option<String>;
41}
42
43/// An abstract binary package.
44pub trait AbstractBinary {
45    /// Get the name of the binary package.
46    fn name(&self) -> Option<String>;
47}
48
49use crate::debcargo::{DebcargoBinary, DebcargoEditor, DebcargoSource};
50use debian_control::{Binary as PlainBinary, Control as PlainControl, Source as PlainSource};
51
52impl AbstractControlEditor for DebcargoEditor {
53    fn source<'a>(&'a mut self) -> Option<Box<dyn AbstractSource<'a> + 'a>> {
54        Some(Box::new(DebcargoEditor::source(self)) as Box<dyn AbstractSource<'a>>)
55    }
56
57    fn binaries<'a>(&'a mut self) -> Vec<Box<dyn AbstractBinary + 'a>> {
58        DebcargoEditor::binaries(self)
59            .map(|b| Box::new(b) as Box<dyn AbstractBinary>)
60            .collect()
61    }
62
63    fn commit(&mut self) -> bool {
64        DebcargoEditor::commit(self).unwrap()
65    }
66
67    fn wrap_and_sort(&mut self) {}
68}
69
70impl AbstractBinary for PlainBinary {
71    fn name(&self) -> Option<String> {
72        self.name()
73    }
74}
75
76impl AbstractSource<'_> for PlainSource {
77    fn name(&self) -> Option<String> {
78        self.name()
79    }
80
81    fn ensure_build_dep(&mut self, dep: Entry) {
82        if let Some(mut build_deps) = self.build_depends() {
83            ensure_relation(&mut build_deps, dep);
84            self.set_build_depends(&build_deps);
85        } else {
86            self.set_build_depends(&Relations::from(vec![dep]));
87        }
88    }
89
90    fn set_maintainer(&mut self, maintainer: &str) {
91        (self as &mut debian_control::lossless::Source).set_maintainer(maintainer);
92    }
93
94    fn set_uploaders(&mut self, uploaders: &[&str]) {
95        (self as &mut debian_control::lossless::Source).set_uploaders(uploaders);
96    }
97
98    fn set_vcs_url(&mut self, vcs_type: &str, url: &str) {
99        let field_name = format!("Vcs-{}", vcs_type);
100        self.as_mut_deb822().set(&field_name, url);
101    }
102
103    fn get_vcs_url(&self, vcs_type: &str) -> Option<String> {
104        let field_name = format!("Vcs-{}", vcs_type);
105        self.as_deb822().get(&field_name)
106    }
107}
108
109impl AbstractBinary for DebcargoBinary<'_> {
110    fn name(&self) -> Option<String> {
111        Some(self.name().to_string())
112    }
113}
114
115impl<'a> AbstractSource<'a> for DebcargoSource<'a> {
116    fn name(&self) -> Option<String> {
117        self.name()
118    }
119
120    fn ensure_build_dep(&mut self, dep: Entry) {
121        // TODO: Check that it's not already there
122        if let Some(build_deps) = self
123            .toml_section_mut()
124            .get_mut("build_depends")
125            .and_then(|v| v.as_array_mut())
126        {
127            build_deps.push(dep.to_string());
128        }
129    }
130
131    fn set_maintainer(&mut self, maintainer: &str) {
132        (self as &mut crate::debcargo::DebcargoSource).set_maintainer(maintainer);
133    }
134
135    fn set_uploaders(&mut self, uploaders: &[&str]) {
136        (self as &mut crate::debcargo::DebcargoSource)
137            .set_uploaders(uploaders.iter().map(|s| s.to_string()).collect::<Vec<_>>());
138    }
139
140    fn set_vcs_url(&mut self, vcs_type: &str, url: &str) {
141        (self as &mut crate::debcargo::DebcargoSource).set_vcs_url(vcs_type, url);
142    }
143
144    fn get_vcs_url(&self, vcs_type: &str) -> Option<String> {
145        match vcs_type.to_lowercase().as_str() {
146            "git" => self.vcs_git(),
147            "browser" => self.vcs_browser(),
148            _ => self.get_extra_field(&format!("Vcs-{}", vcs_type)),
149        }
150    }
151}
152
153impl<E: crate::editor::Editor<PlainControl>> AbstractControlEditor for E {
154    fn source<'a>(&'a mut self) -> Option<Box<dyn AbstractSource<'a> + 'a>> {
155        PlainControl::source(self).map(|s| Box::new(s) as Box<dyn AbstractSource>)
156    }
157
158    fn binaries<'a>(&'a mut self) -> Vec<Box<dyn AbstractBinary + 'a>> {
159        PlainControl::binaries(self)
160            .map(|b| Box::new(b) as Box<dyn AbstractBinary>)
161            .collect()
162    }
163
164    fn commit(&mut self) -> bool {
165        !(self as &mut dyn crate::editor::Editor<PlainControl>)
166            .commit()
167            .unwrap()
168            .is_empty()
169    }
170
171    fn wrap_and_sort(&mut self) {
172        (self as &mut dyn crate::editor::Editor<PlainControl>).wrap_and_sort(
173            deb822_lossless::Indentation::Spaces(4),
174            false,
175            None,
176        )
177    }
178}
179
180/// Open a control file for editing.
181pub fn edit_control<'a>(
182    tree: &dyn breezyshim::workingtree::WorkingTree,
183    subpath: &Path,
184) -> Result<Box<dyn AbstractControlEditor + 'a>, crate::editor::EditorError<deb822_lossless::Error>>
185{
186    if tree.has_filename(&subpath.join("debian/debcargo.toml")) {
187        Ok(Box::new(crate::debcargo::DebcargoEditor::from_directory(
188            &tree.abspath(subpath).unwrap(),
189        )?))
190    } else {
191        let control_path = tree.abspath(&subpath.join(std::path::Path::new("debian/control")));
192        Ok(Box::new(crate::control::TemplatedControlEditor::open(
193            control_path.unwrap(),
194        )?) as Box<dyn AbstractControlEditor>)
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use breezyshim::controldir::{create_standalone_workingtree, ControlDirFormat};
201    use breezyshim::prelude::*;
202    use std::path::Path;
203    use std::str::FromStr;
204
205    #[test]
206    fn test_edit_control_debcargo() {
207        let td = tempfile::tempdir().unwrap();
208        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
209        // Write dummy debcargo.toml
210        tree.mkdir(Path::new("debian")).unwrap();
211        std::fs::write(
212            td.path().join("debian/debcargo.toml"),
213            br#"
214maintainer = "Alice <alice@example.com>"
215homepage = "https://example.com"
216description = "Example package"
217"#,
218        )
219        .unwrap();
220
221        std::fs::write(
222            td.path().join("Cargo.toml"),
223            br#"
224[package]
225name = "example"
226version = "0.1.0"
227edition = "2018"
228"#,
229        )
230        .unwrap();
231
232        tree.add(&[(Path::new("debian")), (Path::new("debian/debcargo.toml"))])
233            .unwrap();
234
235        let mut editor = super::edit_control(&tree, Path::new("")).unwrap();
236
237        editor.commit();
238    }
239
240    #[test]
241    fn test_edit_control_regular() {
242        let td = tempfile::tempdir().unwrap();
243        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
244        // Write dummy debian/control
245        tree.mkdir(Path::new("debian")).unwrap();
246        tree.put_file_bytes_non_atomic(
247            Path::new("debian/control"),
248            br#"
249Source: example
250Maintainer: Alice <alice@example.com>
251Homepage: https://example.com
252
253Package: example
254Architecture: any
255Description: Example package
256"#,
257        )
258        .unwrap();
259
260        tree.add(&[(Path::new("debian")), (Path::new("debian/control"))])
261            .unwrap();
262
263        let mut editor = super::edit_control(&tree, Path::new("")).unwrap();
264
265        editor.commit();
266    }
267
268    #[test]
269    fn test_edit_source_ensure_build_depends() {
270        let td = tempfile::tempdir().unwrap();
271        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
272        // Write dummy debian/control
273        tree.mkdir(Path::new("debian")).unwrap();
274        tree.put_file_bytes_non_atomic(
275            Path::new("debian/control"),
276            br#"
277Source: example
278Maintainer: Alice <alice@example.com>
279Build-Depends: libc6
280
281Package: example
282Architecture: any
283Description: Example package
284"#,
285        )
286        .unwrap();
287        tree.add(&[Path::new("debian/control")]).unwrap();
288
289        let mut editor = super::edit_control(&tree, Path::new("")).unwrap();
290        let mut source = editor.source().unwrap();
291        source.ensure_build_dep(
292            debian_control::lossless::relations::Entry::from_str("libssl-dev").unwrap(),
293        );
294        std::mem::drop(source);
295        editor.commit();
296
297        let text = tree.get_file_text(Path::new("debian/control")).unwrap();
298        assert_eq!(
299            std::str::from_utf8(&text).unwrap(),
300            r#"
301Source: example
302Maintainer: Alice <alice@example.com>
303Build-Depends: libc6, libssl-dev
304
305Package: example
306Architecture: any
307Description: Example package
308"#
309        );
310    }
311
312    #[test]
313    fn test_abstract_source_set_vcs_url_plain() {
314        let td = tempfile::tempdir().unwrap();
315        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
316        // Write dummy debian/control
317        tree.mkdir(Path::new("debian")).unwrap();
318        tree.put_file_bytes_non_atomic(
319            Path::new("debian/control"),
320            br#"Source: example
321Maintainer: Alice <alice@example.com>
322
323Package: example
324Architecture: any
325Description: Example package
326"#,
327        )
328        .unwrap();
329        tree.add(&[Path::new("debian/control")]).unwrap();
330
331        let mut editor = super::edit_control(&tree, Path::new("")).unwrap();
332        let mut source = editor.source().unwrap();
333
334        // Test setting various VCS URLs
335        source.set_vcs_url("Git", "https://github.com/example/repo.git");
336        source.set_vcs_url("Browser", "https://github.com/example/repo");
337
338        std::mem::drop(source);
339        editor.commit();
340
341        let text = tree.get_file_text(Path::new("debian/control")).unwrap();
342        assert_eq!(
343            std::str::from_utf8(&text).unwrap(),
344            r#"Source: example
345Maintainer: Alice <alice@example.com>
346Vcs-Git: https://github.com/example/repo.git
347Vcs-Browser: https://github.com/example/repo
348
349Package: example
350Architecture: any
351Description: Example package
352"#
353        );
354    }
355
356    #[test]
357    fn test_abstract_source_set_vcs_url_debcargo() {
358        let td = tempfile::tempdir().unwrap();
359        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
360        // Write dummy debcargo.toml
361        tree.mkdir(Path::new("debian")).unwrap();
362        std::fs::write(
363            td.path().join("debian/debcargo.toml"),
364            br#"maintainer = "Alice <alice@example.com>"
365"#,
366        )
367        .unwrap();
368
369        std::fs::write(
370            td.path().join("Cargo.toml"),
371            br#"[package]
372name = "example"
373version = "0.1.0"
374"#,
375        )
376        .unwrap();
377
378        tree.add(&[(Path::new("debian")), (Path::new("debian/debcargo.toml"))])
379            .unwrap();
380
381        let mut editor = super::edit_control(&tree, Path::new("")).unwrap();
382        let mut source = editor.source().unwrap();
383
384        // Test setting native VCS URLs
385        source.set_vcs_url("Git", "https://github.com/example/repo.git");
386        source.set_vcs_url("Browser", "https://github.com/example/repo");
387
388        // Test setting non-native VCS URL
389        source.set_vcs_url("Svn", "https://svn.example.com/repo");
390
391        std::mem::drop(source);
392        editor.commit();
393
394        // Read back the debcargo.toml to verify
395        let content = std::fs::read_to_string(td.path().join("debian/debcargo.toml")).unwrap();
396        assert_eq!(
397            content,
398            r#"maintainer = "Alice <alice@example.com>"
399
400[source]
401vcs_git = "https://github.com/example/repo.git"
402vcs_browser = "https://github.com/example/repo"
403extra_lines = ["Vcs-Svn: https://svn.example.com/repo"]
404"#
405        );
406    }
407
408    #[test]
409    fn test_abstract_source_get_vcs_url_plain() {
410        let td = tempfile::tempdir().unwrap();
411        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
412        // Write dummy debian/control with VCS fields
413        tree.mkdir(Path::new("debian")).unwrap();
414        tree.put_file_bytes_non_atomic(
415            Path::new("debian/control"),
416            br#"Source: example
417Maintainer: Alice <alice@example.com>
418Vcs-Git: https://github.com/example/repo.git
419Vcs-Browser: https://github.com/example/repo
420Vcs-Svn: https://svn.example.com/repo
421
422Package: example
423Architecture: any
424Description: Example package
425"#,
426        )
427        .unwrap();
428        tree.add(&[Path::new("debian/control")]).unwrap();
429
430        let mut editor = super::edit_control(&tree, Path::new("")).unwrap();
431        let source = editor.source().unwrap();
432
433        // Test getting various VCS URLs
434        assert_eq!(
435            source.get_vcs_url("Git"),
436            Some("https://github.com/example/repo.git".to_string())
437        );
438        assert_eq!(
439            source.get_vcs_url("Browser"),
440            Some("https://github.com/example/repo".to_string())
441        );
442        assert_eq!(
443            source.get_vcs_url("Svn"),
444            Some("https://svn.example.com/repo".to_string())
445        );
446        assert_eq!(source.get_vcs_url("Bzr"), None);
447    }
448
449    #[test]
450    fn test_abstract_source_get_vcs_url_debcargo() {
451        let td = tempfile::tempdir().unwrap();
452        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
453        // Write dummy debcargo.toml with VCS fields
454        tree.mkdir(Path::new("debian")).unwrap();
455        std::fs::write(
456            td.path().join("debian/debcargo.toml"),
457            br#"maintainer = "Alice <alice@example.com>"
458
459[source]
460vcs_git = "https://github.com/example/repo.git"
461vcs_browser = "https://github.com/example/repo"
462extra_lines = ["Vcs-Svn: https://svn.example.com/repo", "Vcs-Bzr: https://bzr.example.com/repo"]
463"#,
464        )
465        .unwrap();
466
467        std::fs::write(
468            td.path().join("Cargo.toml"),
469            br#"[package]
470name = "example"
471version = "0.1.0"
472"#,
473        )
474        .unwrap();
475
476        tree.add(&[(Path::new("debian")), (Path::new("debian/debcargo.toml"))])
477            .unwrap();
478
479        let mut editor = super::edit_control(&tree, Path::new("")).unwrap();
480        let source = editor.source().unwrap();
481
482        // Test getting native VCS URLs
483        assert_eq!(
484            source.get_vcs_url("Git"),
485            Some("https://github.com/example/repo.git".to_string())
486        );
487        assert_eq!(
488            source.get_vcs_url("Browser"),
489            Some("https://github.com/example/repo".to_string())
490        );
491
492        // Test getting non-native VCS URLs from extra_lines
493        assert_eq!(
494            source.get_vcs_url("Svn"),
495            Some("https://svn.example.com/repo".to_string())
496        );
497        assert_eq!(
498            source.get_vcs_url("Bzr"),
499            Some("https://bzr.example.com/repo".to_string())
500        );
501
502        // Test getting non-existent VCS URL
503        assert_eq!(source.get_vcs_url("Hg"), None);
504    }
505}