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