debian_workbench/
debcargo.rs

1//! debcargo.toml file manipulation
2
3// TODO: Reuse the debcargo crate for more of this.
4
5use debian_control::fields::MultiArch;
6use std::collections::{HashMap, HashSet};
7use std::path::{Path, PathBuf};
8use toml_edit::{value, DocumentMut, Table};
9
10/// The default maintainer for Rust packages.
11pub const DEFAULT_MAINTAINER: &str =
12    "Debian Rust Maintainers <pkg-rust-maintainers@alioth-lists.debian.net>";
13
14/// The default section for Rust packages.
15pub const DEFAULT_SECTION: &str = "rust";
16
17/// The current standards version.
18pub const CURRENT_STANDARDS_VERSION: &str = "4.5.1";
19
20/// The default priority for Rust packages.
21pub const DEFAULT_PRIORITY: debian_control::Priority = debian_control::Priority::Optional;
22
23/// A wrapper around a debcargo.toml file.
24pub struct DebcargoEditor {
25    /// Path to the debcargo.toml file.
26    debcargo_toml_path: Option<PathBuf>,
27
28    /// The contents of the debcargo.toml file.
29    pub debcargo: DocumentMut,
30
31    /// The contents of the Cargo.toml file.
32    pub cargo: Option<DocumentMut>,
33}
34
35impl From<DocumentMut> for DebcargoEditor {
36    fn from(doc: DocumentMut) -> Self {
37        Self {
38            cargo: None,
39            debcargo_toml_path: None,
40            debcargo: doc,
41        }
42    }
43}
44
45impl Default for DebcargoEditor {
46    fn default() -> Self {
47        Self::new()
48    }
49}
50
51impl DebcargoEditor {
52    /// Create a new DebcargoEditor with no contents.
53    pub fn new() -> Self {
54        Self {
55            debcargo_toml_path: None,
56            debcargo: DocumentMut::new(),
57            cargo: None,
58        }
59    }
60
61    /// Return the name of the crate.
62    fn crate_name(&self) -> Option<&str> {
63        self.cargo
64            .as_ref()
65            .and_then(|c| c["package"]["name"].as_str())
66    }
67
68    /// Return the version of the crate.
69    fn crate_version(&self) -> Option<semver::Version> {
70        self.cargo
71            .as_ref()
72            .and_then(|c| c["package"]["version"].as_str())
73            .map(|s| semver::Version::parse(s).unwrap())
74    }
75
76    /// Open a debcargo.toml file.
77    pub fn open(path: &Path) -> Result<Self, std::io::Error> {
78        let content = std::fs::read_to_string(path)?;
79        Ok(Self {
80            debcargo_toml_path: Some(path.to_path_buf()),
81            cargo: None,
82            debcargo: content.parse().unwrap(),
83        })
84    }
85
86    /// Open a debcargo.toml file in a directory.
87    pub fn from_directory(path: &std::path::Path) -> Result<Self, std::io::Error> {
88        let debcargo_toml_path = path.join("debian/debcargo.toml");
89        let debcargo_toml = std::fs::read_to_string(&debcargo_toml_path)?;
90        let cargo_toml = std::fs::read_to_string(path.join("Cargo.toml"))?;
91        Ok(Self {
92            debcargo_toml_path: Some(debcargo_toml_path),
93            debcargo: debcargo_toml.parse().unwrap(),
94            cargo: Some(cargo_toml.parse().unwrap()),
95        })
96    }
97
98    /// Commit changes to the debcargo.toml file.
99    pub fn commit(&self) -> std::io::Result<bool> {
100        let old_contents = std::fs::read_to_string(self.debcargo_toml_path.as_ref().unwrap())?;
101        let new_contents = self.debcargo.to_string();
102        if old_contents == new_contents {
103            return Ok(false);
104        }
105        std::fs::write(
106            self.debcargo_toml_path.as_ref().unwrap(),
107            new_contents.as_bytes(),
108        )?;
109        Ok(true)
110    }
111
112    /// Return the source package
113    pub fn source(&mut self) -> DebcargoSource {
114        DebcargoSource { main: self }
115    }
116
117    fn semver_suffix(&self) -> bool {
118        self.debcargo["source"]
119            .get("semver_suffix")
120            .and_then(|v| v.as_bool())
121            .unwrap_or(false)
122    }
123
124    /// Return an iterator over the binaries in the package.
125    pub fn binaries(&mut self) -> impl Iterator<Item = DebcargoBinary<'_>> {
126        let semver_suffix = self.semver_suffix();
127
128        let mut ret: HashMap<String, String> = HashMap::new();
129        ret.insert(
130            debcargo_binary_name(
131                self.crate_name().unwrap(),
132                &if semver_suffix {
133                    semver_pair(&self.crate_version().unwrap())
134                } else {
135                    "".to_string()
136                },
137            ),
138            "lib".to_string(),
139        );
140
141        if self.debcargo["bin"].as_bool().unwrap_or(!semver_suffix) {
142            let bin_name = self.debcargo["bin_name"]
143                .as_str()
144                .unwrap_or_else(|| self.crate_name().unwrap());
145            ret.insert(bin_name.to_owned(), "bin".to_string());
146        }
147
148        let global_summary = self.global_summary();
149        let global_description = self.global_description();
150        let crate_name = self.crate_name().unwrap().to_string();
151        let crate_version = self.crate_version().unwrap();
152        let features = self.features();
153
154        self.debcargo
155            .as_table_mut()
156            .iter_mut()
157            .filter_map(move |(key, item)| {
158                let kind = ret.remove(&key.to_string())?;
159                Some(DebcargoBinary::new(
160                    kind,
161                    key.to_string(),
162                    item.as_table_mut().unwrap(),
163                    global_summary.clone(),
164                    global_description.clone(),
165                    crate_name.clone(),
166                    crate_version.clone(),
167                    semver_suffix,
168                    features.clone(),
169                ))
170            })
171    }
172
173    fn global_summary(&self) -> Option<String> {
174        if let Some(summary) = self.debcargo.get("summary").and_then(|v| v.as_str()) {
175            Some(format!("{} - Rust source code", summary))
176        } else {
177            self.cargo.as_ref().and_then(|c| {
178                c["package"]
179                    .get("description")
180                    .and_then(|v| v.as_str())
181                    .map(|s| s.split('\n').next().unwrap().to_string())
182            })
183        }
184    }
185
186    fn global_description(&self) -> Option<String> {
187        self.debcargo
188            .get("description")
189            .and_then(|v| v.as_str())
190            .map(|description| description.to_owned())
191    }
192
193    fn features(&self) -> Option<HashSet<String>> {
194        self.cargo
195            .as_ref()
196            .and_then(|c| c["features"].as_table())
197            .map(|t| t.iter().map(|(k, _)| k.to_string()).collect())
198    }
199}
200
201/// The source package in a debcargo.toml file.
202pub struct DebcargoSource<'a> {
203    main: &'a mut DebcargoEditor,
204}
205
206impl DebcargoSource<'_> {
207    /// Return the source section of the debcargo.toml file.
208    pub fn toml_section_mut(&mut self) -> &mut Table {
209        self.main.debcargo["source"].as_table_mut().unwrap()
210    }
211
212    /// Set the standards version.
213    pub fn set_standards_version(&mut self, version: &str) -> &mut Self {
214        self.toml_section_mut()["standards-version"] = value(version);
215        self
216    }
217
218    /// Return the standards version.
219    pub fn standards_version(&self) -> &str {
220        self.main
221            .debcargo
222            .get("source")
223            .and_then(|s| s.get("standards-version"))
224            .and_then(|v| v.as_str())
225            .unwrap_or(CURRENT_STANDARDS_VERSION)
226    }
227
228    /// Set the homepage.
229    pub fn set_homepage(&mut self, homepage: &str) -> &mut Self {
230        self.toml_section_mut()["homepage"] = value(homepage);
231        self
232    }
233
234    /// Return the homepage.
235    pub fn homepage(&self) -> Option<&str> {
236        let default_homepage = self
237            .main
238            .cargo
239            .as_ref()
240            .and_then(|c| c.get("package"))
241            .and_then(|x| x.get("homepage"))
242            .and_then(|v| v.as_str());
243        self.main
244            .debcargo
245            .get("source")
246            .and_then(|s| s.get("homepage"))
247            .and_then(|v| v.as_str())
248            .or(default_homepage)
249    }
250
251    /// Set the VCS Git URL.
252    pub fn set_vcs_git(&mut self, git: &str) -> &mut Self {
253        self.toml_section_mut()["vcs_git"] = value(git);
254        self
255    }
256
257    /// Return the VCS Git URL.
258    pub fn vcs_git(&self) -> Option<String> {
259        let default_git = self.main.crate_name().map(|c| {
260            format!(
261                "https://salsa.debian.org/rust-team/debcargo-conf.git [src/{}]",
262                c.to_lowercase()
263            )
264        });
265
266        self.main
267            .debcargo
268            .get("source")
269            .and_then(|s| s.get("vcs_git"))
270            .and_then(|v| v.as_str())
271            .map_or(default_git, |s| Some(s.to_string()))
272    }
273
274    /// Get the VCS browser URL.
275    pub fn vcs_browser(&self) -> Option<String> {
276        let default_vcs_browser = self.main.crate_name().map(|c| {
277            format!(
278                "https://salsa.debian.org/rust-team/debcargo-conf/tree/master/src/{}",
279                c.to_lowercase()
280            )
281        });
282
283        self.main
284            .debcargo
285            .get("source")
286            .and_then(|s| s.get("vcs_browser"))
287            .and_then(|v| v.as_str())
288            .map_or(default_vcs_browser, |s| Some(s.to_string()))
289    }
290
291    /// Set the VCS browser URL.
292    pub fn set_vcs_browser(&mut self, browser: &str) -> &mut Self {
293        self.toml_section_mut()["vcs_browser"] = value(browser);
294        self
295    }
296
297    /// Get the section.
298    pub fn section(&self) -> &str {
299        self.main
300            .debcargo
301            .get("source")
302            .and_then(|s| s.get("section"))
303            .and_then(|v| v.as_str())
304            .unwrap_or(DEFAULT_SECTION)
305    }
306
307    /// Set the section.
308    pub fn set_section(&mut self, section: &str) -> &mut Self {
309        self.toml_section_mut()["section"] = value(section);
310        self
311    }
312
313    /// Get the name of the package.
314    pub fn name(&self) -> Option<String> {
315        let crate_name = self.main.crate_name()?;
316        let semver_suffix = self.main.semver_suffix();
317        if semver_suffix {
318            let crate_version = self.main.crate_version()?;
319            Some(format!(
320                "rust-{}-{}",
321                debnormalize(crate_name),
322                semver_pair(&crate_version)
323            ))
324        } else {
325            Some(format!("rust-{}", debnormalize(crate_name)))
326        }
327    }
328
329    /// Get the priority.
330    pub fn priority(&self) -> debian_control::Priority {
331        self.main
332            .debcargo
333            .get("source")
334            .and_then(|s| s.get("priority"))
335            .and_then(|v| v.as_str())
336            .and_then(|s| s.parse().ok())
337            .unwrap_or(DEFAULT_PRIORITY)
338    }
339
340    /// Set the priority.
341    pub fn set_priority(&mut self, priority: debian_control::Priority) -> &mut Self {
342        self.toml_section_mut()["priority"] = value(priority.to_string());
343        self
344    }
345
346    /// Get whether the package build requires root.
347    pub fn rules_requires_root(&self) -> bool {
348        self.main
349            .debcargo
350            .get("source")
351            .and_then(|s| s.get("requires_root"))
352            .and_then(|v| v.as_bool())
353            .unwrap_or(false)
354    }
355
356    /// Set whether the package build requires root.
357    pub fn set_rules_requires_root(&mut self, requires_root: bool) -> &mut Self {
358        self.toml_section_mut()["requires_root"] = value(if requires_root { "yes" } else { "no" });
359        self
360    }
361
362    /// Get the maintainer.
363    pub fn maintainer(&self) -> &str {
364        self.main
365            .debcargo
366            .get("source")
367            .and_then(|s| s.get("maintainer"))
368            .and_then(|v| v.as_str())
369            .unwrap_or(DEFAULT_MAINTAINER)
370    }
371
372    /// Set the maintainer.
373    pub fn set_maintainer(&mut self, maintainer: &str) -> &mut Self {
374        self.toml_section_mut()["maintainer"] = value(maintainer);
375        self
376    }
377
378    /// Get the uploaders.
379    pub fn uploaders(&self) -> Option<Vec<String>> {
380        self.main
381            .debcargo
382            .get("source")
383            .and_then(|s| s.get("uploaders"))
384            .and_then(|x| x.as_array())
385            .map(|a| {
386                a.iter()
387                    .filter_map(|v| v.as_str())
388                    .map(|s| s.to_string())
389                    .collect()
390            })
391    }
392
393    /// Set the uploaders.
394    pub fn set_uploaders(&mut self, uploaders: Vec<String>) -> &mut Self {
395        let mut array = toml_edit::Array::new();
396        for u in uploaders {
397            array.push(u);
398        }
399        self.toml_section_mut()["uploaders"] = value(array);
400        self
401    }
402}
403
404#[allow(dead_code)]
405/// A binary package in a debcargo.toml file.
406pub struct DebcargoBinary<'a> {
407    table: &'a mut Table,
408    key: String,
409    name: String,
410    section: String,
411    global_summary: Option<String>,
412    global_description: Option<String>,
413    crate_name: String,
414    crate_version: semver::Version,
415    semver_suffix: bool,
416    features: Option<HashSet<String>>,
417}
418
419impl<'a> DebcargoBinary<'a> {
420    fn new(
421        key: String,
422        name: String,
423        table: &'a mut Table,
424        global_summary: Option<String>,
425        global_description: Option<String>,
426        crate_name: String,
427        crate_version: semver::Version,
428        semver_suffix: bool,
429        features: Option<HashSet<String>>,
430    ) -> Self {
431        Self {
432            key: key.to_owned(),
433            name,
434            section: format!("packages.{}", key),
435            table,
436            global_summary,
437            global_description,
438            crate_name,
439            crate_version,
440            semver_suffix,
441            features,
442        }
443    }
444
445    /// Get the name of the binary package.
446    pub fn name(&self) -> &str {
447        &self.name
448    }
449
450    /// Get the architecture.
451    pub fn architecture(&self) -> Option<&str> {
452        Some("any")
453    }
454
455    /// Get the multi-architecture setting.
456    pub fn multi_arch(&self) -> Option<MultiArch> {
457        Some(MultiArch::Same)
458    }
459
460    /// Get the package section.
461    pub fn section(&self) -> Option<&str> {
462        self.table["section"].as_str()
463    }
464
465    /// Get the package summary.
466    pub fn summary(&self) -> Option<&str> {
467        if let Some(summary) = self.table.get("summary").and_then(|v| v.as_str()) {
468            Some(summary)
469        } else {
470            self.global_summary.as_deref()
471        }
472    }
473
474    /// Get the package long description.
475    pub fn long_description(&self) -> Option<String> {
476        if let Some(description) = self.table.get("description").and_then(|v| v.as_str()) {
477            Some(description.to_string())
478        } else if let Some(description) = self.global_description.as_ref() {
479            Some(description.clone())
480        } else {
481            match self.key.as_str() {
482                "lib" => Some(format!("Source code for Debianized Rust crate \"{}\"", self.crate_name)),
483                "bin" => Some("This package contains the source for the Rust mio crate, packaged by debcargo for use with cargo and dh-cargo.".to_string()),
484                _ => None,
485            }
486        }
487    }
488
489    /// Return the package description.
490    pub fn description(&self) -> Option<String> {
491        Some(crate::control::format_description(
492            self.summary()?,
493            self.long_description()?.lines().collect(),
494        ))
495    }
496
497    /// Get the extra dependencies.
498    pub fn depends(&self) -> Option<&str> {
499        self.table["depends"].as_str()
500    }
501
502    /// Get the extra recommends.
503    pub fn recommends(&self) -> Option<&str> {
504        self.table["recommends"].as_str()
505    }
506
507    /// Get the extra suggests.
508    pub fn suggests(&self) -> Option<&str> {
509        self.table["suggests"].as_str()
510    }
511
512    #[allow(dead_code)]
513    fn default_provides(&self) -> Option<String> {
514        let mut ret = HashSet::new();
515        let semver_suffix = self.semver_suffix;
516        let semver = &self.crate_version;
517
518        let mut suffixes = vec![];
519        if !semver_suffix {
520            suffixes.push("".to_string());
521        }
522
523        suffixes.push(format!("-{}", semver.major));
524        suffixes.push(format!("-{}.{}", semver.major, semver.minor));
525        suffixes.push(format!(
526            "-{}.{}.{}",
527            semver.major, semver.minor, semver.patch
528        ));
529        for ver_suffix in suffixes {
530            let mut feature_suffixes = HashSet::new();
531            feature_suffixes.insert("".to_string());
532            feature_suffixes.insert("+default".to_string());
533            feature_suffixes.extend(
534                self.features
535                    .as_ref()
536                    .map(|k| k.iter().map(|k| format!("+{}", k)).collect::<HashSet<_>>())
537                    .unwrap_or_default(),
538            );
539            for feature_suffix in feature_suffixes {
540                ret.insert(debcargo_binary_name(
541                    &self.crate_name,
542                    &format!("{}{}", ver_suffix, &feature_suffix),
543                ));
544            }
545        }
546        ret.remove(self.name());
547        if ret.is_empty() {
548            None
549        } else {
550            Some(format!(
551                "\n{}",
552                &ret.iter()
553                    .map(|s| format!("{} (= ${{binary:Version}})", s))
554                    .collect::<Vec<_>>()
555                    .join(",\n ")
556            ))
557        }
558    }
559}
560
561fn debnormalize(s: &str) -> String {
562    s.to_lowercase().replace('_', "-")
563}
564
565fn semver_pair(s: &semver::Version) -> String {
566    format!("{}.{}", s.major, s.minor)
567}
568
569fn debcargo_binary_name(crate_name: &str, suffix: &str) -> String {
570    format!("librust-{}{}-dev", debnormalize(crate_name), suffix)
571}
572
573/// Unmangle a debcargo version.
574pub fn unmangle_debcargo_version(version: &str) -> String {
575    version.replace("~", "-")
576}
577
578#[cfg(test)]
579mod tests {
580    #[test]
581    fn test_debcargo_binary_name() {
582        assert_eq!(super::debcargo_binary_name("foo", ""), "librust-foo-dev");
583        assert_eq!(
584            super::debcargo_binary_name("foo", "-1"),
585            "librust-foo-1-dev"
586        );
587        assert_eq!(
588            super::debcargo_binary_name("foo", "-1.2"),
589            "librust-foo-1.2-dev"
590        );
591        assert_eq!(
592            super::debcargo_binary_name("foo", "-1.2.3"),
593            "librust-foo-1.2.3-dev"
594        );
595    }
596
597    #[test]
598    fn test_semver_pair() {
599        assert_eq!(super::semver_pair(&"1.2.3".parse().unwrap()), "1.2");
600        assert_eq!(super::semver_pair(&"1.2.6".parse().unwrap()), "1.2");
601    }
602
603    #[test]
604    fn test_debnormalize() {
605        assert_eq!(super::debnormalize("foo_bar"), "foo-bar");
606        assert_eq!(super::debnormalize("foo"), "foo");
607    }
608
609    #[test]
610    fn test_debcargo_editor() {
611        let mut editor = super::DebcargoEditor::new();
612        editor.debcargo["source"]["standards-version"] = toml_edit::value("4.5.1");
613        editor.debcargo["source"]["homepage"] = toml_edit::value("https://example.com");
614        editor.debcargo["source"]["vcs_git"] = toml_edit::value("https://example.com");
615        editor.debcargo["source"]["vcs_browser"] = toml_edit::value("https://example.com");
616        editor.debcargo["source"]["section"] = toml_edit::value("notrust");
617        editor.debcargo["source"]["priority"] = toml_edit::value("optional");
618        editor.debcargo["source"]["requires_root"] = toml_edit::value("no");
619        editor.debcargo["source"]["maintainer"] =
620            toml_edit::value("Jelmer Vernooij <jelmer@debian.org>");
621
622        assert_eq!(editor.source().standards_version(), "4.5.1");
623        assert_eq!(
624            editor.source().vcs_git().as_deref(),
625            Some("https://example.com")
626        );
627        assert_eq!(
628            editor.source().vcs_browser().as_deref(),
629            Some("https://example.com")
630        );
631        assert_eq!(editor.source().section(), "notrust");
632        assert_eq!(editor.source().priority(), super::DEFAULT_PRIORITY);
633        assert!(!editor.source().rules_requires_root());
634        assert_eq!(
635            editor.source().maintainer(),
636            "Jelmer Vernooij <jelmer@debian.org>"
637        );
638        assert_eq!(editor.source().name(), None);
639        assert_eq!(editor.source().uploaders(), None);
640        assert_eq!(editor.source().homepage(), Some("https://example.com"));
641    }
642}