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.try_name().as_deref() == Some("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 let name = rel.try_name();
192 if name.as_deref() == Some("debhelper-compat")
193 || name.as_deref() == Some("debhelper")
194 {
195 return Err(EnsureDebhelperError::DebhelperInWrongField(
196 field_name.to_string(),
197 ));
198 }
199 }
200 }
201 }
202
203 let mut rels = source.build_depends().unwrap_or_default();
204
205 for entry in rels.entries() {
207 let has_debhelper_compat = entry
208 .relations()
209 .any(|rel| rel.try_name().as_deref() == Some("debhelper-compat"));
210
211 if !has_debhelper_compat {
212 continue;
213 }
214
215 if entry.relations().count() > 1 {
216 return Err(EnsureDebhelperError::ComplexDebhelperCompatRule);
217 }
218
219 let rel = entry.relations().next().unwrap();
220 let Some((constraint, version)) = rel.version() else {
221 return Err(EnsureDebhelperError::DebhelperCompatWithoutVersion);
222 };
223
224 if constraint != debian_control::relations::VersionConstraint::Equal {
225 return Err(EnsureDebhelperError::ComplexDebhelperCompatRule);
226 }
227
228 if &version >= minimum_version {
229 return Ok(false);
230 }
231 }
232
233 let changed = crate::relations::ensure_minimum_version(&mut rels, "debhelper", minimum_version);
235
236 if changed {
237 source.set_build_depends(&rels);
238 }
239
240 Ok(changed)
241}
242
243pub fn get_sequences(source: &debian_control::lossless::Source) -> impl Iterator<Item = String> {
264 let build_depends = source.build_depends().unwrap_or_default();
265
266 build_depends
267 .entries()
268 .flat_map(|entry| entry.relations().collect::<Vec<_>>())
269 .filter_map(|rel| {
270 let name = rel.try_name()?;
271 name.strip_prefix("dh-sequence-").map(|s| s.to_string())
272 })
273 .collect::<Vec<_>>()
274 .into_iter()
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn test_parse_debhelper_compat() {
283 assert_eq!(super::parse_debhelper_compat("9"), Some(9));
284 assert_eq!(super::parse_debhelper_compat("9 # comment"), Some(9));
285 assert_eq!(
286 super::parse_debhelper_compat("9 # comment # comment"),
287 Some(9)
288 );
289 assert_eq!(super::parse_debhelper_compat(""), None);
290 assert_eq!(super::parse_debhelper_compat(" # comment"), None);
291 }
292
293 #[test]
294 fn test_get_debhelper_compat_level_from_control() {
295 let text = "Source: foo
296Build-Depends: debhelper-compat (= 9)
297
298Package: foo
299Architecture: any
300";
301
302 let control = debian_control::Control::read_relaxed(&mut text.as_bytes())
303 .unwrap()
304 .0;
305
306 assert_eq!(
307 super::get_debhelper_compat_level_from_control(&control),
308 Some(9)
309 );
310 }
311
312 #[test]
313 fn test_get_debhelper_compat_level_from_control_x_dh_compat() {
314 let text = "Source: foo
315X-DH-Compat: 9
316Build-Depends: debhelper
317";
318
319 let control = debian_control::Control::read_relaxed(&mut text.as_bytes())
320 .unwrap()
321 .0;
322
323 assert_eq!(
324 super::get_debhelper_compat_level_from_control(&control),
325 Some(9)
326 );
327 }
328
329 mod ensure_minimum_debhelper_version_tests {
330 use super::*;
331
332 #[test]
333 fn test_already() {
334 let text = "Source: foo\nBuild-Depends: debhelper (>= 10)\n";
335 let control = debian_control::Control::read_relaxed(text.as_bytes())
336 .unwrap()
337 .0;
338 let mut source = control.source().unwrap();
339
340 assert!(
341 !ensure_minimum_debhelper_version(&mut source, &"10".parse().unwrap()).unwrap()
342 );
343 assert_eq!(
344 source.build_depends().unwrap().to_string(),
345 "debhelper (>= 10)"
346 );
347
348 assert!(!ensure_minimum_debhelper_version(&mut source, &"9".parse().unwrap()).unwrap());
349 assert_eq!(
350 source.build_depends().unwrap().to_string(),
351 "debhelper (>= 10)"
352 );
353 }
354
355 #[test]
356 fn test_already_compat() {
357 let text = "Source: foo\nBuild-Depends: debhelper-compat (= 10)\n";
358 let control = debian_control::Control::read_relaxed(text.as_bytes())
359 .unwrap()
360 .0;
361 let mut source = control.source().unwrap();
362
363 assert!(
364 !ensure_minimum_debhelper_version(&mut source, &"10".parse().unwrap()).unwrap()
365 );
366 assert_eq!(
367 source.build_depends().unwrap().to_string(),
368 "debhelper-compat (= 10)"
369 );
370
371 assert!(!ensure_minimum_debhelper_version(&mut source, &"9".parse().unwrap()).unwrap());
372 assert_eq!(
373 source.build_depends().unwrap().to_string(),
374 "debhelper-compat (= 10)"
375 );
376 }
377
378 #[test]
379 fn test_bump() {
380 let text = "Source: foo\nBuild-Depends: debhelper (>= 10)\n";
381 let control = debian_control::Control::read_relaxed(text.as_bytes())
382 .unwrap()
383 .0;
384 let mut source = control.source().unwrap();
385
386 assert!(ensure_minimum_debhelper_version(&mut source, &"11".parse().unwrap()).unwrap());
387 assert_eq!(
388 source.build_depends().unwrap().to_string(),
389 "debhelper (>= 11)"
390 );
391 }
392
393 #[test]
394 fn test_bump_compat() {
395 let text = "Source: foo\nBuild-Depends: debhelper-compat (= 10)\n";
396 let control = debian_control::Control::read_relaxed(text.as_bytes())
397 .unwrap()
398 .0;
399 let mut source = control.source().unwrap();
400
401 assert!(ensure_minimum_debhelper_version(&mut source, &"11".parse().unwrap()).unwrap());
402 assert_eq!(
403 source.build_depends().unwrap().to_string(),
404 "debhelper (>= 11), debhelper-compat (= 10)"
405 );
406
407 assert!(
408 ensure_minimum_debhelper_version(&mut source, &"11.1".parse().unwrap()).unwrap()
409 );
410 assert_eq!(
411 source.build_depends().unwrap().to_string(),
412 "debhelper (>= 11.1), debhelper-compat (= 10)"
413 );
414 }
415
416 #[test]
417 fn test_not_set() {
418 let text = "Source: foo\n";
419 let control = debian_control::Control::read_relaxed(text.as_bytes())
420 .unwrap()
421 .0;
422 let mut source = control.source().unwrap();
423
424 assert!(ensure_minimum_debhelper_version(&mut source, &"10".parse().unwrap()).unwrap());
425 assert_eq!(
426 source.build_depends().unwrap().to_string(),
427 "debhelper (>= 10)"
428 );
429 }
430
431 #[test]
432 fn test_in_indep() {
433 let text = "Source: foo\nBuild-Depends-Indep: debhelper (>= 9)\n";
434 let control = debian_control::Control::read_relaxed(text.as_bytes())
435 .unwrap()
436 .0;
437 let mut source = control.source().unwrap();
438
439 let result = ensure_minimum_debhelper_version(&mut source, &"10".parse().unwrap());
440 assert!(result.is_err());
441 assert_eq!(
442 result.unwrap_err(),
443 EnsureDebhelperError::DebhelperInWrongField("Build-Depends-Indep".to_string())
444 );
445 }
446 }
447
448 mod get_sequences_tests {
449 use super::*;
450
451 #[test]
452 fn test_no_sequences() {
453 let text = "Source: foo\nBuild-Depends: debhelper (>= 10)\n";
454 let control = debian_control::Control::read_relaxed(text.as_bytes())
455 .unwrap()
456 .0;
457 let source = control.source().unwrap();
458
459 let sequences: Vec<String> = get_sequences(&source).collect();
460 assert_eq!(sequences, Vec::<String>::new());
461 }
462
463 #[test]
464 fn test_single_sequence() {
465 let text = "Source: foo\nBuild-Depends: dh-sequence-python3, debhelper (>= 10)\n";
466 let control = debian_control::Control::read_relaxed(text.as_bytes())
467 .unwrap()
468 .0;
469 let source = control.source().unwrap();
470
471 let sequences: Vec<String> = get_sequences(&source).collect();
472 assert_eq!(sequences, vec!["python3"]);
473 }
474
475 #[test]
476 fn test_multiple_sequences() {
477 let text = "Source: foo\nBuild-Depends: dh-sequence-python3, dh-sequence-nodejs, debhelper (>= 10)\n";
478 let control = debian_control::Control::read_relaxed(text.as_bytes())
479 .unwrap()
480 .0;
481 let source = control.source().unwrap();
482
483 let sequences: Vec<String> = get_sequences(&source).collect();
484 assert_eq!(sequences, vec!["python3", "nodejs"]);
485 }
486
487 #[test]
488 fn test_no_build_depends() {
489 let text = "Source: foo\n";
490 let control = debian_control::Control::read_relaxed(text.as_bytes())
491 .unwrap()
492 .0;
493 let source = control.source().unwrap();
494
495 let sequences: Vec<String> = get_sequences(&source).collect();
496 assert_eq!(sequences, Vec::<String>::new());
497 }
498 }
499}