Skip to main content

ambient_ci/action_impl/
dput.rs

1use std::{
2    path::{Path, PathBuf},
3    process::Command,
4};
5
6use clingwrap::runner::CommandRunner;
7use serde::{Deserialize, Serialize};
8use walkdir::WalkDir;
9
10use crate::{
11    action::{ActionError, Context},
12    action_impl::ActionImpl,
13};
14
15/// Upload a built `deb` package using the `dput` command.
16///
17/// The host must have `dput` configured to support the specified upload
18/// target.
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct Dput {
21    artifacts: PathBuf,
22    dput_target: Option<String>,
23}
24
25impl Dput {
26    /// Create a new `Dput` action.
27    pub fn new<P: AsRef<Path>>(artifacts: P, dput_target: Option<String>) -> Self {
28        Self {
29            artifacts: artifacts.as_ref().to_path_buf(),
30            dput_target,
31        }
32    }
33}
34
35impl ActionImpl for Dput {
36    fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
37        if let Some(target) = &self.dput_target {
38            let changes = changes_file(context.artifacts_dir())?;
39            dput(target, &changes)?;
40        }
41        Ok(())
42    }
43}
44
45fn changes_file(dir: &Path) -> Result<PathBuf, DputError> {
46    let mut result = None;
47    for entry in WalkDir::new(dir).into_iter() {
48        let path = entry.map_err(DputError::WalkDir)?.path().to_path_buf();
49        if path.display().to_string().ends_with(".changes") {
50            if result.is_some() {
51                return Err(DputError::ManyChanges);
52            }
53            result = Some(path);
54        }
55    }
56
57    if let Some(path) = result {
58        Ok(path)
59    } else {
60        Err(DputError::NoChanges)
61    }
62}
63
64fn dput(dest: &str, changes: &Path) -> Result<(), DputError> {
65    let mut cmd = Command::new("dput");
66    cmd.arg(dest).arg(changes);
67
68    let mut runner = CommandRunner::new(cmd);
69    runner.capture_stdout();
70    runner.capture_stderr();
71
72    let output = runner
73        .execute()
74        .map_err(|err| DputError::Execute("dput", err))?;
75
76    if output.status.code() != Some(0) {
77        let stderr = String::from_utf8_lossy(&output.stderr);
78        return Err(DputError::Dput(
79            dest.into(),
80            changes.to_path_buf(),
81            stderr.into(),
82        ));
83    }
84
85    Ok(())
86}
87
88/// Errors from `Dput` action.
89#[derive(Debug, thiserror::Error)]
90pub enum DputError {
91    /// `dput` failed.
92    #[error("dput failed to upload {1} to {0}:\n{2}")]
93    Dput(String, PathBuf, String),
94
95    /// Can't run program.
96    #[error("failed to run program {0}")]
97    Execute(&'static str, #[source] clingwrap::runner::CommandError),
98
99    /// Can't list files.
100    #[error("failed to list contents of upload directory")]
101    WalkDir(#[source] walkdir::Error),
102
103    /// Can't find a .changes file.
104    #[error("no *.changes file built for deb project")]
105    NoChanges,
106
107    /// Found more than one .changes file.
108    #[error("more than one *.changes file built for deb project")]
109    ManyChanges,
110}
111
112impl From<DputError> for ActionError {
113    fn from(value: DputError) -> Self {
114        Self::Dput(value)
115    }
116}