1use debian_control::fields::MultiArch;
6use std::collections::{HashMap, HashSet};
7use std::path::{Path, PathBuf};
8use toml_edit::{value, DocumentMut, Table};
9
10pub const DEFAULT_MAINTAINER: &str =
12 "Debian Rust Maintainers <pkg-rust-maintainers@alioth-lists.debian.net>";
13
14pub const DEFAULT_SECTION: &str = "rust";
16
17pub const CURRENT_STANDARDS_VERSION: &str = "4.5.1";
19
20pub const DEFAULT_PRIORITY: debian_control::Priority = debian_control::Priority::Optional;
22
23pub struct DebcargoEditor {
25 debcargo_toml_path: Option<PathBuf>,
27
28 pub debcargo: DocumentMut,
30
31 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 pub fn new() -> Self {
54 Self {
55 debcargo_toml_path: None,
56 debcargo: DocumentMut::new(),
57 cargo: None,
58 }
59 }
60
61 fn crate_name(&self) -> Option<&str> {
63 self.cargo
64 .as_ref()
65 .and_then(|c| c["package"]["name"].as_str())
66 }
67
68 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 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 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 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 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 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
201pub struct DebcargoSource<'a> {
203 main: &'a mut DebcargoEditor,
204}
205
206impl DebcargoSource<'_> {
207 pub fn toml_section_mut(&mut self) -> &mut Table {
209 self.main.debcargo["source"].as_table_mut().unwrap()
210 }
211
212 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 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 pub fn set_homepage(&mut self, homepage: &str) -> &mut Self {
230 self.toml_section_mut()["homepage"] = value(homepage);
231 self
232 }
233
234 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 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 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 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 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 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 pub fn set_section(&mut self, section: &str) -> &mut Self {
309 self.toml_section_mut()["section"] = value(section);
310 self
311 }
312
313 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 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 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 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 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 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 pub fn set_maintainer(&mut self, maintainer: &str) -> &mut Self {
374 self.toml_section_mut()["maintainer"] = value(maintainer);
375 self
376 }
377
378 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 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)]
405pub 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 pub fn name(&self) -> &str {
447 &self.name
448 }
449
450 pub fn architecture(&self) -> Option<&str> {
452 Some("any")
453 }
454
455 pub fn multi_arch(&self) -> Option<MultiArch> {
457 Some(MultiArch::Same)
458 }
459
460 pub fn section(&self) -> Option<&str> {
462 self.table["section"].as_str()
463 }
464
465 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 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 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 pub fn depends(&self) -> Option<&str> {
499 self.table["depends"].as_str()
500 }
501
502 pub fn recommends(&self) -> Option<&str> {
504 self.table["recommends"].as_str()
505 }
506
507 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
573pub 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}