debian_workbench/
debhelper.rs1use debversion::Version;
3use std::path::Path;
4
5fn parse_debhelper_compat(s: &str) -> Option<u8> {
7 s.split_once('#').map_or(s, |s| s.0).trim().parse().ok()
8}
9
10pub fn read_debhelper_compat_file(path: &Path) -> Result<Option<u8>, std::io::Error> {
15 match std::fs::read_to_string(path) {
16 Ok(content) => Ok(parse_debhelper_compat(&content)),
17 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
18 Err(e) => Err(e),
19 }
20}
21
22pub fn get_debhelper_compat_level_from_control(control: &debian_control::Control) -> Option<u8> {
30 let source = control.source()?;
31
32 if let Some(dh_compat) = source.as_deb822().get("X-DH-Compat") {
33 return parse_debhelper_compat(dh_compat.as_str());
34 }
35
36 let build_depends = source.build_depends()?;
37
38 let rels = build_depends
39 .entries()
40 .flat_map(|entry| entry.relations().collect::<Vec<_>>())
41 .find(|r| r.name() == "debhelper-compat");
42
43 rels.and_then(|r| r.version().and_then(|v| v.1.to_string().parse().ok()))
44}
45
46pub fn get_debhelper_compat_level(path: &Path) -> Result<Option<u8>, std::io::Error> {
54 match read_debhelper_compat_file(&path.join("debian/compat")) {
55 Ok(Some(level)) => {
56 return Ok(Some(level));
57 }
58 Err(e) => {
59 return Err(e);
60 }
61 Ok(None) => {}
62 }
63
64 let p = path.join("debian/control");
65
66 match std::fs::File::open(p) {
67 Ok(f) => {
68 let control = debian_control::Control::read_relaxed(f).unwrap().0;
69 Ok(get_debhelper_compat_level_from_control(&control))
70 }
71 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
72 Err(e) => Err(e),
73 }
74}
75
76fn get_lintian_compat_levels() -> &'static SupportedCompatLevels {
80 lazy_static::lazy_static! {
81 static ref LINTIAN_COMPAT_LEVELS: SupportedCompatLevels = {
82 let output = std::process::Command::new("dh_assistant")
85 .arg("supported-compat-levels")
86 .output()
87 .expect("failed to run dh_assistant")
88 .stdout;
89 serde_json::from_slice(&output).expect("failed to parse dh_assistant output")
90 };
91 };
92 &LINTIAN_COMPAT_LEVELS
93}
94
95#[derive(Debug, serde::Deserialize)]
96#[allow(dead_code)]
97struct SupportedCompatLevels {
98 #[serde(rename = "HIGHEST_STABLE_COMPAT_LEVEL")]
99 highest_stable_compat_level: u8,
100 #[serde(rename = "LOWEST_NON_DEPRECATED_COMPAT_LEVEL")]
101 lowest_non_deprecated_compat_level: u8,
102 #[serde(rename = "LOWEST_VIRTUAL_DEBHELPER_COMPAT_LEVEL")]
103 lowest_virtual_debhelper_compat_level: u8,
104 #[serde(rename = "MAX_COMPAT_LEVEL")]
105 max_compat_level: u8,
106 #[serde(rename = "MIN_COMPAT_LEVEL")]
107 min_compat_level: u8,
108 #[serde(rename = "MIN_COMPAT_LEVEL_NOT_SCHEDULED_FOR_REMOVAL")]
109 min_compat_level_not_scheduled_for_removal: u8,
110}
111
112pub fn lowest_non_deprecated_compat_level() -> u8 {
114 get_lintian_compat_levels().lowest_non_deprecated_compat_level
115}
116
117pub fn highest_stable_compat_level() -> u8 {
119 get_lintian_compat_levels().highest_stable_compat_level
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
124pub enum EnsureDebhelperError {
125 DebhelperInWrongField(String),
127 ComplexDebhelperCompatRule,
129 DebhelperCompatWithoutVersion,
131}
132
133impl std::fmt::Display for EnsureDebhelperError {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 match self {
136 EnsureDebhelperError::DebhelperInWrongField(field) => {
137 write!(f, "debhelper in {}", field)
138 }
139 EnsureDebhelperError::ComplexDebhelperCompatRule => {
140 write!(f, "Complex rule for debhelper-compat, aborting")
141 }
142 EnsureDebhelperError::DebhelperCompatWithoutVersion => {
143 write!(f, "debhelper-compat without version, aborting")
144 }
145 }
146 }
147}
148
149impl std::error::Error for EnsureDebhelperError {}
150
151pub fn ensure_minimum_debhelper_version(
177 source: &mut debian_control::lossless::Source,
178 minimum_version: &Version,
179) -> Result<bool, EnsureDebhelperError> {
180 for (field_name, rels_opt) in [
182 ("Build-Depends-Arch", source.build_depends_arch()),
183 ("Build-Depends-Indep", source.build_depends_indep()),
184 ] {
185 let Some(rels) = rels_opt else {
186 continue;
187 };
188
189 for entry in rels.entries() {
190 for rel in entry.relations() {
191 if rel.name() == "debhelper-compat" || rel.name() == "debhelper" {
192 return Err(EnsureDebhelperError::DebhelperInWrongField(
193 field_name.to_string(),
194 ));
195 }
196 }
197 }
198 }
199
200 let mut rels = source.build_depends().unwrap_or_default();
201
202 for entry in rels.entries() {
204 let has_debhelper_compat = entry
205 .relations()
206 .any(|rel| rel.name() == "debhelper-compat");
207
208 if !has_debhelper_compat {
209 continue;
210 }
211
212 if entry.relations().count() > 1 {
213 return Err(EnsureDebhelperError::ComplexDebhelperCompatRule);
214 }
215
216 let rel = entry.relations().next().unwrap();
217 let Some((constraint, version)) = rel.version() else {
218 return Err(EnsureDebhelperError::DebhelperCompatWithoutVersion);
219 };
220
221 if constraint != debian_control::relations::VersionConstraint::Equal {
222 return Err(EnsureDebhelperError::ComplexDebhelperCompatRule);
223 }
224
225 if &version >= minimum_version {
226 return Ok(false);
227 }
228 }
229
230 let changed = crate::relations::ensure_minimum_version(&mut rels, "debhelper", minimum_version);
232
233 if changed {
234 source.set_build_depends(&rels);
235 }
236
237 Ok(changed)
238}
239
240pub fn get_sequences(source: &debian_control::lossless::Source) -> impl Iterator<Item = String> {
261 let build_depends = source.build_depends().unwrap_or_default();
262
263 build_depends
264 .entries()
265 .flat_map(|entry| entry.relations().collect::<Vec<_>>())
266 .filter_map(|rel| {
267 let name = rel.name();
268 if name.starts_with("dh-sequence-") {
269 Some(name[12..].to_string())
270 } else {
271 None
272 }
273 })
274 .collect::<Vec<_>>()
275 .into_iter()
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn test_parse_debhelper_compat() {
284 assert_eq!(super::parse_debhelper_compat("9"), Some(9));
285 assert_eq!(super::parse_debhelper_compat("9 # comment"), Some(9));
286 assert_eq!(
287 super::parse_debhelper_compat("9 # comment # comment"),
288 Some(9)
289 );
290 assert_eq!(super::parse_debhelper_compat(""), None);
291 assert_eq!(super::parse_debhelper_compat(" # comment"), None);
292 }
293
294 #[test]
295 fn test_get_debhelper_compat_level_from_control() {
296 let text = "Source: foo
297Build-Depends: debhelper-compat (= 9)
298
299Package: foo
300Architecture: any
301";
302
303 let control = debian_control::Control::read_relaxed(&mut text.as_bytes())
304 .unwrap()
305 .0;
306
307 assert_eq!(
308 super::get_debhelper_compat_level_from_control(&control),
309 Some(9)
310 );
311 }
312
313 #[test]
314 fn test_get_debhelper_compat_level_from_control_x_dh_compat() {
315 let text = "Source: foo
316X-DH-Compat: 9
317Build-Depends: debhelper
318";
319
320 let control = debian_control::Control::read_relaxed(&mut text.as_bytes())
321 .unwrap()
322 .0;
323
324 assert_eq!(
325 super::get_debhelper_compat_level_from_control(&control),
326 Some(9)
327 );
328 }
329
330 mod ensure_minimum_debhelper_version_tests {
331 use super::*;
332
333 #[test]
334 fn test_already() {
335 let text = "Source: foo\nBuild-Depends: debhelper (>= 10)\n";
336 let control = debian_control::Control::read_relaxed(text.as_bytes())
337 .unwrap()
338 .0;
339 let mut source = control.source().unwrap();
340
341 assert!(
342 !ensure_minimum_debhelper_version(&mut source, &"10".parse().unwrap()).unwrap()
343 );
344 assert_eq!(
345 source.build_depends().unwrap().to_string(),
346 "debhelper (>= 10)"
347 );
348
349 assert!(!ensure_minimum_debhelper_version(&mut source, &"9".parse().unwrap()).unwrap());
350 assert_eq!(
351 source.build_depends().unwrap().to_string(),
352 "debhelper (>= 10)"
353 );
354 }
355
356 #[test]
357 fn test_already_compat() {
358 let text = "Source: foo\nBuild-Depends: debhelper-compat (= 10)\n";
359 let control = debian_control::Control::read_relaxed(text.as_bytes())
360 .unwrap()
361 .0;
362 let mut source = control.source().unwrap();
363
364 assert!(
365 !ensure_minimum_debhelper_version(&mut source, &"10".parse().unwrap()).unwrap()
366 );
367 assert_eq!(
368 source.build_depends().unwrap().to_string(),
369 "debhelper-compat (= 10)"
370 );
371
372 assert!(!ensure_minimum_debhelper_version(&mut source, &"9".parse().unwrap()).unwrap());
373 assert_eq!(
374 source.build_depends().unwrap().to_string(),
375 "debhelper-compat (= 10)"
376 );
377 }
378
379 #[test]
380 fn test_bump() {
381 let text = "Source: foo\nBuild-Depends: debhelper (>= 10)\n";
382 let control = debian_control::Control::read_relaxed(text.as_bytes())
383 .unwrap()
384 .0;
385 let mut source = control.source().unwrap();
386
387 assert!(ensure_minimum_debhelper_version(&mut source, &"11".parse().unwrap()).unwrap());
388 assert_eq!(
389 source.build_depends().unwrap().to_string(),
390 "debhelper (>= 11)"
391 );
392 }
393
394 #[test]
395 fn test_bump_compat() {
396 let text = "Source: foo\nBuild-Depends: debhelper-compat (= 10)\n";
397 let control = debian_control::Control::read_relaxed(text.as_bytes())
398 .unwrap()
399 .0;
400 let mut source = control.source().unwrap();
401
402 assert!(ensure_minimum_debhelper_version(&mut source, &"11".parse().unwrap()).unwrap());
403 assert_eq!(
404 source.build_depends().unwrap().to_string(),
405 "debhelper (>= 11), debhelper-compat (= 10)"
406 );
407
408 assert!(
409 ensure_minimum_debhelper_version(&mut source, &"11.1".parse().unwrap()).unwrap()
410 );
411 assert_eq!(
412 source.build_depends().unwrap().to_string(),
413 "debhelper (>= 11.1), debhelper-compat (= 10)"
414 );
415 }
416
417 #[test]
418 fn test_not_set() {
419 let text = "Source: foo\n";
420 let control = debian_control::Control::read_relaxed(text.as_bytes())
421 .unwrap()
422 .0;
423 let mut source = control.source().unwrap();
424
425 assert!(ensure_minimum_debhelper_version(&mut source, &"10".parse().unwrap()).unwrap());
426 assert_eq!(
427 source.build_depends().unwrap().to_string(),
428 "debhelper (>= 10)"
429 );
430 }
431
432 #[test]
433 fn test_in_indep() {
434 let text = "Source: foo\nBuild-Depends-Indep: debhelper (>= 9)\n";
435 let control = debian_control::Control::read_relaxed(text.as_bytes())
436 .unwrap()
437 .0;
438 let mut source = control.source().unwrap();
439
440 let result = ensure_minimum_debhelper_version(&mut source, &"10".parse().unwrap());
441 assert!(result.is_err());
442 assert_eq!(
443 result.unwrap_err(),
444 EnsureDebhelperError::DebhelperInWrongField("Build-Depends-Indep".to_string())
445 );
446 }
447 }
448
449 mod get_sequences_tests {
450 use super::*;
451
452 #[test]
453 fn test_no_sequences() {
454 let text = "Source: foo\nBuild-Depends: debhelper (>= 10)\n";
455 let control = debian_control::Control::read_relaxed(text.as_bytes())
456 .unwrap()
457 .0;
458 let source = control.source().unwrap();
459
460 let sequences: Vec<String> = get_sequences(&source).collect();
461 assert_eq!(sequences, Vec::<String>::new());
462 }
463
464 #[test]
465 fn test_single_sequence() {
466 let text = "Source: foo\nBuild-Depends: dh-sequence-python3, debhelper (>= 10)\n";
467 let control = debian_control::Control::read_relaxed(text.as_bytes())
468 .unwrap()
469 .0;
470 let source = control.source().unwrap();
471
472 let sequences: Vec<String> = get_sequences(&source).collect();
473 assert_eq!(sequences, vec!["python3"]);
474 }
475
476 #[test]
477 fn test_multiple_sequences() {
478 let text = "Source: foo\nBuild-Depends: dh-sequence-python3, dh-sequence-nodejs, debhelper (>= 10)\n";
479 let control = debian_control::Control::read_relaxed(text.as_bytes())
480 .unwrap()
481 .0;
482 let source = control.source().unwrap();
483
484 let sequences: Vec<String> = get_sequences(&source).collect();
485 assert_eq!(sequences, vec!["python3", "nodejs"]);
486 }
487
488 #[test]
489 fn test_no_build_depends() {
490 let text = "Source: foo\n";
491 let control = debian_control::Control::read_relaxed(text.as_bytes())
492 .unwrap()
493 .0;
494 let source = control.source().unwrap();
495
496 let sequences: Vec<String> = get_sequences(&source).collect();
497 assert_eq!(sequences, Vec::<String>::new());
498 }
499 }
500}