1use breezyshim::dirty_tracker::DirtyTreeTracker;
2use breezyshim::error::Error;
3use breezyshim::tree::WorkingTree;
4use debian_analyzer::control::TemplatedControlEditor;
5use debian_analyzer::{
6 add_changelog_entry, apply_or_revert, certainty_sufficient, get_committer, ApplyError,
7 Certainty, ChangelogError,
8};
9use debian_control::control::Binary;
10use debian_control::fields::MultiArch;
11use debversion::Version;
12use lazy_regex::regex_captures;
13use lazy_static::lazy_static;
14use reqwest::blocking::Client;
15use serde::Deserialize;
16use serde_yaml::from_value;
17use std::collections::HashMap;
18use std::fs;
19use std::io::Read;
20use std::io::Write;
21use std::path::Path;
22use std::time::SystemTime;
23
24pub const MULTIARCH_HINTS_URL: &str = "https://dedup.debian.net/static/multiarch-hints.yaml.xz";
25const USER_AGENT: &str = concat!("apply-multiarch-hints/", env!("CARGO_PKG_VERSION"));
26
27const DEFAULT_VALUE_MULTIARCH_HINT: i32 = 100;
28
29#[derive(Debug, Clone, Copy, std::hash::Hash, PartialEq, Eq)]
30pub enum HintKind {
31 MaForeign,
32 FileConflict,
33 MaForeignLibrary,
34 DepAny,
35 MaSame,
36 ArchAll,
37 MaWorkaround,
38}
39
40impl std::str::FromStr for HintKind {
41 type Err = String;
42
43 fn from_str(s: &str) -> Result<Self, Self::Err> {
44 match s {
45 "ma-foreign" => Ok(HintKind::MaForeign),
46 "file-conflict" => Ok(HintKind::FileConflict),
47 "ma-foreign-library" => Ok(HintKind::MaForeignLibrary),
48 "dep-any" => Ok(HintKind::DepAny),
49 "ma-same" => Ok(HintKind::MaSame),
50 "arch-all" => Ok(HintKind::ArchAll),
51 "ma-workaround" => Ok(HintKind::MaWorkaround),
52 _ => Err(format!("Invalid hint kind: {:?}", s)),
53 }
54 }
55}
56
57fn hint_value(hint: HintKind) -> i32 {
58 match hint {
59 HintKind::MaForeign => 20,
60 HintKind::FileConflict => 50,
61 HintKind::MaForeignLibrary => 20,
62 HintKind::DepAny => 20,
63 HintKind::MaSame => 20,
64 HintKind::ArchAll => 20,
65 HintKind::MaWorkaround => 20,
66 }
67}
68
69pub fn calculate_value(hints: &[HintKind]) -> i32 {
70 hints.iter().map(|hint| hint_value(*hint)).sum::<i32>() + DEFAULT_VALUE_MULTIARCH_HINT
71}
72
73fn format_system_time(system_time: SystemTime) -> String {
74 let datetime: chrono::DateTime<chrono::Utc> = system_time.into();
75 datetime.format("%a, %d %b %Y %H:%M:%S GMT").to_string()
76}
77
78#[derive(Debug, Deserialize, PartialEq, Eq, Ord, PartialOrd, Clone, Copy)]
79pub enum Severity {
80 #[serde(rename = "low")]
81 Low,
82 #[serde(rename = "normal")]
83 Normal,
84 #[serde(rename = "high")]
85 High,
86}
87
88fn deserialize_severity<'de, D>(deserializer: D) -> Result<Severity, D::Error>
89where
90 D: serde::Deserializer<'de>,
91{
92 let s = String::deserialize(deserializer)?;
93 match s.as_str() {
94 "low" => Ok(Severity::Low),
95 "normal" => Ok(Severity::Normal),
96 "high" => Ok(Severity::High),
97 _ => Err(serde::de::Error::custom(format!(
98 "Invalid severity: {:?}",
99 s
100 ))),
101 }
102}
103
104#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
105pub struct Hint {
106 pub binary: String,
107 pub description: String,
108 pub source: 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('#').last().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 map.entry(hint.source.as_str())
125 .or_insert_with(Vec::new)
126 .push(hint);
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: "coinmp".to_string(),
184 }]
185 );
186 }
187
188 #[test]
189 fn test_invalid_header() {
190 let hints = parse_multiarch_hints(
191 r#"\
192format: blah
193"#
194 .as_bytes(),
195 );
196 assert!(hints.is_err());
197 }
198}
199
200pub fn cache_download_multiarch_hints(
201 url: Option<&str>,
202) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
203 let cache_home = if let Ok(xdg_cache_home) = std::env::var("XDG_CACHE_HOME") {
204 Path::new(&xdg_cache_home).to_path_buf()
205 } else if let Ok(home) = std::env::var("HOME") {
206 Path::new(&home).join(".cache")
207 } else {
208 log::warn!("Unable to find cache directory, not caching");
209 return download_multiarch_hints(url, None).map(|x| x.unwrap());
210 };
211 let cache_dir = cache_home.join("lintian-brush");
212 fs::create_dir_all(&cache_dir)?;
213 let local_hints_path = cache_dir.join("multiarch-hints.yml");
214 let last_modified = match fs::metadata(&local_hints_path) {
215 Ok(metadata) => Some(metadata.modified()?),
216 Err(_) => None,
217 };
218
219 match download_multiarch_hints(url, last_modified) {
220 Ok(None) => {
221 let mut buffer = Vec::new();
222 std::fs::File::open(&local_hints_path)?.read_to_end(&mut buffer)?;
223 Ok(buffer)
224 }
225 Ok(Some(buffer)) => {
226 fs::File::create(&local_hints_path)?.write_all(&buffer)?;
227 Ok(buffer)
228 }
229 Err(e) => Err(e),
230 }
231}
232
233pub fn download_multiarch_hints(
234 url: Option<&str>,
235 since: Option<SystemTime>,
236) -> Result<Option<Vec<u8>>, Box<dyn std::error::Error>> {
237 let url = url.unwrap_or(MULTIARCH_HINTS_URL);
238 let client = Client::builder().user_agent(USER_AGENT).build()?;
239 let mut request = client.get(url).header("Accept-Encoding", "identity");
240 if let Some(since) = since {
241 request = request.header("If-Modified-Since", format_system_time(since));
242 }
243 let response = request.send()?;
244 if response.status() == reqwest::StatusCode::NOT_MODIFIED {
245 Ok(None)
246 } else if response.status() != reqwest::StatusCode::OK {
247 Err(format!(
248 "Unable to download multiarch hints: {:?}",
249 response.status()
250 )
251 .into())
252 } else if url.ends_with(".xz") {
253 let mut reader = xz2::read::XzDecoder::new(response);
255 let mut buffer = Vec::new();
256 reader.read_to_end(&mut buffer)?;
257 Ok(Some(buffer))
258 } else {
259 Ok(Some(response.bytes()?.to_vec()))
260 }
261}
262
263#[derive(Debug, Clone)]
264pub struct Change {
265 pub binary: String,
266 pub hint: Hint,
267 pub description: String,
268 pub certainty: Certainty,
269}
270
271pub struct OverallResult {
272 pub changes: Vec<Change>,
273}
274
275impl OverallResult {
276 pub fn value(&self) -> i32 {
277 let kinds = self
278 .changes
279 .iter()
280 .map(|x| x.hint.kind().parse().unwrap())
281 .collect::<Vec<_>>();
282 calculate_value(&kinds)
283 }
284}
285
286fn apply_hint_ma_foreign(binary: &mut Binary, _hint: &Hint) -> Option<String> {
287 if binary.multi_arch() != Some(MultiArch::Foreign) {
288 binary.set_multi_arch(Some(MultiArch::Foreign));
289 Some("Add Multi-Arch: foreign.".to_string())
290 } else {
291 None
292 }
293}
294
295fn apply_hint_ma_foreign_lib(binary: &mut Binary, _hint: &Hint) -> Option<String> {
296 if binary.multi_arch() == Some(MultiArch::Foreign) {
297 binary.set_multi_arch(None);
298 Some("Drop Multi-Arch: foreign.".to_string())
299 } else {
300 None
301 }
302}
303
304fn apply_hint_file_conflict(binary: &mut Binary, _hint: &Hint) -> Option<String> {
305 if binary.multi_arch() == Some(MultiArch::Same) {
306 binary.set_multi_arch(None);
307 Some("Drop Multi-Arch: same.".to_string())
308 } else {
309 None
310 }
311}
312
313fn apply_hint_ma_same(binary: &mut Binary, _hint: &Hint) -> Option<String> {
314 if binary.multi_arch() == Some(MultiArch::Same) {
315 return None;
316 }
317 binary.set_multi_arch(Some(MultiArch::Same));
318 Some("Add Multi-Arch: same.".to_string())
319}
320
321fn apply_hint_arch_all(binary: &mut Binary, _hint: &Hint) -> Option<String> {
322 if binary.architecture().as_deref() == Some("all") {
323 return None;
324 }
325 binary.set_architecture(Some("all"));
326 Some("Make package Architecture: all.".to_string())
327}
328
329fn apply_hint_dep_any(binary: &mut Binary, hint: &Hint) -> Option<String> {
330 if let Some((_whole, binary_package, dep)) = regex_captures!(
331 r"(.*) could have its dependency on (.*) annotated with :any",
332 hint.description.as_str()
333 ) {
334 assert_eq!(binary_package, binary.name().unwrap());
335
336 let mut changed = false;
337 if let Some(depends) = binary.depends() {
338 for entry in depends.entries() {
339 for mut r in entry.relations() {
340 if r.name() == dep && r.archqual().as_deref() != Some("any") {
341 r.set_archqual("any");
342 changed = true;
343 }
344 }
345 }
346 if changed {
347 binary.set_depends(Some(&depends));
348 Some(format!("Add :any qualifier for {} dependency.", dep))
349 } else {
350 None
351 }
352 } else {
353 None
354 }
355 } else {
356 log::warn!("Unable to parse dep-any hint: {:?}", hint.description);
357 None
358 }
359}
360
361fn apply_hint_ma_workaround(binary: &mut Binary, hint: &Hint) -> Option<String> {
362 if let Some((_whole, binary_package)) = regex_captures!(
363 r"(.*) should be Architecture: any \+ Multi-Arch: same",
364 hint.description.as_str()
365 ) {
366 assert_eq!(binary_package, binary.name().unwrap());
367 binary.set_multi_arch(Some(MultiArch::Same));
368 binary.set_architecture(Some("any"));
369 Some("Add Multi-Arch: same and set Architecture: any.".to_string())
370 } else {
371 log::warn!("Unable to parse ma-workaround hint: {:?}", hint.description);
372 None
373 }
374}
375
376struct Applier {
377 kind: &'static str,
378 certainty: Certainty,
379 cb: fn(&mut Binary, &Hint) -> Option<String>,
380}
381
382lazy_static! {
383 static ref APPLIERS: Vec<Applier> = vec![
384 Applier {
385 kind: "ma-foreign",
386 certainty: Certainty::Certain,
387 cb: apply_hint_ma_foreign,
388 },
389 Applier {
390 kind: "file-conflict",
391 certainty: Certainty::Certain,
392 cb: apply_hint_file_conflict,
393 },
394 Applier {
395 kind: "ma-foreign-library",
396 certainty: Certainty::Certain,
397 cb: apply_hint_ma_foreign_lib,
398 },
399 Applier {
400 kind: "dep-any",
401 certainty: Certainty::Certain,
402 cb: apply_hint_dep_any,
403 },
404 Applier {
405 kind: "ma-same",
406 certainty: Certainty::Certain,
407 cb: apply_hint_ma_same,
408 },
409 Applier {
410 kind: "arch-all",
411 certainty: Certainty::Possible,
412 cb: apply_hint_arch_all,
413 },
414 Applier {
415 kind: "ma-workaround",
416 certainty: Certainty::Certain,
417 cb: apply_hint_ma_workaround,
418 },
419 ];
420}
421
422fn find_applier(kind: &str) -> Option<&'static Applier> {
423 APPLIERS.iter().find(|x| x.kind == kind)
424}
425
426fn changes_by_description(changes: &[Change]) -> HashMap<String, Vec<String>> {
427 let mut by_description = HashMap::new();
428 for change in changes {
429 by_description
430 .entry(change.description.clone())
431 .or_insert_with(Vec::new)
432 .push(change.binary.clone());
433 }
434 by_description
435}
436
437#[derive(Debug)]
438pub enum OverallError {
439 BrzError(Error),
440 NotDebianPackage(std::path::PathBuf),
441 Other(String),
442 Python(pyo3::PyErr),
443 NoWhoami,
444 NoChanges,
445 GeneratedFile(std::path::PathBuf),
446 FormattingUnpreservable(std::path::PathBuf),
447}
448
449impl From<debian_analyzer::editor::EditorError> for OverallError {
450 fn from(e: debian_analyzer::editor::EditorError) -> Self {
451 match e {
452 debian_analyzer::editor::EditorError::GeneratedFile(p, _) => {
453 OverallError::GeneratedFile(p)
454 }
455 debian_analyzer::editor::EditorError::FormattingUnpreservable(p, _) => {
456 OverallError::FormattingUnpreservable(p)
457 }
458 debian_analyzer::editor::EditorError::BrzError(e) => OverallError::BrzError(e),
459 debian_analyzer::editor::EditorError::IoError(e) => OverallError::Other(e.to_string()),
460 debian_analyzer::editor::EditorError::TemplateError(p, _e) => {
461 OverallError::GeneratedFile(p)
462 }
463 }
464 }
465}
466
467impl std::fmt::Display for OverallError {
468 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
469 match self {
470 OverallError::NotDebianPackage(p) => {
471 write!(f, "{} is not a Debian package.", p.display())
472 }
473 OverallError::GeneratedFile(p) => {
474 write!(f, "Generated file: {}", p.display())
475 }
476 OverallError::FormattingUnpreservable(p) => {
477 write!(f, "Formatting unpreservable: {}", p.display())
478 }
479 OverallError::BrzError(e) => write!(f, "{}", e),
480 OverallError::Python(e) => write!(f, "{}", e),
481 OverallError::NoWhoami => write!(f, "No committer configured."),
482 OverallError::NoChanges => write!(f, "No changes to apply."),
483 OverallError::Other(e) => write!(f, "{}", e),
484 }
485 }
486}
487
488impl std::error::Error for OverallError {}
489
490impl From<Error> for OverallError {
491 fn from(e: Error) -> Self {
492 match e {
493 Error::PointlessCommit => OverallError::NoChanges,
494 Error::NoWhoami => OverallError::NoWhoami,
495 Error::Other(e) => OverallError::Python(e),
496 e => OverallError::BrzError(e),
497 }
498 }
499}
500
501impl From<ChangelogError> for OverallError {
502 fn from(e: ChangelogError) -> Self {
503 match e {
504 ChangelogError::NotDebianPackage(p) => OverallError::NotDebianPackage(p),
505 ChangelogError::Python(e) => OverallError::Other(e.to_string()),
506 }
507 }
508}
509
510pub fn apply_multiarch_hints(
511 local_tree: &WorkingTree,
512 subpath: &std::path::Path,
513 hints: &HashMap<&str, Vec<&Hint>>,
514 minimum_certainty: Option<Certainty>,
515 committer: Option<String>,
516 dirty_tracker: Option<&mut DirtyTreeTracker>,
517 update_changelog: bool,
518 _allow_reformatting: Option<bool>,
519) -> Result<OverallResult, OverallError> {
520 let minimum_certainty = minimum_certainty.unwrap_or(Certainty::Certain);
521 let basis_tree = local_tree.basis_tree().unwrap();
522 let (changes, _tree_changes, mut specific_files) = match apply_or_revert(
523 local_tree,
524 subpath,
525 &basis_tree,
526 dirty_tracker,
527 |path| -> Result<Vec<Change>, OverallError> {
528 let mut changes: Vec<Change> = vec![];
529
530 let control_path = path.join("debian/control");
531
532 let editor = match TemplatedControlEditor::open(control_path.as_path()) {
533 Ok(editor) => editor,
534 Err(e) => {
535 return Err(OverallError::Other(e.to_string()));
536 }
537 };
538
539 for mut binary in editor.binaries() {
540 let package = binary.name().unwrap();
541 if let Some(hints) = hints.get(package.as_str()) {
542 for hint in hints {
543 let kind = hint.kind();
544 let applier = match find_applier(kind) {
545 Some(applier) => applier,
546 None => {
547 log::warn!("Unknown hint kind: {}", kind);
548 continue;
549 }
550 };
551 if !certainty_sufficient(applier.certainty, Some(minimum_certainty)) {
552 continue;
553 }
554 if let Some(description) = (applier.cb)(&mut binary, hint) {
555 changes.push(Change {
556 binary: binary.name().unwrap(),
557 hint: (*hint).clone(),
558 description,
559 certainty: applier.certainty,
560 });
561 }
562 }
563 }
564 }
565
566 editor.commit()?;
567 Ok(changes)
568 },
569 ) {
570 Ok(r) => r,
571 Err(ApplyError::NoChanges(_)) => return Err(OverallError::NoChanges),
572 Err(ApplyError::BrzError(e)) => return Err(OverallError::BrzError(e)),
573 Err(ApplyError::CallbackError(_)) => panic!("Unexpected callback error"),
574 };
575
576 let by_description = changes_by_description(changes.as_slice());
577 let mut overall_description = vec!["Apply multi-arch hints.\n".to_string()];
578 for (description, mut binaries) in by_description {
579 binaries.sort();
580 overall_description.push(format!(" + {}: {}\n", binaries.join(", "), description));
581 }
582
583 let changelog_path = subpath.join("debian/changelog");
584
585 if update_changelog {
586 add_changelog_entry(
587 local_tree,
588 changelog_path.as_path(),
589 overall_description
590 .iter()
591 .map(|x| x.as_str())
592 .collect::<Vec<_>>()
593 .as_slice(),
594 )?;
595 if let Some(specific_files) = specific_files.as_mut() {
596 specific_files.push(changelog_path);
597 }
598 }
599
600 overall_description.push("\n".to_string());
601 overall_description.push("Changes-By: apply-multiarch-hints\n".to_string());
602
603 let committer = committer.unwrap_or_else(|| get_committer(local_tree));
604
605 let specific_files_ref = specific_files
606 .as_ref()
607 .map(|x| x.iter().map(|x| x.as_path()).collect::<Vec<_>>());
608
609 let mut builder = local_tree
610 .build_commit()
611 .message(overall_description.concat().as_str())
612 .allow_pointless(false)
613 .committer(&committer);
614
615 if let Some(specific_files) = specific_files_ref.as_deref() {
616 builder = builder.specific_files(specific_files);
617 }
618
619 builder.commit()?;
620
621 Ok(OverallResult { changes })
622}