1use breezyshim::dirty_tracker::DirtyTreeTracker;
2use breezyshim::error::Error;
3use breezyshim::tree::WorkingTree;
4use breezyshim::workingtree::GenericWorkingTree;
5use debian_analyzer::{
6 add_changelog_entry, apply_or_revert, certainty_sufficient, get_committer, ApplyError,
7 Certainty, ChangelogError,
8};
9use debian_control::fields::MultiArch;
10use debian_workspace::action::{Action, ActionPlan, Deb822Action, ParagraphSelector};
11use debian_workspace::appliers::apply_actions;
12use debian_workspace::workspace::Workspace;
13use debversion::Version;
14use lazy_regex::regex_captures;
15use lazy_static::lazy_static;
16use reqwest::blocking::Client;
17use serde::Deserialize;
18use serde_yaml::from_value;
19use std::collections::HashMap;
20use std::fs;
21use std::io::Read;
22use std::io::Write;
23use std::path::Path;
24use std::time::SystemTime;
25
26pub const MULTIARCH_HINTS_URL: &str = "https://dedup.debian.net/static/multiarch-hints.yaml.xz";
27const USER_AGENT: &str = concat!("apply-multiarch-hints/", env!("CARGO_PKG_VERSION"));
28
29const DEFAULT_VALUE_MULTIARCH_HINT: i32 = 100;
30
31#[derive(Debug, Clone, Copy, std::hash::Hash, PartialEq, Eq)]
32pub enum HintKind {
33 MaForeign,
34 FileConflict,
35 MaForeignLibrary,
36 DepAny,
37 MaSame,
38 ArchAll,
39 MaWorkaround,
40}
41
42impl std::str::FromStr for HintKind {
43 type Err = String;
44
45 fn from_str(s: &str) -> Result<Self, Self::Err> {
46 match s {
47 "ma-foreign" => Ok(HintKind::MaForeign),
48 "file-conflict" => Ok(HintKind::FileConflict),
49 "ma-foreign-library" => Ok(HintKind::MaForeignLibrary),
50 "dep-any" => Ok(HintKind::DepAny),
51 "ma-same" => Ok(HintKind::MaSame),
52 "arch-all" => Ok(HintKind::ArchAll),
53 "ma-workaround" => Ok(HintKind::MaWorkaround),
54 _ => Err(format!("Invalid hint kind: {:?}", s)),
55 }
56 }
57}
58
59fn hint_value(hint: HintKind) -> i32 {
60 match hint {
61 HintKind::MaForeign => 20,
62 HintKind::FileConflict => 50,
63 HintKind::MaForeignLibrary => 20,
64 HintKind::DepAny => 20,
65 HintKind::MaSame => 20,
66 HintKind::ArchAll => 20,
67 HintKind::MaWorkaround => 20,
68 }
69}
70
71pub fn calculate_value(hints: &[HintKind]) -> i32 {
72 hints.iter().map(|hint| hint_value(*hint)).sum::<i32>() + DEFAULT_VALUE_MULTIARCH_HINT
73}
74
75fn format_system_time(system_time: SystemTime) -> String {
76 let datetime: chrono::DateTime<chrono::Utc> = system_time.into();
77 datetime.format("%a, %d %b %Y %H:%M:%S GMT").to_string()
78}
79
80#[derive(Debug, Deserialize, PartialEq, Eq, Ord, PartialOrd, Clone, Copy)]
81pub enum Severity {
82 #[serde(rename = "low")]
83 Low,
84 #[serde(rename = "normal")]
85 Normal,
86 #[serde(rename = "high")]
87 High,
88}
89
90fn deserialize_severity<'de, D>(deserializer: D) -> Result<Severity, D::Error>
91where
92 D: serde::Deserializer<'de>,
93{
94 let s = String::deserialize(deserializer)?;
95 match s.as_str() {
96 "low" => Ok(Severity::Low),
97 "normal" => Ok(Severity::Normal),
98 "high" => Ok(Severity::High),
99 _ => Err(serde::de::Error::custom(format!(
100 "Invalid severity: {:?}",
101 s
102 ))),
103 }
104}
105
106#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
107pub struct Hint {
108 pub binary: String,
109 pub description: String,
110 pub source: String,
111 pub link: String,
112 #[serde(deserialize_with = "deserialize_severity")]
113 pub severity: Severity,
114 pub version: Option<Version>,
115}
116
117impl Hint {
118 pub fn kind(&self) -> &str {
119 self.link.split('#').next_back().unwrap()
120 }
121}
122
123pub fn multiarch_hints_by_source(hints: &[Hint]) -> HashMap<&str, Vec<&Hint>> {
124 let mut map = HashMap::new();
125 for hint in hints {
126 map.entry(hint.source.as_str())
127 .or_insert_with(Vec::new)
128 .push(hint);
129 }
130 map
131}
132
133pub fn multiarch_hints_by_binary(hints: &[Hint]) -> HashMap<&str, Vec<&Hint>> {
134 let mut map = HashMap::new();
135 for hint in hints {
136 map.entry(hint.binary.as_str())
137 .or_insert_with(Vec::new)
138 .push(hint);
139 }
140 map
141}
142
143pub fn parse_multiarch_hints(f: &[u8]) -> Result<Vec<Hint>, serde_yaml::Error> {
144 let data = serde_yaml::from_slice::<serde_yaml::Value>(f)?;
145 if let Some(format) = data["format"].as_str() {
146 if format != "multiarch-hints-1.0" {
147 return Err(serde::de::Error::custom(format!(
148 "Invalid format: {:?}",
149 format
150 )));
151 }
152 } else {
153 return Err(serde::de::Error::custom("Missing format"));
154 }
155 from_value(data["hints"].clone())
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn test_some_entries() {
164 let hints = parse_multiarch_hints(
165 r#"format: multiarch-hints-1.0
166hints:
167- binary: coinor-libcoinmp-dev
168 description: coinor-libcoinmp-dev conflicts on ...
169 link: https://wiki.debian.org/MultiArch/Hints#file-conflict
170 severity: high
171 source: coinmp
172 version: 1.8.3-2+b11
173"#
174 .as_bytes(),
175 )
176 .unwrap();
177 assert_eq!(
178 hints,
179 vec![Hint {
180 binary: "coinor-libcoinmp-dev".to_string(),
181 description: "coinor-libcoinmp-dev conflicts on ...".to_string(),
182 link: "https://wiki.debian.org/MultiArch/Hints#file-conflict".to_string(),
183 severity: Severity::High,
184 version: Some("1.8.3-2+b11".parse().unwrap()),
185 source: "coinmp".to_string(),
186 }]
187 );
188 }
189
190 #[test]
191 fn test_invalid_header() {
192 let hints = parse_multiarch_hints(
193 r#"\
194format: blah
195"#
196 .as_bytes(),
197 );
198 assert!(hints.is_err());
199 }
200
201 fn make_hint(binary: &str, kind: &str, description: &str) -> Hint {
202 Hint {
203 binary: binary.to_string(),
204 description: description.to_string(),
205 link: format!("https://wiki.debian.org/MultiArch/Hints#{}", kind),
206 severity: Severity::Normal,
207 version: None,
208 source: "src".to_string(),
209 }
210 }
211
212 fn setup_ws(
213 control: &str,
214 ) -> (
215 tempfile::TempDir,
216 debian_workspace::fs_workspace::FsWorkspace,
217 ) {
218 let tmp = tempfile::TempDir::new().unwrap();
219 let debian = tmp.path().join("debian");
220 std::fs::create_dir_all(&debian).unwrap();
221 std::fs::write(debian.join("control"), control).unwrap();
222 let ws = debian_workspace::fs_workspace::FsWorkspace::new(
223 tmp.path(),
224 Some("src".into()),
225 Some("1.0".parse().unwrap()),
226 );
227 (tmp, ws)
228 }
229
230 fn detect_one(
231 ws: &debian_workspace::fs_workspace::FsWorkspace,
232 hints_list: &[Hint],
233 ) -> Vec<(Change, ActionPlan)> {
234 let by_binary = multiarch_hints_by_binary(hints_list);
235 detect_multiarch_hints(ws, &by_binary, Certainty::Possible).unwrap()
236 }
237
238 fn apply_and_read(
239 ws: &debian_workspace::fs_workspace::FsWorkspace,
240 plans: &[ActionPlan],
241 ) -> String {
242 let actions: Vec<_> = plans
243 .iter()
244 .flat_map(|p| p.actions.iter().cloned())
245 .collect();
246 debian_workspace::appliers::apply_actions(ws.base_path(), &actions).unwrap();
247 std::fs::read_to_string(ws.base_path().join("debian/control")).unwrap()
248 }
249
250 #[test]
251 fn detect_ma_foreign() {
252 let (_tmp, ws) = setup_ws("Source: src\n\nPackage: foo\nArchitecture: any\n");
253 let hints = vec![make_hint("foo", "ma-foreign", "foo could be MA: foreign")];
254 let results = detect_one(&ws, &hints);
255 assert_eq!(results.len(), 1);
256 assert_eq!(results[0].0.binary, "foo");
257 assert_eq!(results[0].0.description, "Add Multi-Arch: foreign.");
258
259 let control = apply_and_read(
260 &ws,
261 &results.iter().map(|(_, p)| p.clone()).collect::<Vec<_>>(),
262 );
263 assert!(control.contains("Multi-Arch: foreign"), "got: {}", control);
264 }
265
266 #[test]
267 fn detect_ma_foreign_noop_when_already_set() {
268 let (_tmp, ws) =
269 setup_ws("Source: src\n\nPackage: foo\nArchitecture: any\nMulti-Arch: foreign\n");
270 let hints = vec![make_hint("foo", "ma-foreign", "foo could be MA: foreign")];
271 let results = detect_one(&ws, &hints);
272 assert_eq!(results.len(), 0);
273 }
274
275 #[test]
276 fn detect_ma_foreign_library() {
277 let (_tmp, ws) =
278 setup_ws("Source: src\n\nPackage: foo\nArchitecture: any\nMulti-Arch: foreign\n");
279 let hints = vec![make_hint(
280 "foo",
281 "ma-foreign-library",
282 "foo should not be MA: foreign",
283 )];
284 let results = detect_one(&ws, &hints);
285 assert_eq!(results.len(), 1);
286 assert_eq!(results[0].0.description, "Drop Multi-Arch: foreign.");
287
288 let control = apply_and_read(
289 &ws,
290 &results.iter().map(|(_, p)| p.clone()).collect::<Vec<_>>(),
291 );
292 assert!(!control.contains("Multi-Arch"), "got: {}", control);
293 }
294
295 #[test]
296 fn detect_file_conflict() {
297 let (_tmp, ws) =
298 setup_ws("Source: src\n\nPackage: foo\nArchitecture: any\nMulti-Arch: same\n");
299 let hints = vec![make_hint("foo", "file-conflict", "foo conflicts")];
300 let results = detect_one(&ws, &hints);
301 assert_eq!(results.len(), 1);
302 assert_eq!(results[0].0.description, "Drop Multi-Arch: same.");
303
304 let control = apply_and_read(
305 &ws,
306 &results.iter().map(|(_, p)| p.clone()).collect::<Vec<_>>(),
307 );
308 assert!(!control.contains("Multi-Arch"), "got: {}", control);
309 }
310
311 #[test]
312 fn detect_ma_same() {
313 let (_tmp, ws) = setup_ws("Source: src\n\nPackage: foo\nArchitecture: any\n");
314 let hints = vec![make_hint("foo", "ma-same", "foo should be MA: same")];
315 let results = detect_one(&ws, &hints);
316 assert_eq!(results.len(), 1);
317 assert_eq!(results[0].0.description, "Add Multi-Arch: same.");
318
319 let control = apply_and_read(
320 &ws,
321 &results.iter().map(|(_, p)| p.clone()).collect::<Vec<_>>(),
322 );
323 assert!(control.contains("Multi-Arch: same"), "got: {}", control);
324 }
325
326 #[test]
327 fn detect_arch_all() {
328 let (_tmp, ws) = setup_ws("Source: src\n\nPackage: foo\nArchitecture: any\n");
329 let hints = vec![make_hint("foo", "arch-all", "foo should be arch:all")];
330 let results = detect_one(&ws, &hints);
331 assert_eq!(results.len(), 1);
332 assert_eq!(results[0].0.description, "Make package Architecture: all.");
333 assert_eq!(results[0].0.certainty, Certainty::Possible);
334
335 let control = apply_and_read(
336 &ws,
337 &results.iter().map(|(_, p)| p.clone()).collect::<Vec<_>>(),
338 );
339 assert!(control.contains("Architecture: all"), "got: {}", control);
340 }
341
342 #[test]
343 fn detect_arch_all_noop_when_already_all() {
344 let (_tmp, ws) = setup_ws("Source: src\n\nPackage: foo\nArchitecture: all\n");
345 let hints = vec![make_hint("foo", "arch-all", "foo should be arch:all")];
346 let results = detect_one(&ws, &hints);
347 assert_eq!(results.len(), 0);
348 }
349
350 #[test]
351 fn detect_dep_any() {
352 let (_tmp, ws) =
353 setup_ws("Source: src\n\nPackage: foo\nArchitecture: any\nDepends: libbar (>= 1.0)\n");
354 let hints = vec![make_hint(
355 "foo",
356 "dep-any",
357 "foo could have its dependency on libbar annotated with :any",
358 )];
359 let results = detect_one(&ws, &hints);
360 assert_eq!(results.len(), 1);
361 assert_eq!(
362 results[0].0.description,
363 "Add :any qualifier for libbar dependency."
364 );
365
366 let control = apply_and_read(
367 &ws,
368 &results.iter().map(|(_, p)| p.clone()).collect::<Vec<_>>(),
369 );
370 assert!(control.contains("libbar:any"), "got: {}", control);
371 }
372
373 #[test]
374 fn detect_dep_any_noop_when_already_annotated() {
375 let (_tmp, ws) = setup_ws(
376 "Source: src\n\nPackage: foo\nArchitecture: any\nDepends: libbar:any (>= 1.0)\n",
377 );
378 let hints = vec![make_hint(
379 "foo",
380 "dep-any",
381 "foo could have its dependency on libbar annotated with :any",
382 )];
383 let results = detect_one(&ws, &hints);
384 assert_eq!(results.len(), 0);
385 }
386
387 #[test]
388 fn detect_ma_workaround() {
389 let (_tmp, ws) = setup_ws("Source: src\n\nPackage: foo\nArchitecture: all\n");
390 let hints = vec![make_hint(
391 "foo",
392 "ma-workaround",
393 "foo should be Architecture: any + Multi-Arch: same",
394 )];
395 let results = detect_one(&ws, &hints);
396 assert_eq!(results.len(), 1);
397 assert_eq!(
398 results[0].0.description,
399 "Add Multi-Arch: same and set Architecture: any."
400 );
401
402 let control = apply_and_read(
403 &ws,
404 &results.iter().map(|(_, p)| p.clone()).collect::<Vec<_>>(),
405 );
406 assert!(control.contains("Multi-Arch: same"), "got: {}", control);
407 assert!(control.contains("Architecture: any"), "got: {}", control);
408 }
409
410 #[test]
411 fn detect_skips_unknown_binary() {
412 let (_tmp, ws) = setup_ws("Source: src\n\nPackage: foo\nArchitecture: any\n");
413 let hints = vec![make_hint("bar", "ma-foreign", "bar could be MA: foreign")];
414 let results = detect_one(&ws, &hints);
415 assert_eq!(results.len(), 0);
416 }
417
418 #[test]
419 fn detect_skips_below_minimum_certainty() {
420 let (_tmp, ws) = setup_ws("Source: src\n\nPackage: foo\nArchitecture: any\n");
421 let hints = vec![make_hint("foo", "arch-all", "foo should be arch:all")];
422 let by_binary = multiarch_hints_by_binary(&hints);
423 let results = detect_multiarch_hints(&ws, &by_binary, Certainty::Certain).unwrap();
425 assert_eq!(results.len(), 0);
426 }
427
428 #[test]
429 fn detect_no_control_file() {
430 let tmp = tempfile::TempDir::new().unwrap();
431 let ws = debian_workspace::fs_workspace::FsWorkspace::new(
432 tmp.path(),
433 Some("src".into()),
434 Some("1.0".parse().unwrap()),
435 );
436 let hints = vec![make_hint("foo", "ma-foreign", "foo could be MA: foreign")];
437 let by_binary = multiarch_hints_by_binary(&hints);
438 let results = detect_multiarch_hints(&ws, &by_binary, Certainty::Possible).unwrap();
439 assert_eq!(results.len(), 0);
440 }
441}
442
443pub fn cache_download_multiarch_hints(
444 url: Option<&str>,
445) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
446 let cache_home = if let Ok(xdg_cache_home) = std::env::var("XDG_CACHE_HOME") {
447 Path::new(&xdg_cache_home).to_path_buf()
448 } else if let Ok(home) = std::env::var("HOME") {
449 Path::new(&home).join(".cache")
450 } else {
451 log::warn!("Unable to find cache directory, not caching");
452 return download_multiarch_hints(url, None)?
453 .ok_or_else(|| "Expected download data but got None".into());
454 };
455 let cache_dir = cache_home.join("lintian-brush");
456 fs::create_dir_all(&cache_dir)?;
457 let local_hints_path = cache_dir.join("multiarch-hints.yml");
458 let last_modified = match fs::metadata(&local_hints_path) {
459 Ok(metadata) => Some(metadata.modified()?),
460 Err(_) => None,
461 };
462
463 match download_multiarch_hints(url, last_modified) {
464 Ok(None) => {
465 let mut buffer = Vec::new();
466 std::fs::File::open(&local_hints_path)?.read_to_end(&mut buffer)?;
467 Ok(buffer)
468 }
469 Ok(Some(buffer)) => {
470 fs::File::create(&local_hints_path)?.write_all(&buffer)?;
471 Ok(buffer)
472 }
473 Err(e) => Err(e),
474 }
475}
476
477pub fn download_multiarch_hints(
478 url: Option<&str>,
479 since: Option<SystemTime>,
480) -> Result<Option<Vec<u8>>, Box<dyn std::error::Error>> {
481 let url = url.unwrap_or(MULTIARCH_HINTS_URL);
482 let client = Client::builder().user_agent(USER_AGENT).build()?;
483 let mut request = client.get(url).header("Accept-Encoding", "identity");
484 if let Some(since) = since {
485 request = request.header("If-Modified-Since", format_system_time(since));
486 }
487 let response = request.send()?;
488 if response.status() == reqwest::StatusCode::NOT_MODIFIED {
489 Ok(None)
490 } else if response.status() != reqwest::StatusCode::OK {
491 Err(format!(
492 "Unable to download multiarch hints: {:?}",
493 response.status()
494 )
495 .into())
496 } else if url.ends_with(".xz") {
497 let mut reader = xz2::read::XzDecoder::new(response);
499 let mut buffer = Vec::new();
500 reader.read_to_end(&mut buffer)?;
501 Ok(Some(buffer))
502 } else {
503 Ok(Some(response.bytes()?.to_vec()))
504 }
505}
506
507#[derive(Debug, Clone)]
508pub struct Change {
509 pub binary: String,
510 pub hint: Hint,
511 pub description: String,
512 pub certainty: Certainty,
513}
514
515pub struct OverallResult {
516 pub changes: Vec<Change>,
517}
518
519impl OverallResult {
520 pub fn value(&self) -> i32 {
521 let kinds = self
522 .changes
523 .iter()
524 .map(|x| x.hint.kind().parse().unwrap())
525 .collect::<Vec<_>>();
526 calculate_value(&kinds)
527 }
528}
529
530fn control_file() -> std::path::PathBuf {
531 std::path::PathBuf::from("debian/control")
532}
533
534fn detect_hint_ma_foreign(
535 binary: &debian_control::lossless::control::Binary,
536 _hint: &Hint,
537) -> Option<(String, Vec<Action>)> {
538 if binary.multi_arch() == Some(MultiArch::Foreign) {
539 return None;
540 }
541 let pkg = binary.name()?;
542 Some((
543 "Add Multi-Arch: foreign.".to_string(),
544 vec![Action::Deb822(Deb822Action::SetField {
545 file: control_file(),
546 paragraph: ParagraphSelector::Binary { package: pkg },
547 field: "Multi-Arch".to_string(),
548 value: "foreign".to_string(),
549 })],
550 ))
551}
552
553fn detect_hint_ma_foreign_lib(
554 binary: &debian_control::lossless::control::Binary,
555 _hint: &Hint,
556) -> Option<(String, Vec<Action>)> {
557 if binary.multi_arch() != Some(MultiArch::Foreign) {
558 return None;
559 }
560 let pkg = binary.name()?;
561 Some((
562 "Drop Multi-Arch: foreign.".to_string(),
563 vec![Action::Deb822(Deb822Action::RemoveField {
564 file: control_file(),
565 paragraph: ParagraphSelector::Binary { package: pkg },
566 field: "Multi-Arch".to_string(),
567 })],
568 ))
569}
570
571fn detect_hint_file_conflict(
572 binary: &debian_control::lossless::control::Binary,
573 _hint: &Hint,
574) -> Option<(String, Vec<Action>)> {
575 if binary.multi_arch() != Some(MultiArch::Same) {
576 return None;
577 }
578 let pkg = binary.name()?;
579 Some((
580 "Drop Multi-Arch: same.".to_string(),
581 vec![Action::Deb822(Deb822Action::RemoveField {
582 file: control_file(),
583 paragraph: ParagraphSelector::Binary { package: pkg },
584 field: "Multi-Arch".to_string(),
585 })],
586 ))
587}
588
589fn detect_hint_ma_same(
590 binary: &debian_control::lossless::control::Binary,
591 _hint: &Hint,
592) -> Option<(String, Vec<Action>)> {
593 if binary.multi_arch() == Some(MultiArch::Same) {
594 return None;
595 }
596 let pkg = binary.name()?;
597 Some((
598 "Add Multi-Arch: same.".to_string(),
599 vec![Action::Deb822(Deb822Action::SetField {
600 file: control_file(),
601 paragraph: ParagraphSelector::Binary { package: pkg },
602 field: "Multi-Arch".to_string(),
603 value: "same".to_string(),
604 })],
605 ))
606}
607
608fn detect_hint_arch_all(
609 binary: &debian_control::lossless::control::Binary,
610 _hint: &Hint,
611) -> Option<(String, Vec<Action>)> {
612 if binary.architecture().as_deref() == Some("all") {
613 return None;
614 }
615 let pkg = binary.name()?;
616 Some((
617 "Make package Architecture: all.".to_string(),
618 vec![Action::Deb822(Deb822Action::SetField {
619 file: control_file(),
620 paragraph: ParagraphSelector::Binary { package: pkg },
621 field: "Architecture".to_string(),
622 value: "all".to_string(),
623 })],
624 ))
625}
626
627fn detect_hint_dep_any(
628 binary: &debian_control::lossless::control::Binary,
629 hint: &Hint,
630) -> Option<(String, Vec<Action>)> {
631 let Some((_whole, binary_package, dep)) = regex_captures!(
632 r"(.*) could have its dependency on (.*) annotated with :any",
633 hint.description.as_str()
634 ) else {
635 log::warn!("Unable to parse dep-any hint: {:?}", hint.description);
636 return None;
637 };
638 assert_eq!(binary_package, binary.name().unwrap());
639
640 let depends = binary.depends()?;
641 let entry_text = depends.entries().find_map(|entry| {
642 entry.relations().find_map(|r| {
643 if r.try_name().as_deref() == Some(dep) && r.archqual().as_deref() != Some("any") {
644 Some(entry.to_string().replacen(dep, &format!("{}:any", dep), 1))
647 } else {
648 None
649 }
650 })
651 })?;
652
653 let pkg = binary.name()?;
654 Some((
655 format!("Add :any qualifier for {} dependency.", dep),
656 vec![Action::Deb822(Deb822Action::ReplaceRelation {
657 file: control_file(),
658 paragraph: ParagraphSelector::Binary { package: pkg },
659 field: "Depends".to_string(),
660 from_package: dep.to_string(),
661 to_entry: entry_text,
662 })],
663 ))
664}
665
666fn detect_hint_ma_workaround(
667 binary: &debian_control::lossless::control::Binary,
668 hint: &Hint,
669) -> Option<(String, Vec<Action>)> {
670 let Some((_whole, binary_package)) = regex_captures!(
671 r"(.*) should be Architecture: any \+ Multi-Arch: same",
672 hint.description.as_str()
673 ) else {
674 log::warn!("Unable to parse ma-workaround hint: {:?}", hint.description);
675 return None;
676 };
677 assert_eq!(binary_package, binary.name().unwrap());
678 let pkg = binary.name()?;
679 Some((
680 "Add Multi-Arch: same and set Architecture: any.".to_string(),
681 vec![
682 Action::Deb822(Deb822Action::SetField {
683 file: control_file(),
684 paragraph: ParagraphSelector::Binary {
685 package: pkg.clone(),
686 },
687 field: "Multi-Arch".to_string(),
688 value: "same".to_string(),
689 }),
690 Action::Deb822(Deb822Action::SetField {
691 file: control_file(),
692 paragraph: ParagraphSelector::Binary { package: pkg },
693 field: "Architecture".to_string(),
694 value: "any".to_string(),
695 }),
696 ],
697 ))
698}
699
700type DetectorFn =
701 fn(&debian_control::lossless::control::Binary, &Hint) -> Option<(String, Vec<Action>)>;
702
703struct Detector {
704 kind: &'static str,
705 certainty: Certainty,
706 cb: DetectorFn,
707}
708
709lazy_static! {
710 static ref DETECTORS: Vec<Detector> = vec![
711 Detector {
712 kind: "ma-foreign",
713 certainty: Certainty::Certain,
714 cb: detect_hint_ma_foreign,
715 },
716 Detector {
717 kind: "file-conflict",
718 certainty: Certainty::Certain,
719 cb: detect_hint_file_conflict,
720 },
721 Detector {
722 kind: "ma-foreign-library",
723 certainty: Certainty::Certain,
724 cb: detect_hint_ma_foreign_lib,
725 },
726 Detector {
727 kind: "dep-any",
728 certainty: Certainty::Certain,
729 cb: detect_hint_dep_any,
730 },
731 Detector {
732 kind: "ma-same",
733 certainty: Certainty::Certain,
734 cb: detect_hint_ma_same,
735 },
736 Detector {
737 kind: "arch-all",
738 certainty: Certainty::Possible,
739 cb: detect_hint_arch_all,
740 },
741 Detector {
742 kind: "ma-workaround",
743 certainty: Certainty::Certain,
744 cb: detect_hint_ma_workaround,
745 },
746 ];
747}
748
749fn find_detector(kind: &str) -> Option<&'static Detector> {
750 DETECTORS.iter().find(|x| x.kind == kind)
751}
752
753pub fn detect_multiarch_hints(
759 ws: &dyn Workspace,
760 hints: &HashMap<&str, Vec<&Hint>>,
761 minimum_certainty: Certainty,
762) -> Result<Vec<(Change, ActionPlan)>, debian_workspace::Error> {
763 let control = match ws.parsed_control() {
764 Ok(c) => c,
765 Err(debian_workspace::Error::NotFound) => return Ok(Vec::new()),
766 Err(e) => return Err(e),
767 };
768
769 let mut results = Vec::new();
770 for binary in control.binaries() {
771 let Some(package) = binary.name() else {
772 continue;
773 };
774 let Some(package_hints) = hints.get(package.as_str()) else {
775 continue;
776 };
777 for hint in package_hints {
778 let kind = hint.kind();
779 let detector = match find_detector(kind) {
780 Some(d) => d,
781 None => {
782 log::warn!("Unknown hint kind: {}", kind);
783 continue;
784 }
785 };
786 if !certainty_sufficient(detector.certainty, Some(minimum_certainty)) {
787 continue;
788 }
789 if let Some((description, actions)) = (detector.cb)(&binary, hint) {
790 results.push((
791 Change {
792 binary: package.clone(),
793 hint: (*hint).clone(),
794 description: description.clone(),
795 certainty: detector.certainty,
796 },
797 ActionPlan {
798 label: description,
799 opinionated: false,
800 actions,
801 },
802 ));
803 }
804 }
805 }
806 Ok(results)
807}
808
809fn changes_by_description(changes: &[Change]) -> HashMap<String, Vec<String>> {
810 let mut by_description: HashMap<String, Vec<String>> = HashMap::new();
811 for change in changes {
812 by_description
813 .entry(change.description.clone())
814 .or_default()
815 .push(change.binary.clone());
816 }
817 by_description
818}
819
820#[derive(Debug)]
821pub enum OverallError {
822 BrzError(Error),
823 NotDebianPackage(std::path::PathBuf),
824 Other(String),
825 NoWhoami,
826 NoChanges,
827 GeneratedFile(std::path::PathBuf),
828 FormattingUnpreservable(std::path::PathBuf),
829}
830
831impl From<debian_analyzer::editor::EditorError> for OverallError {
832 fn from(e: debian_analyzer::editor::EditorError) -> Self {
833 match e {
834 debian_analyzer::editor::EditorError::GeneratedFile(p, _) => {
835 OverallError::GeneratedFile(p)
836 }
837 debian_analyzer::editor::EditorError::FormattingUnpreservable(p, _) => {
838 OverallError::FormattingUnpreservable(p)
839 }
840 debian_analyzer::editor::EditorError::BrzError(e) => OverallError::BrzError(e),
841 debian_analyzer::editor::EditorError::IoError(e) => OverallError::Other(e.to_string()),
842 debian_analyzer::editor::EditorError::TemplateError(p, _e) => {
843 OverallError::GeneratedFile(p)
844 }
845 }
846 }
847}
848
849impl std::fmt::Display for OverallError {
850 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
851 match self {
852 OverallError::NotDebianPackage(p) => {
853 write!(f, "{} is not a Debian package.", p.display())
854 }
855 OverallError::GeneratedFile(p) => {
856 write!(f, "Generated file: {}", p.display())
857 }
858 OverallError::FormattingUnpreservable(p) => {
859 write!(f, "Formatting unpreservable: {}", p.display())
860 }
861 OverallError::BrzError(e) => write!(f, "{}", e),
862 OverallError::NoWhoami => write!(f, "No committer configured."),
863 OverallError::NoChanges => write!(f, "No changes to apply."),
864 OverallError::Other(e) => write!(f, "{}", e),
865 }
866 }
867}
868
869impl std::error::Error for OverallError {}
870
871impl From<Error> for OverallError {
872 fn from(e: Error) -> Self {
873 match e {
874 Error::PointlessCommit => OverallError::NoChanges,
875 Error::NoWhoami => OverallError::NoWhoami,
876 Error::Other(e) => OverallError::Other(e.to_string()),
877 e => OverallError::BrzError(e),
878 }
879 }
880}
881
882impl From<ChangelogError> for OverallError {
883 fn from(e: ChangelogError) -> Self {
884 match e {
885 ChangelogError::NotDebianPackage(p) => OverallError::NotDebianPackage(p),
886 ChangelogError::Python(e) => OverallError::Other(e.to_string()),
887 }
888 }
889}
890
891#[derive(Debug, Clone)]
893pub struct ApplyMultiarchHintsConfig {
894 pub minimum_certainty: Option<Certainty>,
895 pub committer: Option<String>,
896 pub update_changelog: bool,
897 pub allow_reformatting: Option<bool>,
898}
899
900impl Default for ApplyMultiarchHintsConfig {
901 fn default() -> Self {
902 Self {
903 minimum_certainty: None,
904 committer: None,
905 update_changelog: true,
906 allow_reformatting: None,
907 }
908 }
909}
910
911#[allow(clippy::result_large_err)]
912pub fn apply_multiarch_hints(
913 local_tree: &GenericWorkingTree,
914 subpath: &std::path::Path,
915 hints: &HashMap<&str, Vec<&Hint>>,
916 dirty_tracker: Option<&mut DirtyTreeTracker>,
917 config: &ApplyMultiarchHintsConfig,
918) -> Result<OverallResult, OverallError> {
919 let minimum_certainty = config.minimum_certainty.unwrap_or(Certainty::Certain);
920 let basis_tree = local_tree.basis_tree().map_err(OverallError::BrzError)?;
921 let (changes, _tree_changes, mut specific_files) = match apply_or_revert(
922 local_tree,
923 subpath,
924 &basis_tree,
925 dirty_tracker,
926 |path| -> Result<Vec<Change>, OverallError> {
927 let ws = debian_workspace::fs_workspace::FsWorkspace::new(path, None, None);
928 let detected = detect_multiarch_hints(&ws, hints, minimum_certainty)
929 .map_err(|e| OverallError::Other(e.to_string()))?;
930
931 if detected.is_empty() {
932 return Ok(Vec::new());
933 }
934
935 let all_actions: Vec<_> = detected
936 .iter()
937 .flat_map(|(_, plan)| plan.actions.iter().cloned())
938 .collect();
939 apply_actions(path, &all_actions).map_err(|e| OverallError::Other(e.to_string()))?;
940
941 Ok(detected.into_iter().map(|(change, _)| change).collect())
942 },
943 ) {
944 Ok(r) => r,
945 Err(ApplyError::NoChanges(_)) => return Err(OverallError::NoChanges),
946 Err(ApplyError::BrzError(e)) => return Err(OverallError::BrzError(e)),
947 Err(ApplyError::CallbackError(_)) => panic!("Unexpected callback error"),
948 };
949
950 let by_description = changes_by_description(changes.as_slice());
951 let mut overall_description = vec!["Apply multi-arch hints.\n".to_string()];
952 for (description, mut binaries) in by_description {
953 binaries.sort();
954 overall_description.push(format!(" + {}: {}\n", binaries.join(", "), description));
955 }
956
957 let changelog_path = subpath.join("debian/changelog");
958
959 if config.update_changelog {
960 add_changelog_entry(
961 local_tree,
962 changelog_path.as_path(),
963 overall_description
964 .iter()
965 .map(|x| x.as_str())
966 .collect::<Vec<_>>()
967 .as_slice(),
968 )?;
969 if let Some(specific_files) = specific_files.as_mut() {
970 specific_files.push(changelog_path);
971 }
972 }
973
974 overall_description.push("\n".to_string());
975 overall_description.push("Changes-By: apply-multiarch-hints\n".to_string());
976
977 let committer = config
978 .committer
979 .clone()
980 .unwrap_or_else(|| get_committer(local_tree));
981
982 let specific_files_ref = specific_files
983 .as_ref()
984 .map(|x| x.iter().map(|x| x.as_path()).collect::<Vec<_>>());
985
986 let mut builder = local_tree
987 .build_commit()
988 .message(overall_description.concat().as_str())
989 .allow_pointless(false)
990 .committer(&committer);
991
992 if let Some(specific_files) = specific_files_ref.as_deref() {
993 builder = builder.specific_files(specific_files);
994 }
995
996 builder.commit()?;
997
998 Ok(OverallResult { changes })
999}