use breezyshim::dirty_tracker::DirtyTreeTracker;
use breezyshim::error::Error;
use breezyshim::tree::WorkingTree;
use breezyshim::workingtree::GenericWorkingTree;
use debian_analyzer::{
add_changelog_entry, apply_or_revert, certainty_sufficient, get_committer, ApplyError,
ChangelogError,
};
use debian_control::fields::MultiArch;
use debian_workspace::action::{Action, ActionPlan, Deb822Action, ParagraphSelector};
use debian_workspace::appliers::apply_actions;
use debian_workspace::workspace::Workspace;
use debversion::Version;
use lazy_regex::regex_captures;
use lazy_static::lazy_static;
use reqwest::blocking::Client;
use serde::Deserialize;
use serde_yaml::from_value;
use std::collections::HashMap;
use std::fs;
use std::io::Read;
use std::io::Write;
use std::path::Path;
use std::time::SystemTime;
pub use debian_analyzer::Certainty;
pub const MULTIARCH_HINTS_URL: &str = "https://dedup.debian.net/static/multiarch-hints.yaml.xz";
const USER_AGENT: &str = concat!("apply-multiarch-hints/", env!("CARGO_PKG_VERSION"));
const DEFAULT_VALUE_MULTIARCH_HINT: i32 = 100;
#[derive(Debug, Clone, Copy, std::hash::Hash, PartialEq, Eq)]
pub enum HintKind {
MaForeign,
FileConflict,
MaForeignLibrary,
DepAny,
MaSame,
ArchAll,
MaWorkaround,
}
impl std::str::FromStr for HintKind {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"ma-foreign" => Ok(HintKind::MaForeign),
"file-conflict" => Ok(HintKind::FileConflict),
"ma-foreign-library" => Ok(HintKind::MaForeignLibrary),
"dep-any" => Ok(HintKind::DepAny),
"ma-same" => Ok(HintKind::MaSame),
"arch-all" => Ok(HintKind::ArchAll),
"ma-workaround" => Ok(HintKind::MaWorkaround),
_ => Err(format!("Invalid hint kind: {:?}", s)),
}
}
}
fn hint_value(hint: HintKind) -> i32 {
match hint {
HintKind::MaForeign => 20,
HintKind::FileConflict => 50,
HintKind::MaForeignLibrary => 20,
HintKind::DepAny => 20,
HintKind::MaSame => 20,
HintKind::ArchAll => 20,
HintKind::MaWorkaround => 20,
}
}
pub fn calculate_value(hints: &[HintKind]) -> i32 {
hints.iter().map(|hint| hint_value(*hint)).sum::<i32>() + DEFAULT_VALUE_MULTIARCH_HINT
}
fn format_system_time(system_time: SystemTime) -> String {
let datetime: chrono::DateTime<chrono::Utc> = system_time.into();
datetime.format("%a, %d %b %Y %H:%M:%S GMT").to_string()
}
#[derive(Debug, Deserialize, PartialEq, Eq, Ord, PartialOrd, Clone, Copy)]
pub enum Severity {
#[serde(rename = "low")]
Low,
#[serde(rename = "normal")]
Normal,
#[serde(rename = "high")]
High,
}
fn deserialize_severity<'de, D>(deserializer: D) -> Result<Severity, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.as_str() {
"low" => Ok(Severity::Low),
"normal" => Ok(Severity::Normal),
"high" => Ok(Severity::High),
_ => Err(serde::de::Error::custom(format!(
"Invalid severity: {:?}",
s
))),
}
}
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
pub struct Hint {
pub binary: String,
pub description: String,
#[serde(default)]
pub source: Option<String>,
pub link: String,
#[serde(deserialize_with = "deserialize_severity")]
pub severity: Severity,
pub version: Option<Version>,
}
impl Hint {
pub fn kind(&self) -> &str {
self.link.split('#').next_back().unwrap()
}
}
pub fn multiarch_hints_by_source(hints: &[Hint]) -> HashMap<&str, Vec<&Hint>> {
let mut map = HashMap::new();
for hint in hints {
if let Some(source) = hint.source.as_deref() {
map.entry(source).or_insert_with(Vec::new).push(hint);
}
}
map
}
pub fn multiarch_hints_by_binary(hints: &[Hint]) -> HashMap<&str, Vec<&Hint>> {
let mut map = HashMap::new();
for hint in hints {
map.entry(hint.binary.as_str())
.or_insert_with(Vec::new)
.push(hint);
}
map
}
pub fn parse_multiarch_hints(f: &[u8]) -> Result<Vec<Hint>, serde_yaml::Error> {
let data = serde_yaml::from_slice::<serde_yaml::Value>(f)?;
if let Some(format) = data["format"].as_str() {
if format != "multiarch-hints-1.0" {
return Err(serde::de::Error::custom(format!(
"Invalid format: {:?}",
format
)));
}
} else {
return Err(serde::de::Error::custom("Missing format"));
}
from_value(data["hints"].clone())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_some_entries() {
let hints = parse_multiarch_hints(
r#"format: multiarch-hints-1.0
hints:
- binary: coinor-libcoinmp-dev
description: coinor-libcoinmp-dev conflicts on ...
link: https://wiki.debian.org/MultiArch/Hints#file-conflict
severity: high
source: coinmp
version: 1.8.3-2+b11
"#
.as_bytes(),
)
.unwrap();
assert_eq!(
hints,
vec![Hint {
binary: "coinor-libcoinmp-dev".to_string(),
description: "coinor-libcoinmp-dev conflicts on ...".to_string(),
link: "https://wiki.debian.org/MultiArch/Hints#file-conflict".to_string(),
severity: Severity::High,
version: Some("1.8.3-2+b11".parse().unwrap()),
source: Some("coinmp".to_string()),
}]
);
}
#[test]
fn test_missing_source() {
let hints = parse_multiarch_hints(
r#"format: multiarch-hints-1.0
hints:
- binary: somepkg
description: some description
link: https://wiki.debian.org/MultiArch/Hints#file-conflict
severity: high
"#
.as_bytes(),
)
.unwrap();
assert_eq!(
hints,
vec![Hint {
binary: "somepkg".to_string(),
description: "some description".to_string(),
link: "https://wiki.debian.org/MultiArch/Hints#file-conflict".to_string(),
severity: Severity::High,
version: None,
source: None,
}]
);
}
#[test]
fn test_invalid_header() {
let hints = parse_multiarch_hints(
r#"\
format: blah
"#
.as_bytes(),
);
assert!(hints.is_err());
}
fn make_hint(binary: &str, kind: &str, description: &str) -> Hint {
Hint {
binary: binary.to_string(),
description: description.to_string(),
link: format!("https://wiki.debian.org/MultiArch/Hints#{}", kind),
severity: Severity::Normal,
version: None,
source: Some("src".to_string()),
}
}
fn setup_ws(
control: &str,
) -> (
tempfile::TempDir,
debian_workspace::fs_workspace::FsWorkspace,
) {
let tmp = tempfile::TempDir::new().unwrap();
let debian = tmp.path().join("debian");
std::fs::create_dir_all(&debian).unwrap();
std::fs::write(debian.join("control"), control).unwrap();
let ws = debian_workspace::fs_workspace::FsWorkspace::new(
tmp.path(),
Some("src".into()),
Some("1.0".parse().unwrap()),
);
(tmp, ws)
}
fn detect_one(
ws: &debian_workspace::fs_workspace::FsWorkspace,
hints_list: &[Hint],
) -> Vec<(Change, ActionPlan)> {
let by_binary = multiarch_hints_by_binary(hints_list);
detect_multiarch_hints(ws, &by_binary, Certainty::Possible).unwrap()
}
fn apply_and_read(
ws: &debian_workspace::fs_workspace::FsWorkspace,
plans: &[ActionPlan],
) -> String {
let actions: Vec<_> = plans
.iter()
.flat_map(|p| p.actions.iter().cloned())
.collect();
debian_workspace::appliers::apply_actions(ws.base_path(), &actions).unwrap();
std::fs::read_to_string(ws.base_path().join("debian/control")).unwrap()
}
#[test]
fn detect_ma_foreign() {
let (_tmp, ws) = setup_ws("Source: src\n\nPackage: foo\nArchitecture: any\n");
let hints = vec![make_hint("foo", "ma-foreign", "foo could be MA: foreign")];
let results = detect_one(&ws, &hints);
assert_eq!(results.len(), 1);
assert_eq!(results[0].0.binary, "foo");
assert_eq!(results[0].0.description, "Add Multi-Arch: foreign.");
let control = apply_and_read(
&ws,
&results.iter().map(|(_, p)| p.clone()).collect::<Vec<_>>(),
);
assert!(control.contains("Multi-Arch: foreign"), "got: {}", control);
}
#[test]
fn detect_ma_foreign_noop_when_already_set() {
let (_tmp, ws) =
setup_ws("Source: src\n\nPackage: foo\nArchitecture: any\nMulti-Arch: foreign\n");
let hints = vec![make_hint("foo", "ma-foreign", "foo could be MA: foreign")];
let results = detect_one(&ws, &hints);
assert_eq!(results.len(), 0);
}
#[test]
fn detect_ma_foreign_library() {
let (_tmp, ws) =
setup_ws("Source: src\n\nPackage: foo\nArchitecture: any\nMulti-Arch: foreign\n");
let hints = vec![make_hint(
"foo",
"ma-foreign-library",
"foo should not be MA: foreign",
)];
let results = detect_one(&ws, &hints);
assert_eq!(results.len(), 1);
assert_eq!(results[0].0.description, "Drop Multi-Arch: foreign.");
let control = apply_and_read(
&ws,
&results.iter().map(|(_, p)| p.clone()).collect::<Vec<_>>(),
);
assert!(!control.contains("Multi-Arch"), "got: {}", control);
}
#[test]
fn detect_file_conflict() {
let (_tmp, ws) =
setup_ws("Source: src\n\nPackage: foo\nArchitecture: any\nMulti-Arch: same\n");
let hints = vec![make_hint("foo", "file-conflict", "foo conflicts")];
let results = detect_one(&ws, &hints);
assert_eq!(results.len(), 1);
assert_eq!(results[0].0.description, "Drop Multi-Arch: same.");
let control = apply_and_read(
&ws,
&results.iter().map(|(_, p)| p.clone()).collect::<Vec<_>>(),
);
assert!(!control.contains("Multi-Arch"), "got: {}", control);
}
#[test]
fn detect_ma_same() {
let (_tmp, ws) = setup_ws("Source: src\n\nPackage: foo\nArchitecture: any\n");
let hints = vec![make_hint("foo", "ma-same", "foo should be MA: same")];
let results = detect_one(&ws, &hints);
assert_eq!(results.len(), 1);
assert_eq!(results[0].0.description, "Add Multi-Arch: same.");
let control = apply_and_read(
&ws,
&results.iter().map(|(_, p)| p.clone()).collect::<Vec<_>>(),
);
assert!(control.contains("Multi-Arch: same"), "got: {}", control);
}
#[test]
fn detect_arch_all() {
let (_tmp, ws) = setup_ws("Source: src\n\nPackage: foo\nArchitecture: any\n");
let hints = vec![make_hint("foo", "arch-all", "foo should be arch:all")];
let results = detect_one(&ws, &hints);
assert_eq!(results.len(), 1);
assert_eq!(results[0].0.description, "Make package Architecture: all.");
assert_eq!(results[0].0.certainty, Certainty::Possible);
let control = apply_and_read(
&ws,
&results.iter().map(|(_, p)| p.clone()).collect::<Vec<_>>(),
);
assert!(control.contains("Architecture: all"), "got: {}", control);
}
#[test]
fn detect_arch_all_noop_when_already_all() {
let (_tmp, ws) = setup_ws("Source: src\n\nPackage: foo\nArchitecture: all\n");
let hints = vec![make_hint("foo", "arch-all", "foo should be arch:all")];
let results = detect_one(&ws, &hints);
assert_eq!(results.len(), 0);
}
#[test]
fn detect_dep_any() {
let (_tmp, ws) =
setup_ws("Source: src\n\nPackage: foo\nArchitecture: any\nDepends: libbar (>= 1.0)\n");
let hints = vec![make_hint(
"foo",
"dep-any",
"foo could have its dependency on libbar annotated with :any",
)];
let results = detect_one(&ws, &hints);
assert_eq!(results.len(), 1);
assert_eq!(
results[0].0.description,
"Add :any qualifier for libbar dependency."
);
let control = apply_and_read(
&ws,
&results.iter().map(|(_, p)| p.clone()).collect::<Vec<_>>(),
);
assert!(control.contains("libbar:any"), "got: {}", control);
}
#[test]
fn detect_dep_any_noop_when_already_annotated() {
let (_tmp, ws) = setup_ws(
"Source: src\n\nPackage: foo\nArchitecture: any\nDepends: libbar:any (>= 1.0)\n",
);
let hints = vec![make_hint(
"foo",
"dep-any",
"foo could have its dependency on libbar annotated with :any",
)];
let results = detect_one(&ws, &hints);
assert_eq!(results.len(), 0);
}
#[test]
fn detect_ma_workaround() {
let (_tmp, ws) = setup_ws("Source: src\n\nPackage: foo\nArchitecture: all\n");
let hints = vec![make_hint(
"foo",
"ma-workaround",
"foo should be Architecture: any + Multi-Arch: same",
)];
let results = detect_one(&ws, &hints);
assert_eq!(results.len(), 1);
assert_eq!(
results[0].0.description,
"Add Multi-Arch: same and set Architecture: any."
);
let control = apply_and_read(
&ws,
&results.iter().map(|(_, p)| p.clone()).collect::<Vec<_>>(),
);
assert!(control.contains("Multi-Arch: same"), "got: {}", control);
assert!(control.contains("Architecture: any"), "got: {}", control);
}
#[test]
fn detect_skips_unknown_binary() {
let (_tmp, ws) = setup_ws("Source: src\n\nPackage: foo\nArchitecture: any\n");
let hints = vec![make_hint("bar", "ma-foreign", "bar could be MA: foreign")];
let results = detect_one(&ws, &hints);
assert_eq!(results.len(), 0);
}
#[test]
fn detect_skips_below_minimum_certainty() {
let (_tmp, ws) = setup_ws("Source: src\n\nPackage: foo\nArchitecture: any\n");
let hints = vec![make_hint("foo", "arch-all", "foo should be arch:all")];
let by_binary = multiarch_hints_by_binary(&hints);
let results = detect_multiarch_hints(&ws, &by_binary, Certainty::Certain).unwrap();
assert_eq!(results.len(), 0);
}
#[test]
fn detect_no_control_file() {
let tmp = tempfile::TempDir::new().unwrap();
let ws = debian_workspace::fs_workspace::FsWorkspace::new(
tmp.path(),
Some("src".into()),
Some("1.0".parse().unwrap()),
);
let hints = vec![make_hint("foo", "ma-foreign", "foo could be MA: foreign")];
let by_binary = multiarch_hints_by_binary(&hints);
let results = detect_multiarch_hints(&ws, &by_binary, Certainty::Possible).unwrap();
assert_eq!(results.len(), 0);
}
}
pub fn cache_dir() -> Option<std::path::PathBuf> {
let cache_home = if let Ok(xdg_cache_home) = std::env::var("XDG_CACHE_HOME") {
Path::new(&xdg_cache_home).to_path_buf()
} else if let Ok(home) = std::env::var("HOME") {
Path::new(&home).join(".cache")
} else {
return None;
};
Some(cache_home.join("lintian-brush"))
}
pub fn cache_file_path() -> Option<std::path::PathBuf> {
cache_dir().map(|d| d.join("multiarch-hints.yml"))
}
pub fn cache_download_multiarch_hints(
url: Option<&str>,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let Some(local_hints_path) = cache_file_path() else {
log::warn!("Unable to find cache directory, not caching");
return download_multiarch_hints(url, None)?
.ok_or_else(|| "Expected download data but got None".into());
};
if let Some(parent) = local_hints_path.parent() {
fs::create_dir_all(parent)?;
}
let last_modified = match fs::metadata(&local_hints_path) {
Ok(metadata) => Some(metadata.modified()?),
Err(_) => None,
};
match download_multiarch_hints(url, last_modified) {
Ok(None) => {
let mut buffer = Vec::new();
std::fs::File::open(&local_hints_path)?.read_to_end(&mut buffer)?;
Ok(buffer)
}
Ok(Some(buffer)) => {
fs::File::create(&local_hints_path)?.write_all(&buffer)?;
Ok(buffer)
}
Err(e) => Err(e),
}
}
pub fn download_multiarch_hints(
url: Option<&str>,
since: Option<SystemTime>,
) -> Result<Option<Vec<u8>>, Box<dyn std::error::Error>> {
let url = url.unwrap_or(MULTIARCH_HINTS_URL);
let client = Client::builder().user_agent(USER_AGENT).build()?;
let mut request = client.get(url).header("Accept-Encoding", "identity");
if let Some(since) = since {
request = request.header("If-Modified-Since", format_system_time(since));
}
let response = request.send()?;
if response.status() == reqwest::StatusCode::NOT_MODIFIED {
Ok(None)
} else if response.status() != reqwest::StatusCode::OK {
Err(format!(
"Unable to download multiarch hints: {:?}",
response.status()
)
.into())
} else if url.ends_with(".xz") {
let mut reader = xz2::read::XzDecoder::new(response);
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer)?;
Ok(Some(buffer))
} else {
Ok(Some(response.bytes()?.to_vec()))
}
}
#[cfg(feature = "async")]
pub async fn download_multiarch_hints_async(
url: Option<&str>,
since: Option<SystemTime>,
) -> Result<Option<Vec<u8>>, Box<dyn std::error::Error + Send + Sync>> {
let url = url.unwrap_or(MULTIARCH_HINTS_URL).to_string();
let client = reqwest::Client::builder().user_agent(USER_AGENT).build()?;
let mut request = client.get(&url).header("Accept-Encoding", "identity");
if let Some(since) = since {
request = request.header("If-Modified-Since", format_system_time(since));
}
let response = request.send().await?;
if response.status() == reqwest::StatusCode::NOT_MODIFIED {
return Ok(None);
} else if response.status() != reqwest::StatusCode::OK {
return Err(format!(
"Unable to download multiarch hints: {:?}",
response.status()
)
.into());
}
let bytes = response.bytes().await?.to_vec();
if url.ends_with(".xz") {
let decoded = tokio::task::spawn_blocking(move || {
use std::io::Read;
let mut reader = xz2::read::XzDecoder::new(&bytes[..]);
let mut out = Vec::new();
reader.read_to_end(&mut out)?;
Ok::<Vec<u8>, std::io::Error>(out)
})
.await
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { Box::new(e) })??;
Ok(Some(decoded))
} else {
Ok(Some(bytes))
}
}
#[cfg(feature = "async")]
pub async fn cache_download_multiarch_hints_async(
url: Option<&str>,
) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
let Some(local_hints_path) = cache_file_path() else {
log::warn!("Unable to find cache directory, not caching");
return download_multiarch_hints_async(url, None)
.await?
.ok_or_else(|| "Expected download data but got None".into());
};
if let Some(parent) = local_hints_path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
let last_modified = match tokio::fs::metadata(&local_hints_path).await {
Ok(metadata) => Some(metadata.modified()?),
Err(_) => None,
};
match download_multiarch_hints_async(url, last_modified).await {
Ok(None) => Ok(tokio::fs::read(&local_hints_path).await?),
Ok(Some(buffer)) => {
tokio::fs::write(&local_hints_path, &buffer).await?;
Ok(buffer)
}
Err(e) => Err(e),
}
}
#[derive(Debug, Clone)]
pub struct Change {
pub binary: String,
pub hint: Hint,
pub description: String,
pub certainty: Certainty,
}
pub struct OverallResult {
pub changes: Vec<Change>,
}
impl OverallResult {
pub fn value(&self) -> i32 {
let kinds = self
.changes
.iter()
.map(|x| x.hint.kind().parse().unwrap())
.collect::<Vec<_>>();
calculate_value(&kinds)
}
}
fn control_file() -> std::path::PathBuf {
std::path::PathBuf::from("debian/control")
}
fn detect_hint_ma_foreign(
binary: &debian_control::lossless::control::Binary,
_hint: &Hint,
) -> Option<(String, Vec<Action>)> {
if binary.multi_arch() == Some(MultiArch::Foreign) {
return None;
}
let pkg = binary.name()?;
Some((
"Add Multi-Arch: foreign.".to_string(),
vec![Action::Deb822(Deb822Action::SetField {
file: control_file(),
paragraph: ParagraphSelector::Binary { package: pkg },
field: "Multi-Arch".to_string(),
value: "foreign".to_string(),
})],
))
}
fn detect_hint_ma_foreign_lib(
binary: &debian_control::lossless::control::Binary,
_hint: &Hint,
) -> Option<(String, Vec<Action>)> {
if binary.multi_arch() != Some(MultiArch::Foreign) {
return None;
}
let pkg = binary.name()?;
Some((
"Drop Multi-Arch: foreign.".to_string(),
vec![Action::Deb822(Deb822Action::RemoveField {
file: control_file(),
paragraph: ParagraphSelector::Binary { package: pkg },
field: "Multi-Arch".to_string(),
})],
))
}
fn detect_hint_file_conflict(
binary: &debian_control::lossless::control::Binary,
_hint: &Hint,
) -> Option<(String, Vec<Action>)> {
if binary.multi_arch() != Some(MultiArch::Same) {
return None;
}
let pkg = binary.name()?;
Some((
"Drop Multi-Arch: same.".to_string(),
vec![Action::Deb822(Deb822Action::RemoveField {
file: control_file(),
paragraph: ParagraphSelector::Binary { package: pkg },
field: "Multi-Arch".to_string(),
})],
))
}
fn detect_hint_ma_same(
binary: &debian_control::lossless::control::Binary,
_hint: &Hint,
) -> Option<(String, Vec<Action>)> {
if binary.multi_arch() == Some(MultiArch::Same) {
return None;
}
let pkg = binary.name()?;
Some((
"Add Multi-Arch: same.".to_string(),
vec![Action::Deb822(Deb822Action::SetField {
file: control_file(),
paragraph: ParagraphSelector::Binary { package: pkg },
field: "Multi-Arch".to_string(),
value: "same".to_string(),
})],
))
}
fn detect_hint_arch_all(
binary: &debian_control::lossless::control::Binary,
_hint: &Hint,
) -> Option<(String, Vec<Action>)> {
if binary.architecture().as_deref() == Some("all") {
return None;
}
let pkg = binary.name()?;
Some((
"Make package Architecture: all.".to_string(),
vec![Action::Deb822(Deb822Action::SetField {
file: control_file(),
paragraph: ParagraphSelector::Binary { package: pkg },
field: "Architecture".to_string(),
value: "all".to_string(),
})],
))
}
fn detect_hint_dep_any(
binary: &debian_control::lossless::control::Binary,
hint: &Hint,
) -> Option<(String, Vec<Action>)> {
let Some((_whole, binary_package, dep)) = regex_captures!(
r"(.*) could have its dependency on (.*) annotated with :any",
hint.description.as_str()
) else {
log::warn!("Unable to parse dep-any hint: {:?}", hint.description);
return None;
};
assert_eq!(binary_package, binary.name().unwrap());
let depends = binary.depends()?;
let entry_text = depends.entries().find_map(|entry| {
entry.relations().find_map(|r| {
if r.try_name().as_deref() == Some(dep) && r.archqual().as_deref() != Some("any") {
Some(entry.to_string().replacen(dep, &format!("{}:any", dep), 1))
} else {
None
}
})
})?;
let pkg = binary.name()?;
Some((
format!("Add :any qualifier for {} dependency.", dep),
vec![Action::Deb822(Deb822Action::ReplaceRelation {
file: control_file(),
paragraph: ParagraphSelector::Binary { package: pkg },
field: "Depends".to_string(),
from_package: dep.to_string(),
to_entry: entry_text,
})],
))
}
fn detect_hint_ma_workaround(
binary: &debian_control::lossless::control::Binary,
hint: &Hint,
) -> Option<(String, Vec<Action>)> {
let Some((_whole, binary_package)) = regex_captures!(
r"(.*) should be Architecture: any \+ Multi-Arch: same",
hint.description.as_str()
) else {
log::warn!("Unable to parse ma-workaround hint: {:?}", hint.description);
return None;
};
assert_eq!(binary_package, binary.name().unwrap());
let pkg = binary.name()?;
Some((
"Add Multi-Arch: same and set Architecture: any.".to_string(),
vec![
Action::Deb822(Deb822Action::SetField {
file: control_file(),
paragraph: ParagraphSelector::Binary {
package: pkg.clone(),
},
field: "Multi-Arch".to_string(),
value: "same".to_string(),
}),
Action::Deb822(Deb822Action::SetField {
file: control_file(),
paragraph: ParagraphSelector::Binary { package: pkg },
field: "Architecture".to_string(),
value: "any".to_string(),
}),
],
))
}
type DetectorFn =
fn(&debian_control::lossless::control::Binary, &Hint) -> Option<(String, Vec<Action>)>;
struct Detector {
kind: &'static str,
certainty: Certainty,
cb: DetectorFn,
}
lazy_static! {
static ref DETECTORS: Vec<Detector> = vec![
Detector {
kind: "ma-foreign",
certainty: Certainty::Certain,
cb: detect_hint_ma_foreign,
},
Detector {
kind: "file-conflict",
certainty: Certainty::Certain,
cb: detect_hint_file_conflict,
},
Detector {
kind: "ma-foreign-library",
certainty: Certainty::Certain,
cb: detect_hint_ma_foreign_lib,
},
Detector {
kind: "dep-any",
certainty: Certainty::Certain,
cb: detect_hint_dep_any,
},
Detector {
kind: "ma-same",
certainty: Certainty::Certain,
cb: detect_hint_ma_same,
},
Detector {
kind: "arch-all",
certainty: Certainty::Possible,
cb: detect_hint_arch_all,
},
Detector {
kind: "ma-workaround",
certainty: Certainty::Certain,
cb: detect_hint_ma_workaround,
},
];
}
fn find_detector(kind: &str) -> Option<&'static Detector> {
DETECTORS.iter().find(|x| x.kind == kind)
}
pub fn detect_multiarch_hints(
ws: &dyn Workspace,
hints: &HashMap<&str, Vec<&Hint>>,
minimum_certainty: Certainty,
) -> Result<Vec<(Change, ActionPlan)>, debian_workspace::Error> {
let control = match ws.parsed_control() {
Ok(c) => c,
Err(debian_workspace::Error::NotFound) => return Ok(Vec::new()),
Err(e) => return Err(e),
};
let mut results = Vec::new();
for binary in control.binaries() {
let Some(package) = binary.name() else {
continue;
};
let Some(package_hints) = hints.get(package.as_str()) else {
continue;
};
for hint in package_hints {
let kind = hint.kind();
let detector = match find_detector(kind) {
Some(d) => d,
None => {
log::warn!("Unknown hint kind: {}", kind);
continue;
}
};
if !certainty_sufficient(detector.certainty, Some(minimum_certainty)) {
continue;
}
if let Some((description, actions)) = (detector.cb)(&binary, hint) {
results.push((
Change {
binary: package.clone(),
hint: (*hint).clone(),
description: description.clone(),
certainty: detector.certainty,
},
ActionPlan {
label: description,
opinionated: false,
actions,
},
));
}
}
}
Ok(results)
}
fn changes_by_description(changes: &[Change]) -> HashMap<String, Vec<String>> {
let mut by_description: HashMap<String, Vec<String>> = HashMap::new();
for change in changes {
by_description
.entry(change.description.clone())
.or_default()
.push(change.binary.clone());
}
by_description
}
#[derive(Debug)]
pub enum OverallError {
BrzError(Error),
NotDebianPackage(std::path::PathBuf),
Other(String),
NoWhoami,
NoChanges,
GeneratedFile(std::path::PathBuf),
FormattingUnpreservable(std::path::PathBuf),
}
impl From<debian_analyzer::editor::EditorError> for OverallError {
fn from(e: debian_analyzer::editor::EditorError) -> Self {
match e {
debian_analyzer::editor::EditorError::GeneratedFile(p, _) => {
OverallError::GeneratedFile(p)
}
debian_analyzer::editor::EditorError::FormattingUnpreservable(p, _) => {
OverallError::FormattingUnpreservable(p)
}
debian_analyzer::editor::EditorError::BrzError(e) => OverallError::BrzError(e),
debian_analyzer::editor::EditorError::IoError(e) => OverallError::Other(e.to_string()),
debian_analyzer::editor::EditorError::TemplateError(p, _e) => {
OverallError::GeneratedFile(p)
}
}
}
}
impl std::fmt::Display for OverallError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
OverallError::NotDebianPackage(p) => {
write!(f, "{} is not a Debian package.", p.display())
}
OverallError::GeneratedFile(p) => {
write!(f, "Generated file: {}", p.display())
}
OverallError::FormattingUnpreservable(p) => {
write!(f, "Formatting unpreservable: {}", p.display())
}
OverallError::BrzError(e) => write!(f, "{}", e),
OverallError::NoWhoami => write!(f, "No committer configured."),
OverallError::NoChanges => write!(f, "No changes to apply."),
OverallError::Other(e) => write!(f, "{}", e),
}
}
}
impl std::error::Error for OverallError {}
impl From<Error> for OverallError {
fn from(e: Error) -> Self {
match e {
Error::PointlessCommit => OverallError::NoChanges,
Error::NoWhoami => OverallError::NoWhoami,
Error::Other(e) => OverallError::Other(e.to_string()),
e => OverallError::BrzError(e),
}
}
}
impl From<ChangelogError> for OverallError {
fn from(e: ChangelogError) -> Self {
match e {
ChangelogError::NotDebianPackage(p) => OverallError::NotDebianPackage(p),
ChangelogError::Python(e) => OverallError::Other(e.to_string()),
}
}
}
#[derive(Debug, Clone)]
pub struct ApplyMultiarchHintsConfig {
pub minimum_certainty: Option<Certainty>,
pub committer: Option<String>,
pub update_changelog: bool,
pub allow_reformatting: Option<bool>,
}
impl Default for ApplyMultiarchHintsConfig {
fn default() -> Self {
Self {
minimum_certainty: None,
committer: None,
update_changelog: true,
allow_reformatting: None,
}
}
}
#[allow(clippy::result_large_err)]
pub fn apply_multiarch_hints(
local_tree: &GenericWorkingTree,
subpath: &std::path::Path,
hints: &HashMap<&str, Vec<&Hint>>,
dirty_tracker: Option<&mut DirtyTreeTracker>,
config: &ApplyMultiarchHintsConfig,
) -> Result<OverallResult, OverallError> {
let minimum_certainty = config.minimum_certainty.unwrap_or(Certainty::Certain);
let basis_tree = local_tree.basis_tree().map_err(OverallError::BrzError)?;
let (changes, _tree_changes, mut specific_files) = match apply_or_revert(
local_tree,
subpath,
&basis_tree,
dirty_tracker,
|path| -> Result<Vec<Change>, OverallError> {
let ws = debian_workspace::fs_workspace::FsWorkspace::new(path, None, None);
let detected = detect_multiarch_hints(&ws, hints, minimum_certainty)
.map_err(|e| OverallError::Other(e.to_string()))?;
if detected.is_empty() {
return Ok(Vec::new());
}
let all_actions: Vec<_> = detected
.iter()
.flat_map(|(_, plan)| plan.actions.iter().cloned())
.collect();
apply_actions(path, &all_actions).map_err(|e| OverallError::Other(e.to_string()))?;
Ok(detected.into_iter().map(|(change, _)| change).collect())
},
) {
Ok(r) => r,
Err(ApplyError::NoChanges(_)) => return Err(OverallError::NoChanges),
Err(ApplyError::BrzError(e)) => return Err(OverallError::BrzError(e)),
Err(ApplyError::CallbackError(_)) => panic!("Unexpected callback error"),
};
let by_description = changes_by_description(changes.as_slice());
let mut overall_description = vec!["Apply multi-arch hints.\n".to_string()];
for (description, mut binaries) in by_description {
binaries.sort();
overall_description.push(format!(" + {}: {}\n", binaries.join(", "), description));
}
let changelog_path = subpath.join("debian/changelog");
if config.update_changelog {
add_changelog_entry(
local_tree,
changelog_path.as_path(),
overall_description
.iter()
.map(|x| x.as_str())
.collect::<Vec<_>>()
.as_slice(),
)?;
if let Some(specific_files) = specific_files.as_mut() {
specific_files.push(changelog_path);
}
}
overall_description.push("\n".to_string());
overall_description.push("Changes-By: apply-multiarch-hints\n".to_string());
let committer = config
.committer
.clone()
.unwrap_or_else(|| get_committer(local_tree));
let specific_files_ref = specific_files
.as_ref()
.map(|x| x.iter().map(|x| x.as_path()).collect::<Vec<_>>());
let mut builder = local_tree
.build_commit()
.message(overall_description.concat().as_str())
.allow_pointless(false)
.committer(&committer);
if let Some(specific_files) = specific_files_ref.as_deref() {
builder = builder.specific_files(specific_files);
}
builder.commit()?;
Ok(OverallResult { changes })
}