multiarch_hints/
lib.rs

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        // It would be nicer if there was a content-type, but there isn't :-(
254        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}