1use 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
12pub const DEFAULT_MAINTAINER: &str =
14 "Debian Rust Maintainers <pkg-rust-maintainers@alioth-lists.debian.net>";
15
16pub const DEFAULT_SECTION: &str = "rust";
18
19pub const CURRENT_STANDARDS_VERSION: &str = "4.5.1";
21
22pub const DEFAULT_PRIORITY: debian_control::Priority = debian_control::Priority::Optional;
24
25pub struct DebcargoEditor {
27 debcargo_toml_path: Option<PathBuf>,
29
30 pub debcargo: DocumentMut,
32
33 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 pub fn new() -> Self {
56 Self {
57 debcargo_toml_path: None,
58 debcargo: DocumentMut::new(),
59 cargo: None,
60 }
61 }
62
63 fn crate_name(&self) -> Option<&str> {
65 self.cargo
66 .as_ref()
67 .and_then(|c| c["package"]["name"].as_str())
68 }
69
70 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 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 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 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 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 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
203pub struct DebcargoSource<'a> {
205 main: &'a mut DebcargoEditor,
206}
207
208impl DebcargoSource<'_> {
209 pub fn toml_section_mut(&mut self) -> &mut Table {
211 self.main.debcargo["source"].as_table_mut().unwrap()
212 }
213
214 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 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 pub fn set_homepage(&mut self, homepage: &str) -> &mut Self {
232 self.toml_section_mut()["homepage"] = value(homepage);
233 self
234 }
235
236 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 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 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 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 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 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 pub fn set_section(&mut self, section: &str) -> &mut Self {
311 self.toml_section_mut()["section"] = value(section);
312 self
313 }
314
315 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 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 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 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 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 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 pub fn set_maintainer(&mut self, maintainer: &str) -> &mut Self {
376 self.toml_section_mut()["maintainer"] = value(maintainer);
377 self
378 }
379
380 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 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)]
407pub 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 pub fn name(&self) -> &str {
449 &self.name
450 }
451
452 pub fn architecture(&self) -> Option<&str> {
454 Some("any")
455 }
456
457 pub fn multi_arch(&self) -> Option<MultiArch> {
459 Some(MultiArch::Same)
460 }
461
462 pub fn section(&self) -> Option<&str> {
464 self.table["section"].as_str()
465 }
466
467 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 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 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 pub fn depends(&self) -> Option<&str> {
501 self.table["depends"].as_str()
502 }
503
504 pub fn recommends(&self) -> Option<&str> {
506 self.table["recommends"].as_str()
507 }
508
509 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
575pub 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}