debian_analyzer/
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<'a> DebcargoSource<'a> {
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 semver_suffix = self.main.semver_suffix();
316        if semver_suffix {
317            let crate_name = self.main.crate_name().map(debnormalize);
318            Some(format!(
319                "rust-{}-{}",
320                crate_name?,
321                semver_pair(&self.main.crate_version()?)
322            ))
323        } else {
324            Some(format!("rust-{}", debnormalize(self.main.crate_name()?)))
325        }
326    }
327
328    /// Get the priority.
329    pub fn priority(&self) -> debian_control::Priority {
330        self.main
331            .debcargo
332            .get("source")
333            .and_then(|s| s.get("priority"))
334            .and_then(|v| v.as_str())
335            .and_then(|s| s.parse().ok())
336            .unwrap_or(DEFAULT_PRIORITY)
337    }
338
339    /// Set the priority.
340    pub fn set_priority(&mut self, priority: debian_control::Priority) -> &mut Self {
341        self.toml_section_mut()["priority"] = value(priority.to_string());
342        self
343    }
344
345    /// Get whether the package build requires root.
346    pub fn rules_requires_root(&self) -> bool {
347        self.main
348            .debcargo
349            .get("source")
350            .and_then(|s| s.get("requires_root"))
351            .and_then(|v| v.as_bool())
352            .unwrap_or(false)
353    }
354
355    /// Set whether the package build requires root.
356    pub fn set_rules_requires_root(&mut self, requires_root: bool) -> &mut Self {
357        self.toml_section_mut()["requires_root"] = value(if requires_root { "yes" } else { "no" });
358        self
359    }
360
361    /// Get the maintainer.
362    pub fn maintainer(&self) -> &str {
363        self.main
364            .debcargo
365            .get("source")
366            .and_then(|s| s.get("maintainer"))
367            .and_then(|v| v.as_str())
368            .unwrap_or(DEFAULT_MAINTAINER)
369    }
370
371    /// Set the maintainer.
372    pub fn set_maintainer(&mut self, maintainer: &str) -> &mut Self {
373        self.toml_section_mut()["maintainer"] = value(maintainer);
374        self
375    }
376
377    /// Get the uploaders.
378    pub fn uploaders(&self) -> Option<Vec<String>> {
379        self.main
380            .debcargo
381            .get("source")
382            .and_then(|s| s.get("uploaders"))
383            .and_then(|x| x.as_array())
384            .map(|a| a.iter().map(|v| v.as_str().unwrap().to_string()).collect())
385    }
386
387    /// Set the uploaders.
388    pub fn set_uploaders(&mut self, uploaders: Vec<String>) -> &mut Self {
389        let mut array = toml_edit::Array::new();
390        for u in uploaders {
391            array.push(u);
392        }
393        self.toml_section_mut()["uploaders"] = value(array);
394        self
395    }
396}
397
398#[allow(dead_code)]
399/// A binary package in a debcargo.toml file.
400pub struct DebcargoBinary<'a> {
401    table: &'a mut Table,
402    key: String,
403    name: String,
404    section: String,
405    global_summary: Option<String>,
406    global_description: Option<String>,
407    crate_name: String,
408    crate_version: semver::Version,
409    semver_suffix: bool,
410    features: Option<HashSet<String>>,
411}
412
413impl<'a> DebcargoBinary<'a> {
414    fn new(
415        key: String,
416        name: String,
417        table: &'a mut Table,
418        global_summary: Option<String>,
419        global_description: Option<String>,
420        crate_name: String,
421        crate_version: semver::Version,
422        semver_suffix: bool,
423        features: Option<HashSet<String>>,
424    ) -> Self {
425        Self {
426            key: key.to_owned(),
427            name,
428            section: format!("packages.{}", key),
429            table,
430            global_summary,
431            global_description,
432            crate_name,
433            crate_version,
434            semver_suffix,
435            features,
436        }
437    }
438
439    /// Get the name of the binary package.
440    pub fn name(&self) -> &str {
441        &self.name
442    }
443
444    /// Get the architecture.
445    pub fn architecture(&self) -> Option<&str> {
446        Some("any")
447    }
448
449    /// Get the multi-architecture setting.
450    pub fn multi_arch(&self) -> Option<MultiArch> {
451        Some(MultiArch::Same)
452    }
453
454    /// Get the package section.
455    pub fn section(&self) -> Option<&str> {
456        self.table["section"].as_str()
457    }
458
459    /// Get the package summary.
460    pub fn summary(&self) -> Option<String> {
461        if let Some(summary) = self.table.get("summary").and_then(|v| v.as_str()) {
462            Some(summary.to_string())
463        } else {
464            self.global_summary.clone()
465        }
466    }
467
468    /// Get the package long description.
469    pub fn long_description(&self) -> Option<String> {
470        if let Some(description) = self.table.get("description").and_then(|v| v.as_str()) {
471            Some(description.to_string())
472        } else if let Some(description) = self.global_description.as_ref() {
473            Some(description.to_string())
474        } else {
475            match self.key.as_str() {
476                "lib" => Some(format!("Source code for Debianized Rust crate \"{}\"", self.crate_name)),
477                "bin" => Some("This package contains the source for the Rust mio crate, packaged by debcargo for use with cargo and dh-cargo.".to_owned()),
478                _ => None,
479            }
480        }
481    }
482
483    /// Return the package description.
484    pub fn description(&self) -> Option<String> {
485        Some(crate::control::format_description(
486            &self.summary()?,
487            self.long_description()?.split('\n').collect(),
488        ))
489    }
490
491    /// Get the extra dependencies.
492    pub fn depends(&self) -> Option<&str> {
493        self.table["depends"].as_str()
494    }
495
496    /// Get the extra recommends.
497    pub fn recommends(&self) -> Option<&str> {
498        self.table["recommends"].as_str()
499    }
500
501    /// Get the extra suggests.
502    pub fn suggests(&self) -> Option<&str> {
503        self.table["suggests"].as_str()
504    }
505
506    #[allow(dead_code)]
507    fn default_provides(&self) -> Option<String> {
508        let mut ret = HashSet::new();
509        let semver_suffix = self.semver_suffix;
510        let semver = &self.crate_version;
511
512        let mut suffixes = vec![];
513        if !semver_suffix {
514            suffixes.push("".to_string());
515        }
516
517        suffixes.push(format!("-{}", semver.major));
518        suffixes.push(format!("-{}.{}", semver.major, semver.minor));
519        suffixes.push(format!(
520            "-{}.{}.{}",
521            semver.major, semver.minor, semver.patch
522        ));
523        for ver_suffix in suffixes {
524            let mut feature_suffixes = HashSet::new();
525            feature_suffixes.insert("".to_string());
526            feature_suffixes.insert("+default".to_string());
527            feature_suffixes.extend(
528                self.features
529                    .as_ref()
530                    .map(|k| k.iter().map(|k| format!("+{}", k)).collect::<HashSet<_>>())
531                    .unwrap_or_default(),
532            );
533            for feature_suffix in feature_suffixes {
534                ret.insert(debcargo_binary_name(
535                    &self.crate_name,
536                    &format!("{}{}", ver_suffix, &feature_suffix),
537                ));
538            }
539        }
540        ret.remove(self.name());
541        if ret.is_empty() {
542            None
543        } else {
544            Some(format!(
545                "\n{}",
546                &ret.iter()
547                    .map(|s| format!("{} (= ${{binary:Version}})", s))
548                    .collect::<Vec<_>>()
549                    .join(",\n ")
550            ))
551        }
552    }
553}
554
555fn debnormalize(s: &str) -> String {
556    s.to_lowercase().replace('_', "-")
557}
558
559fn semver_pair(s: &semver::Version) -> String {
560    format!("{}.{}", s.major, s.minor)
561}
562
563fn debcargo_binary_name(crate_name: &str, suffix: &str) -> String {
564    format!("librust-{}{}-dev", debnormalize(crate_name), suffix)
565}
566
567/// Unmangle a debcargo version.
568pub fn unmangle_debcargo_version(version: &str) -> String {
569    version.replace("~", "-")
570}
571
572#[cfg(test)]
573mod tests {
574    #[test]
575    fn test_debcargo_binary_name() {
576        assert_eq!(super::debcargo_binary_name("foo", ""), "librust-foo-dev");
577        assert_eq!(
578            super::debcargo_binary_name("foo", "-1"),
579            "librust-foo-1-dev"
580        );
581        assert_eq!(
582            super::debcargo_binary_name("foo", "-1.2"),
583            "librust-foo-1.2-dev"
584        );
585        assert_eq!(
586            super::debcargo_binary_name("foo", "-1.2.3"),
587            "librust-foo-1.2.3-dev"
588        );
589    }
590
591    #[test]
592    fn test_semver_pair() {
593        assert_eq!(super::semver_pair(&"1.2.3".parse().unwrap()), "1.2");
594        assert_eq!(super::semver_pair(&"1.2.6".parse().unwrap()), "1.2");
595    }
596
597    #[test]
598    fn test_debnormalize() {
599        assert_eq!(super::debnormalize("foo_bar"), "foo-bar");
600        assert_eq!(super::debnormalize("foo"), "foo");
601    }
602
603    #[test]
604    fn test_debcargo_editor() {
605        let mut editor = super::DebcargoEditor::new();
606        editor.debcargo["source"]["standards-version"] = toml_edit::value("4.5.1");
607        editor.debcargo["source"]["homepage"] = toml_edit::value("https://example.com");
608        editor.debcargo["source"]["vcs_git"] = toml_edit::value("https://example.com");
609        editor.debcargo["source"]["vcs_browser"] = toml_edit::value("https://example.com");
610        editor.debcargo["source"]["section"] = toml_edit::value("notrust");
611        editor.debcargo["source"]["priority"] = toml_edit::value("optional");
612        editor.debcargo["source"]["requires_root"] = toml_edit::value("no");
613        editor.debcargo["source"]["maintainer"] =
614            toml_edit::value("Jelmer Vernooij <jelmer@debian.org>");
615
616        assert_eq!(editor.source().standards_version(), "4.5.1");
617        assert_eq!(
618            editor.source().vcs_git().as_deref(),
619            Some("https://example.com")
620        );
621        assert_eq!(
622            editor.source().vcs_browser().as_deref(),
623            Some("https://example.com")
624        );
625        assert_eq!(editor.source().section(), "notrust");
626        assert_eq!(editor.source().priority(), super::DEFAULT_PRIORITY);
627        assert!(!editor.source().rules_requires_root());
628        assert_eq!(
629            editor.source().maintainer(),
630            "Jelmer Vernooij <jelmer@debian.org>"
631        );
632        assert_eq!(editor.source().name(), None);
633        assert_eq!(editor.source().uploaders(), None);
634        assert_eq!(editor.source().homepage(), Some("https://example.com"));
635    }
636}