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