alpm_pkgbuild/bridge/
mod.rs

1//! The `PKGBUILD` to `.SRCINFO` bridge logic.
2
3pub(crate) mod parser;
4
5use std::{
6    io::ErrorKind,
7    path::Path,
8    process::{Command, Stdio},
9};
10
11use log::debug;
12pub use parser::{BridgeOutput, ClearableValue, Keyword, RawPackageName, Value};
13use which::which;
14
15use crate::error::Error;
16
17const DEFAULT_SCRIPT_NAME: &str = "alpm-pkgbuild-bridge";
18
19/// Runs the [`alpm-pkgbuild-bridge`] script, which exposes all relevant information of a
20/// [`PKGBUILD`] in a custom format.
21///
22/// Returns the output of that script as a `String`.
23///
24/// # Examples
25///
26/// ```
27/// use std::{fs::File, io::Write};
28///
29/// use alpm_pkgbuild::bridge::run_bridge_script;
30/// use tempfile::tempdir;
31///
32/// const TEST_FILE: &str = include_str!("../../tests/test_files/normal.pkgbuild");
33///
34/// # fn main() -> testresult::TestResult {
35/// // Create a temporary directory where we write the PKGBUILD to.
36/// let temp_dir = tempdir()?;
37/// let path = temp_dir.path().join("PKGBUILD");
38/// let mut file = File::create(&path)?;
39/// file.write_all(TEST_FILE.as_bytes());
40///
41/// // Call the bridge script on that path.
42/// println!("{}", run_bridge_script(&path)?);
43/// # Ok(())
44/// # }
45/// ```
46///
47/// # Errors
48///
49/// Returns an error if
50///
51/// - `pkgbuild_path` does not exist,
52/// - `pkgbuild_path` does not have a file name,
53/// - `pkgbuild_path` is not a file,
54/// - or running the `alpm-pkgbuild-bridge` script fails.
55///
56/// [`PKGBUILD`]: https://man.archlinux.org/man/PKGBUILD.5
57/// [`alpm-pkgbuild-bridge`]: https://gitlab.archlinux.org/archlinux/alpm/alpm-pkgbuild-bridge
58pub fn run_bridge_script(pkgbuild_path: &Path) -> Result<String, Error> {
59    // Make sure the PKGBUILD path exists.
60    if !pkgbuild_path.exists() {
61        let source = std::io::Error::new(ErrorKind::NotFound, "No such file or directory.");
62        return Err(Error::IoPath {
63            path: pkgbuild_path.to_path_buf(),
64            context: "checking for PKGBUILD",
65            source,
66        });
67    }
68
69    // Make sure the PKGBUILD path contains a filename.
70    let Some(filename) = pkgbuild_path.file_name() else {
71        return Err(Error::InvalidFile {
72            path: pkgbuild_path.to_owned(),
73            context: "No filename provided in path",
74        });
75    };
76
77    // Make sure the PKGBUILD path actually points to a file.
78    let metadata = pkgbuild_path.metadata().map_err(|source| Error::IoPath {
79        path: pkgbuild_path.to_owned(),
80        context: "getting metadata of file",
81        source,
82    })?;
83    if !metadata.file_type().is_file() {
84        return Err(Error::InvalidFile {
85            path: pkgbuild_path.to_owned(),
86            context: "Path doesn't point to a file.",
87        });
88    };
89
90    let script_path = which(DEFAULT_SCRIPT_NAME).map_err(|source| Error::ScriptNotFound {
91        script_name: DEFAULT_SCRIPT_NAME.to_string(),
92        source,
93    })?;
94
95    let mut command = Command::new(script_path);
96    // Change the CWD to the directory that contains the PKGBUILD
97    if let Some(parent) = pkgbuild_path.parent() {
98        // `parent` returns an empty path for relative paths with a single component.
99        if parent != Path::new("") {
100            command.current_dir(parent);
101        }
102    }
103
104    let parameters = vec![filename.to_string_lossy().to_string()];
105    command.args(&parameters);
106
107    command.stdout(Stdio::piped());
108    command.stderr(Stdio::piped());
109
110    debug!(
111        "Spawning command '{DEFAULT_SCRIPT_NAME} {}'",
112        parameters.join(" ")
113    );
114    let child = command.spawn().map_err(|source| Error::ScriptError {
115        context: "spawn",
116        parameters: parameters.clone(),
117        source,
118    })?;
119
120    debug!("Waiting for '{DEFAULT_SCRIPT_NAME}' to finish");
121    let output = child
122        .wait_with_output()
123        .map_err(|source| Error::ScriptError {
124            context: "finish",
125            parameters: parameters.clone(),
126            source,
127        })?;
128
129    if !output.status.success() {
130        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
131        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
132        return Err(Error::ScriptExecutionError {
133            parameters,
134            stdout,
135            stderr,
136        });
137    }
138
139    String::from_utf8(output.stdout).map_err(Error::from)
140}
141
142#[cfg(test)]
143mod tests {
144    use std::{fs::File, io::Write};
145
146    use tempfile::tempdir;
147    use testresult::{TestError, TestResult};
148
149    use super::*;
150
151    /// Make sure the `run_bridge_script` function fails when called on a directory.
152    #[test]
153    fn fail_on_directory() -> TestResult {
154        // Create an empty temporary directory
155        let tempdir = tempdir()?;
156        let temp_path = tempdir.path();
157
158        let result = run_bridge_script(temp_path);
159        let Err(error) = result else {
160            return Err(TestError::from(
161                "Expected an error, got {result:?} instead.",
162            ));
163        };
164
165        let Error::InvalidFile { path, context } = error else {
166            return Err(TestError::from(format!(
167                "Expected an InvalidFile error, got {error:?} instead."
168            )));
169        };
170
171        assert_eq!(temp_path, path);
172        assert_eq!(context, "Path doesn't point to a file.");
173
174        Ok(())
175    }
176
177    /// Make sure the `run_bridge_script` function fails when called on a non-existing file.
178    #[test]
179    fn fail_on_missing_file() -> TestResult {
180        // Create an empty temporary directory
181        let tempdir = tempdir()?;
182        let temp_path = tempdir.path().join("Nonexistent");
183
184        let result = run_bridge_script(&temp_path);
185        let Err(error) = result else {
186            return Err(TestError::from(
187                "Expected an error, got {result:?} instead.",
188            ));
189        };
190
191        let Error::IoPath { path, context, .. } = error else {
192            return Err(TestError::from(format!(
193                "Expected an IoPath error, got {error:?} instead."
194            )));
195        };
196
197        assert_eq!(temp_path, path);
198        assert_eq!(context, "checking for PKGBUILD");
199
200        Ok(())
201    }
202
203    /// Make sure the `run_bridge_script` function fails when called on a non-existing file.
204    #[test]
205    fn fail_on_no_filename() -> TestResult {
206        // Create an empty temporary directory
207        let tempdir = tempdir()?;
208        // Paths that end with `..` will return `None` as a filename in Rust.
209        let temp_path = tempdir.path().join("..");
210
211        let result = run_bridge_script(&temp_path);
212        let Err(error) = result else {
213            return Err(TestError::from(
214                "Expected an error, got {result:?} instead.",
215            ));
216        };
217
218        let Error::InvalidFile { path, context } = error else {
219            return Err(TestError::from(format!(
220                "Expected an InvalidFile error, got {error:?} instead."
221            )));
222        };
223
224        assert_eq!(temp_path, path);
225        assert_eq!(context, "No filename provided in path");
226
227        Ok(())
228    }
229
230    /// Test what happens
231    #[test]
232    fn fail_on_bridge_failure() -> TestResult {
233        // Create an empty temporary directory
234        let tempdir = tempdir()?;
235        let temp_path = tempdir.path().join("PKGBUILd");
236
237        let mut file = File::create_new(&temp_path)?;
238        file.write_all("<->#!%@!Definitely some invalid bash syntax.".as_bytes())?;
239
240        let result = run_bridge_script(&temp_path);
241        let Err(error) = result else {
242            return Err(TestError::from(
243                "Expected an error, got {result:?} instead.",
244            ));
245        };
246
247        let Error::ScriptExecutionError { .. } = error else {
248            return Err(TestError::from(format!(
249                "Expected an ScriptExecutionError error, got {error:?} instead."
250            )));
251        };
252
253        Ok(())
254    }
255}