assorted_debian_utils/
wb.rs

1// Copyright 2021 Sebastian Ramacher
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4//! # Helpers to generate commands for Debian's wanna-build service
5//!
6//! This module provides builders to generate commands for [wanna-build](https://release.debian.org/wanna-build.txt).
7
8use std::{
9    fmt::{Display, Formatter},
10    io::Write,
11    process::{Command, Stdio},
12    str::FromStr,
13};
14
15use serde::{Deserialize, Serialize};
16use thiserror::Error;
17
18use crate::{
19    architectures::{Architecture, ParseError},
20    archive::{Suite, SuiteOrCodename},
21    package::PackageName,
22    version::PackageVersion,
23};
24
25/// Errors when working with `wb`
26#[derive(Debug, Error)]
27pub enum Error {
28    #[error("invalid architecture {0} for wb command '{1}'")]
29    /// An invalid architecture for a command was specified
30    InvalidArchitecture(WBArchitecture, &'static str),
31    #[error("unable to execute 'wb'")]
32    /// Execution of `wb` failed
33    ExecutionError,
34    #[error("unable to exectue 'wb': {0}")]
35    /// Execution of `wb` failed with IO error
36    IOError(#[from] std::io::Error),
37}
38
39/// A command to be executed by `wb`
40#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)]
41pub struct WBCommand(String);
42
43impl WBCommand {
44    /// Execute the command via `wb`
45    ///
46    /// This function runs `wb` and passes the commands on `stdin`.
47    pub fn execute(&self) -> Result<(), Error> {
48        let mut proc = Command::new("wb")
49            .stdin(Stdio::piped())
50            .spawn()
51            .map_err(Error::from)?;
52        if let Some(mut stdin) = proc.stdin.take() {
53            stdin.write_all(self.0.as_bytes()).map_err(Error::from)?;
54        } else {
55            return Err(Error::ExecutionError);
56        }
57        proc.wait_with_output().map_err(Error::from)?;
58        Ok(())
59    }
60}
61
62impl Display for WBCommand {
63    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
64        write!(f, "{}", self.0)
65    }
66}
67
68/// A trait to build `wb` commands
69pub trait WBCommandBuilder {
70    /// Build a `wb` command
71    fn build(&self) -> WBCommand;
72}
73
74/// Architectures understood by `wb`
75///
76/// In addition to the the architectures from [Architecture], `wb` has two special "architectures"
77/// named `ANY` (all binary-dependent architectures) and `ALL` (all architectures). Also, it
78/// supports negation of architectures, e.g., `ANY -i386` refers to all binary-dependent
79/// architectures without `i386`.
80#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
81pub enum WBArchitecture {
82    /// The special `ANY` architecture, i.e., all architectures understood by wb except `all`
83    Any,
84    /// The special `ALL` architecture, i.e., all architectures understood by wb
85    All,
86    /// Specify an architecture
87    Architecture(Architecture),
88    /// Exclude a specific architecture
89    ExcludeArchitecture(Architecture),
90}
91
92impl Display for WBArchitecture {
93    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
94        match self {
95            Self::Any => write!(f, "ANY"),
96            Self::All => write!(f, "ALL"),
97            Self::Architecture(arch) => write!(f, "{arch}"),
98            Self::ExcludeArchitecture(arch) => write!(f, "-{arch}"),
99        }
100    }
101}
102
103impl TryFrom<&str> for WBArchitecture {
104    type Error = ParseError;
105
106    fn try_from(value: &str) -> Result<Self, Self::Error> {
107        match value {
108            "ANY" => Ok(Self::Any),
109            "ALL" => Ok(Self::All),
110            _ => {
111                if let Some(stripped) = value.strip_prefix('-') {
112                    Ok(Self::ExcludeArchitecture(stripped.try_into()?))
113                } else {
114                    Ok(Self::Architecture(value.try_into()?))
115                }
116            }
117        }
118    }
119}
120
121impl FromStr for WBArchitecture {
122    type Err = ParseError;
123
124    fn from_str(s: &str) -> Result<Self, Self::Err> {
125        Self::try_from(s)
126    }
127}
128
129/// Specifier for a source with version, architecture and suite
130#[derive(Clone, Debug, PartialEq, Eq)]
131pub struct SourceSpecifier<'a> {
132    source: &'a PackageName,
133    version: Option<&'a PackageVersion>,
134    architectures: Vec<WBArchitecture>,
135    suite: Option<SuiteOrCodename>,
136}
137
138impl<'a> SourceSpecifier<'a> {
139    /// Create a new source specifier for the given source package name.
140    pub fn new(source: &'a PackageName) -> Self {
141        Self {
142            source,
143            version: None,
144            architectures: Vec::new(),
145            suite: None,
146        }
147    }
148
149    /// Specify version of the source package.
150    pub fn with_version(&mut self, version: &'a PackageVersion) -> &mut Self {
151        self.version = Some(version);
152        self
153    }
154
155    /// Specify suite. If not set, `unstable` is used.
156    pub fn with_suite(&mut self, suite: SuiteOrCodename) -> &mut Self {
157        self.suite = Some(suite);
158        self
159    }
160
161    /// Specify architectures. If not set, the `nmu` will be scheduled for `ANY`.
162    pub fn with_architectures(&mut self, architectures: &[WBArchitecture]) -> &mut Self {
163        self.architectures.extend_from_slice(architectures);
164        self
165    }
166
167    /// Specify architectures. If not set, the `nmu` will be scheduled for `ANY`.
168    pub fn with_archive_architectures(&mut self, architectures: &[Architecture]) -> &mut Self {
169        self.architectures.extend(
170            architectures
171                .iter()
172                .copied()
173                .map(WBArchitecture::Architecture),
174        );
175        self
176    }
177}
178
179impl Display for SourceSpecifier<'_> {
180    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
181        write!(f, "{}", self.source)?;
182        if let Some(version) = self.version {
183            write!(f, "_{version}")?;
184        }
185        write!(f, " . ")?;
186        if self.architectures.is_empty() {
187            write!(f, "{} ", WBArchitecture::Any)?;
188        } else {
189            for arch in &self.architectures {
190                write!(f, "{arch} ")?;
191            }
192        }
193        write!(f, ". {}", self.suite.unwrap_or(Suite::Unstable.into()))
194    }
195}
196
197/// Builder to create a `nmu` command
198#[derive(Clone, Debug, Eq, PartialEq)]
199pub struct BinNMU<'a> {
200    source: &'a SourceSpecifier<'a>,
201    message: &'a str,
202    nmu_version: Option<u32>,
203    extra_depends: Option<&'a str>,
204    priority: Option<i32>,
205    dep_wait: Option<&'a str>,
206}
207
208impl<'a> BinNMU<'a> {
209    /// Create a new `nmu` command for the given `source`.
210    pub fn new(source: &'a SourceSpecifier<'a>, message: &'a str) -> Result<Self, Error> {
211        for arch in &source.architectures {
212            match arch {
213                // unable to nmu with source, -source, ALL, all
214                WBArchitecture::Architecture(Architecture::Source | Architecture::All)
215                | WBArchitecture::ExcludeArchitecture(Architecture::Source | Architecture::All)
216                | WBArchitecture::All => {
217                    return Err(Error::InvalidArchitecture(*arch, "nmu"));
218                }
219                _ => {}
220            }
221        }
222        Ok(Self {
223            source,
224            message,
225            nmu_version: None,
226            extra_depends: None,
227            priority: None,
228            dep_wait: None,
229        })
230    }
231
232    /// Specify the binNMU version. If not set, `wb` tries to auto-detect the binNMU version.
233    pub fn with_nmu_version(&mut self, version: u32) -> &mut Self {
234        self.nmu_version = Some(version);
235        self
236    }
237
238    /// Specify extra dependencies.
239    pub fn with_extra_depends(&mut self, extra_depends: &'a str) -> &mut Self {
240        self.extra_depends = Some(extra_depends);
241        self
242    }
243
244    /// Specify build priority. If not set, the build priority will not be changed.
245    pub fn with_build_priority(&mut self, priority: i32) -> &mut Self {
246        if priority != 0 {
247            self.priority = Some(priority);
248        } else {
249            self.priority = None;
250        }
251        self
252    }
253
254    /// Specify dependency-wait. If not set, no dependency-wait will be set.
255    pub fn with_dependency_wait(&mut self, dw: &'a str) -> &mut Self {
256        self.dep_wait = Some(dw);
257        self
258    }
259}
260
261impl Display for BinNMU<'_> {
262    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
263        write!(f, "nmu ")?;
264        if let Some(nmu_version) = self.nmu_version {
265            write!(f, "{nmu_version} ")?;
266        }
267        write!(f, "{} . -m \"{}\"", self.source, self.message)?;
268        if let Some(extra_depends) = self.extra_depends {
269            write!(f, " --extra-depends \"{extra_depends}\"")?;
270        }
271        if let Some(dep_wait) = self.dep_wait {
272            write!(
273                f,
274                "\n{}",
275                DepWait {
276                    source: self.source,
277                    message: dep_wait
278                }
279            )?;
280        }
281        if let Some(priority) = self.priority {
282            write!(
283                f,
284                "\n{}",
285                BuildPriority {
286                    source: self.source,
287                    priority,
288                }
289            )?;
290        }
291        Ok(())
292    }
293}
294
295impl WBCommandBuilder for BinNMU<'_> {
296    fn build(&self) -> WBCommand {
297        WBCommand(self.to_string())
298    }
299}
300
301/// Builder for the `dw` command
302#[derive(Clone, Debug, Eq, PartialEq)]
303pub struct DepWait<'a> {
304    source: &'a SourceSpecifier<'a>,
305    message: &'a str,
306}
307
308impl<'a> DepWait<'a> {
309    /// Create a new `dw` command for the given `source`.
310    pub fn new(source: &'a SourceSpecifier<'a>, message: &'a str) -> Result<Self, Error> {
311        for arch in &source.architectures {
312            match arch {
313                // unable to dw with source, -source
314                WBArchitecture::Architecture(Architecture::Source)
315                | WBArchitecture::ExcludeArchitecture(Architecture::Source) => {
316                    return Err(Error::InvalidArchitecture(*arch, "dw"));
317                }
318                _ => {}
319            }
320        }
321
322        Ok(Self { source, message })
323    }
324}
325
326impl Display for DepWait<'_> {
327    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
328        write!(f, "dw {} . -m \"{}\"", self.source, self.message)
329    }
330}
331
332impl WBCommandBuilder for DepWait<'_> {
333    fn build(&self) -> WBCommand {
334        WBCommand(self.to_string())
335    }
336}
337
338/// Builder for the `bp` command
339#[derive(Clone, Debug, Eq, PartialEq)]
340pub struct BuildPriority<'a> {
341    source: &'a SourceSpecifier<'a>,
342    priority: i32,
343}
344
345impl<'a> BuildPriority<'a> {
346    /// Create a new `bp` command for the given `source`.
347    pub fn new(source: &'a SourceSpecifier<'a>, priority: i32) -> Result<Self, Error> {
348        for arch in &source.architectures {
349            match *arch {
350                // unable to bp with source, -source
351                WBArchitecture::Architecture(Architecture::Source)
352                | WBArchitecture::ExcludeArchitecture(Architecture::Source) => {
353                    return Err(Error::InvalidArchitecture(*arch, "bp"));
354                }
355                _ => {}
356            }
357        }
358
359        Ok(Self { source, priority })
360    }
361}
362
363impl Display for BuildPriority<'_> {
364    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
365        write!(f, "bp {} {}", self.priority, self.source)
366    }
367}
368
369impl WBCommandBuilder for BuildPriority<'_> {
370    fn build(&self) -> WBCommand {
371        WBCommand(self.to_string())
372    }
373}
374
375/// Builder for the `fail` command
376#[derive(Clone, Debug, Eq, PartialEq)]
377pub struct Fail<'a> {
378    source: &'a SourceSpecifier<'a>,
379    message: &'a str,
380}
381
382impl<'a> Fail<'a> {
383    /// Create a new `fail` command for the given `source`.
384    pub fn new(source: &'a SourceSpecifier<'a>, message: &'a str) -> Result<Self, Error> {
385        for arch in &source.architectures {
386            match *arch {
387                // unable to fail with source, -source
388                WBArchitecture::Architecture(Architecture::Source)
389                | WBArchitecture::ExcludeArchitecture(Architecture::Source) => {
390                    return Err(Error::InvalidArchitecture(*arch, "fail"));
391                }
392                _ => {}
393            }
394        }
395
396        Ok(Self { source, message })
397    }
398}
399
400impl Display for Fail<'_> {
401    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
402        write!(f, "fail {} . -m \"{}\"", self.source, self.message)
403    }
404}
405
406impl WBCommandBuilder for Fail<'_> {
407    fn build(&self) -> WBCommand {
408        WBCommand(self.to_string())
409    }
410}
411
412/// Builder for the `info` command
413#[derive(Clone, Debug, Eq, PartialEq)]
414pub struct Info<'a> {
415    source: &'a SourceSpecifier<'a>,
416}
417
418impl<'a> Info<'a> {
419    /// Create a new `info` command for the given `source`.
420    pub fn new(source: &'a SourceSpecifier<'a>) -> Result<Self, Error> {
421        for arch in &source.architectures {
422            match *arch {
423                // unable to info with source, -source
424                WBArchitecture::Architecture(Architecture::Source)
425                | WBArchitecture::ExcludeArchitecture(Architecture::Source) => {
426                    return Err(Error::InvalidArchitecture(*arch, "info"));
427                }
428                _ => {}
429            }
430        }
431
432        Ok(Self { source })
433    }
434}
435
436impl Display for Info<'_> {
437    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
438        write!(f, "info {}", self.source)
439    }
440}
441
442impl WBCommandBuilder for Info<'_> {
443    fn build(&self) -> WBCommand {
444        WBCommand(self.to_string())
445    }
446}
447
448#[cfg(test)]
449mod test {
450    use super::{
451        BinNMU, BuildPriority, DepWait, Fail, SourceSpecifier, WBArchitecture, WBCommandBuilder,
452    };
453    use crate::{architectures::Architecture, archive::SuiteOrCodename, package::PackageName};
454
455    #[test]
456    fn arch_from_str() {
457        assert_eq!(
458            WBArchitecture::try_from("ANY").unwrap(),
459            WBArchitecture::Any
460        );
461        assert_eq!(
462            WBArchitecture::try_from("ALL").unwrap(),
463            WBArchitecture::All
464        );
465        assert_eq!(
466            WBArchitecture::try_from("amd64").unwrap(),
467            WBArchitecture::Architecture(Architecture::Amd64)
468        );
469        assert_eq!(
470            WBArchitecture::try_from("-amd64").unwrap(),
471            WBArchitecture::ExcludeArchitecture(Architecture::Amd64)
472        );
473        assert!(WBArchitecture::try_from("-ALL").is_err());
474    }
475
476    #[test]
477    fn binnmu() {
478        let source = PackageName::try_from("zathura").unwrap();
479
480        assert_eq!(
481            BinNMU::new(&SourceSpecifier::new(&source), "Rebuild on buildd")
482                .unwrap()
483                .build()
484                .to_string(),
485            "nmu zathura . ANY . unstable . -m \"Rebuild on buildd\""
486        );
487        assert_eq!(
488            BinNMU::new(&SourceSpecifier::new(&source), "Rebuild on buildd")
489                .unwrap()
490                .with_nmu_version(3)
491                .build()
492                .to_string(),
493            "nmu 3 zathura . ANY . unstable . -m \"Rebuild on buildd\""
494        );
495        assert_eq!(
496            BinNMU::new(
497                SourceSpecifier::new(&source).with_version(&"2.3.4".try_into().unwrap()),
498                "Rebuild on buildd"
499            )
500            .unwrap()
501            .build()
502            .to_string(),
503            "nmu zathura_2.3.4 . ANY . unstable . -m \"Rebuild on buildd\""
504        );
505        assert_eq!(
506            BinNMU::new(
507                SourceSpecifier::new(&source).with_architectures(&[
508                    WBArchitecture::Any,
509                    WBArchitecture::ExcludeArchitecture(Architecture::I386)
510                ]),
511                "Rebuild on buildd"
512            )
513            .unwrap()
514            .build()
515            .to_string(),
516            "nmu zathura . ANY -i386 . unstable . -m \"Rebuild on buildd\""
517        );
518        assert_eq!(
519            BinNMU::new(
520                SourceSpecifier::new(&source).with_suite(SuiteOrCodename::TESTING),
521                "Rebuild on buildd"
522            )
523            .unwrap()
524            .build()
525            .to_string(),
526            "nmu zathura . ANY . testing . -m \"Rebuild on buildd\""
527        );
528        assert_eq!(
529            BinNMU::new(&SourceSpecifier::new(&source), "Rebuild on buildd")
530                .unwrap()
531                .with_extra_depends("libgirara-dev")
532                .build()
533                .to_string(),
534            "nmu zathura . ANY . unstable . -m \"Rebuild on buildd\" --extra-depends \"libgirara-dev\""
535        );
536        assert_eq!(
537            BinNMU::new(&SourceSpecifier::new(&source), "Rebuild on buildd")
538                .unwrap()
539                .with_dependency_wait("libgirara-dev")
540                .build()
541                .to_string(),
542            "nmu zathura . ANY . unstable . -m \"Rebuild on buildd\"\ndw zathura . ANY . unstable . -m \"libgirara-dev\""
543        );
544        assert_eq!(
545            BinNMU::new(&SourceSpecifier::new(&source), "Rebuild on buildd")
546                .unwrap()
547                .with_build_priority(-10)
548                .build()
549                .to_string(),
550            "nmu zathura . ANY . unstable . -m \"Rebuild on buildd\"\nbp -10 zathura . ANY . unstable"
551        );
552    }
553
554    #[test]
555    fn nmu_builder() {
556        let source = PackageName::try_from("zathura").unwrap();
557        let source = SourceSpecifier::new(&source);
558        let mut builder = BinNMU::new(&source, "Rebuild on buildd").unwrap();
559        builder.with_nmu_version(3);
560        assert_eq!(
561            builder.build().to_string(),
562            "nmu 3 zathura . ANY . unstable . -m \"Rebuild on buildd\""
563        );
564
565        builder.with_build_priority(0);
566        assert_eq!(
567            builder.build().to_string(),
568            "nmu 3 zathura . ANY . unstable . -m \"Rebuild on buildd\""
569        );
570    }
571
572    #[test]
573    fn bp() {
574        let source = PackageName::try_from("zathura").unwrap();
575
576        assert_eq!(
577            BuildPriority::new(&SourceSpecifier::new(&source), 10)
578                .unwrap()
579                .build()
580                .to_string(),
581            "bp 10 zathura . ANY . unstable"
582        );
583        assert_eq!(
584            BuildPriority::new(
585                SourceSpecifier::new(&source).with_version(&"2.3.4".try_into().unwrap()),
586                10
587            )
588            .unwrap()
589            .build()
590            .to_string(),
591            "bp 10 zathura_2.3.4 . ANY . unstable"
592        );
593        assert_eq!(
594            BuildPriority::new(
595                SourceSpecifier::new(&source).with_architectures(&[
596                    WBArchitecture::Any,
597                    WBArchitecture::ExcludeArchitecture(Architecture::I386)
598                ]),
599                10
600            )
601            .unwrap()
602            .build()
603            .to_string(),
604            "bp 10 zathura . ANY -i386 . unstable"
605        );
606        assert_eq!(
607            BuildPriority::new(
608                SourceSpecifier::new(&source).with_suite(SuiteOrCodename::TESTING),
609                10
610            )
611            .unwrap()
612            .build()
613            .to_string(),
614            "bp 10 zathura . ANY . testing"
615        );
616    }
617
618    #[test]
619    fn dw() {
620        let source = PackageName::try_from("zathura").unwrap();
621
622        assert_eq!(
623            DepWait::new(&SourceSpecifier::new(&source), "libgirara-dev")
624                .unwrap()
625                .build()
626                .to_string(),
627            "dw zathura . ANY . unstable . -m \"libgirara-dev\""
628        );
629        assert_eq!(
630            DepWait::new(
631                SourceSpecifier::new(&source).with_version(&"2.3.4".try_into().unwrap()),
632                "libgirara-dev"
633            )
634            .unwrap()
635            .build()
636            .to_string(),
637            "dw zathura_2.3.4 . ANY . unstable . -m \"libgirara-dev\""
638        );
639        assert_eq!(
640            DepWait::new(
641                SourceSpecifier::new(&source).with_architectures(&[
642                    WBArchitecture::Any,
643                    WBArchitecture::ExcludeArchitecture(Architecture::I386)
644                ]),
645                "libgirara-dev"
646            )
647            .unwrap()
648            .build()
649            .to_string(),
650            "dw zathura . ANY -i386 . unstable . -m \"libgirara-dev\""
651        );
652        assert_eq!(
653            DepWait::new(
654                SourceSpecifier::new(&source).with_suite(SuiteOrCodename::TESTING),
655                "libgirara-dev"
656            )
657            .unwrap()
658            .build()
659            .to_string(),
660            "dw zathura . ANY . testing . -m \"libgirara-dev\""
661        );
662    }
663
664    #[test]
665    fn fail() {
666        let source = PackageName::try_from("zathura").unwrap();
667
668        assert_eq!(
669            Fail::new(&SourceSpecifier::new(&source), "#1234")
670                .unwrap()
671                .build()
672                .to_string(),
673            "fail zathura . ANY . unstable . -m \"#1234\""
674        );
675    }
676}