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::Script {
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.wait_with_output().map_err(|source| Error::Script {
123        context: t!("error-script-finish"),
124        parameters: parameters.clone(),
125        source,
126    })?;
127
128    if !output.status.success() {
129        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
130        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
131        return Err(Error::ScriptExecution {
132            parameters,
133            stdout,
134            stderr,
135        });
136    }
137
138    String::from_utf8(output.stdout).map_err(Error::from)
139}
140
141#[cfg(test)]
142mod tests {
143    use std::{fs::File, io::Write};
144
145    use tempfile::tempdir;
146    use testresult::TestResult;
147
148    use super::*;
149
150    /// Make sure the `run_bridge_script` function fails when called on a directory.
151    #[test]
152    fn fail_on_directory() -> TestResult {
153        // Create an empty temporary directory
154        let tempdir = tempdir()?;
155        let temp_path = tempdir.path();
156
157        let result = run_bridge_script(temp_path);
158        let Err(error) = result else {
159            panic!("Expected an error, got {result:?} instead.");
160        };
161
162        let Error::InvalidFile { path, context } = error else {
163            panic!("Expected an InvalidFile error, got {error:?} instead.");
164        };
165
166        assert_eq!(temp_path, path);
167        assert_eq!(context, "Path doesn't point to a file");
168
169        Ok(())
170    }
171
172    /// Make sure the `run_bridge_script` function fails when called on a non-existing file.
173    #[test]
174    fn fail_on_missing_file() -> TestResult {
175        // Create an empty temporary directory
176        let tempdir = tempdir()?;
177        let temp_path = tempdir.path().join("Nonexistent");
178
179        let result = run_bridge_script(&temp_path);
180        let Err(error) = result else {
181            panic!("Expected an error, got {result:?} instead.");
182        };
183
184        let Error::IoPath { path, context, .. } = error else {
185            panic!("Expected an IoPath error, got {error:?} instead.");
186        };
187
188        assert_eq!(temp_path, path);
189        assert_eq!(context, "checking for PKGBUILD");
190
191        Ok(())
192    }
193
194    /// Make sure the `run_bridge_script` function fails when called on a non-existing file.
195    #[test]
196    fn fail_on_no_filename() -> TestResult {
197        // Create an empty temporary directory
198        let tempdir = tempdir()?;
199        // Paths that end with `..` will return `None` as a filename in Rust.
200        let temp_path = tempdir.path().join("..");
201
202        let result = run_bridge_script(&temp_path);
203        let Err(error) = result else {
204            panic!("Expected an error, got {result:?} instead.");
205        };
206
207        let Error::InvalidFile { path, context } = error else {
208            panic!("Expected an InvalidFile error, got {error:?} instead.");
209        };
210
211        assert_eq!(temp_path, path);
212        assert_eq!(context, "No filename provided in path");
213
214        Ok(())
215    }
216
217    /// Test what happens
218    #[test]
219    fn fail_on_bridge_failure() -> TestResult {
220        // Create an empty temporary directory
221        let tempdir = tempdir()?;
222        let temp_path = tempdir.path().join("PKGBUILd");
223
224        let mut file = File::create_new(&temp_path)?;
225        file.write_all("<->#!%@!Definitely some invalid bash syntax.".as_bytes())?;
226
227        let result = run_bridge_script(&temp_path);
228        let Err(error) = result else {
229            panic!("Expected an error, got {result:?} instead.");
230        };
231
232        let Error::ScriptExecution { .. } = error else {
233            panic!("Expected an ScriptExecutionError error, got {error:?} instead.");
234        };
235
236        Ok(())
237    }
238}