1use 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#[derive(Debug, Error)]
22pub enum Error {
23 #[error("invalid architecture {0} for wb command '{1}'")]
24 InvalidArchitecture(WBArchitecture, &'static str),
26 #[error("unable to execute 'wb'")]
27 ExecutionError,
29 #[error("unable to exectue 'wb': {0}")]
30 IOError(#[from] std::io::Error),
32}
33
34#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)]
36pub struct WBCommand(String);
37
38impl WBCommand {
39 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
63pub trait WBCommandBuilder {
65 fn build(&self) -> WBCommand;
67}
68
69#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
76pub enum WBArchitecture {
77 Any,
79 All,
81 Architecture(Architecture),
83 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#[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 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 pub fn with_version(&mut self, version: &'a PackageVersion) -> &mut Self {
146 self.version = Some(version);
147 self
148 }
149
150 pub fn with_suite(&mut self, suite: SuiteOrCodename) -> &mut Self {
152 self.suite = Some(suite);
153 self
154 }
155
156 pub fn with_architectures(&mut self, architectures: &[WBArchitecture]) -> &mut Self {
158 self.architectures.extend_from_slice(architectures);
159 self
160 }
161
162 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#[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 pub fn new(source: &'a SourceSpecifier<'a>, message: &'a str) -> Result<Self, Error> {
206 for arch in &source.architectures {
207 match arch {
208 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 pub fn with_nmu_version(&mut self, version: u32) -> &mut Self {
229 self.nmu_version = Some(version);
230 self
231 }
232
233 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 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 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#[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 pub fn new(source: &'a SourceSpecifier<'a>, message: &'a str) -> Result<Self, Error> {
306 for arch in &source.architectures {
307 match arch {
308 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#[derive(Clone, Debug, Eq, PartialEq)]
335pub struct BuildPriority<'a> {
336 source: &'a SourceSpecifier<'a>,
337 priority: i32,
338}
339
340impl<'a> BuildPriority<'a> {
341 pub fn new(source: &'a SourceSpecifier<'a>, priority: i32) -> Result<Self, Error> {
343 for arch in &source.architectures {
344 match *arch {
345 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#[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 pub fn new(source: &'a SourceSpecifier<'a>, message: &'a str) -> Result<Self, Error> {
380 for arch in &source.architectures {
381 match *arch {
382 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}