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