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