1use 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#[derive(Debug, Error)]
27pub enum Error {
28 #[error("invalid architecture {0} for wb command '{1}'")]
29 InvalidArchitecture(WBArchitecture, &'static str),
31 #[error("unable to execute 'wb'")]
32 ExecutionError,
34 #[error("unable to exectue 'wb': {0}")]
35 IOError(#[from] std::io::Error),
37}
38
39#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)]
41pub struct WBCommand(String);
42
43impl WBCommand {
44 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
68pub trait WBCommandBuilder {
70 fn build(&self) -> WBCommand;
72}
73
74#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
81pub enum WBArchitecture {
82 Any,
84 All,
86 Architecture(Architecture),
88 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#[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 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 pub fn with_version(&mut self, version: &'a PackageVersion) -> &mut Self {
151 self.version = Some(version);
152 self
153 }
154
155 pub fn with_suite(&mut self, suite: SuiteOrCodename) -> &mut Self {
157 self.suite = Some(suite);
158 self
159 }
160
161 pub fn with_architectures(&mut self, architectures: &[WBArchitecture]) -> &mut Self {
163 self.architectures.extend_from_slice(architectures);
164 self
165 }
166
167 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#[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 pub fn new(source: &'a SourceSpecifier<'a>, message: &'a str) -> Result<Self, Error> {
211 for arch in &source.architectures {
212 match arch {
213 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 pub fn with_nmu_version(&mut self, version: u32) -> &mut Self {
234 self.nmu_version = Some(version);
235 self
236 }
237
238 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 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 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#[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 pub fn new(source: &'a SourceSpecifier<'a>, message: &'a str) -> Result<Self, Error> {
311 for arch in &source.architectures {
312 match arch {
313 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#[derive(Clone, Debug, Eq, PartialEq)]
340pub struct BuildPriority<'a> {
341 source: &'a SourceSpecifier<'a>,
342 priority: i32,
343}
344
345impl<'a> BuildPriority<'a> {
346 pub fn new(source: &'a SourceSpecifier<'a>, priority: i32) -> Result<Self, Error> {
348 for arch in &source.architectures {
349 match *arch {
350 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#[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 pub fn new(source: &'a SourceSpecifier<'a>, message: &'a str) -> Result<Self, Error> {
385 for arch in &source.architectures {
386 match *arch {
387 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#[derive(Clone, Debug, Eq, PartialEq)]
414pub struct Info<'a> {
415 source: &'a SourceSpecifier<'a>,
416}
417
418impl<'a> Info<'a> {
419 pub fn new(source: &'a SourceSpecifier<'a>) -> Result<Self, Error> {
421 for arch in &source.architectures {
422 match *arch {
423 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}