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