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<'a> DebcargoSource<'a> {
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 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 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 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 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 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 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 pub fn set_maintainer(&mut self, maintainer: &str) -> &mut Self {
373 self.toml_section_mut()["maintainer"] = value(maintainer);
374 self
375 }
376
377 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 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)]
399pub 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 pub fn name(&self) -> &str {
441 &self.name
442 }
443
444 pub fn architecture(&self) -> Option<&str> {
446 Some("any")
447 }
448
449 pub fn multi_arch(&self) -> Option<MultiArch> {
451 Some(MultiArch::Same)
452 }
453
454 pub fn section(&self) -> Option<&str> {
456 self.table["section"].as_str()
457 }
458
459 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 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 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 pub fn depends(&self) -> Option<&str> {
493 self.table["depends"].as_str()
494 }
495
496 pub fn recommends(&self) -> Option<&str> {
498 self.table["recommends"].as_str()
499 }
500
501 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
567pub 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}