pacsea/logic/sandbox/
mod.rs

1//! AUR sandbox preflight checks for build dependencies.
2
3mod analyze;
4mod fetch;
5mod parse;
6mod types;
7
8#[cfg(test)]
9mod tests;
10
11pub use analyze::extract_package_name;
12pub use parse::{parse_pkgbuild_conflicts, parse_pkgbuild_deps};
13pub use types::{DependencyDelta, SandboxInfo};
14
15use crate::logic::sandbox::analyze::{
16    analyze_package_from_pkgbuild, analyze_package_from_srcinfo, get_installed_packages,
17};
18use crate::logic::sandbox::fetch::fetch_srcinfo_async;
19use crate::state::types::PackageItem;
20use futures::stream::{FuturesUnordered, StreamExt};
21
22/// What: Create an empty `SandboxInfo` for a package when analysis fails.
23///
24/// Inputs:
25/// - `name`: Package name.
26///
27/// Output:
28/// - Empty `SandboxInfo` with the package name.
29///
30/// Details:
31/// - Used as fallback when analysis fails to ensure package appears in results.
32const fn create_empty_sandbox_info(name: String) -> SandboxInfo {
33    SandboxInfo {
34        package_name: name,
35        depends: Vec::new(),
36        makedepends: Vec::new(),
37        checkdepends: Vec::new(),
38        optdepends: Vec::new(),
39    }
40}
41
42/// What: Handle .SRCINFO analysis for a package.
43///
44/// Inputs:
45/// - `name`: Package name.
46/// - `srcinfo_text`: .SRCINFO file content.
47/// - `installed`: Installed packages set.
48/// - `provided`: Provided packages set.
49///
50/// Output:
51/// - `SandboxInfo` on success.
52///
53/// Details:
54/// - Analyzes dependencies from .SRCINFO and creates `SandboxInfo`.
55fn handle_srcinfo_analysis(
56    name: &str,
57    srcinfo_text: &str,
58    installed: &std::collections::HashSet<String>,
59    provided: &std::collections::HashSet<String>,
60) -> SandboxInfo {
61    analyze_package_from_srcinfo(name, srcinfo_text, installed, provided)
62}
63
64/// What: Handle PKGBUILD fallback analysis for a package.
65///
66/// Inputs:
67/// - `name`: Package name.
68/// - `pkgbuild_text`: PKGBUILD file content.
69/// - `installed`: Installed packages set.
70/// - `provided`: Provided packages set.
71///
72/// Output:
73/// - `SandboxInfo` on success.
74///
75/// Details:
76/// - Analyzes dependencies from PKGBUILD when .SRCINFO is unavailable.
77fn handle_pkgbuild_analysis(
78    name: &str,
79    pkgbuild_text: &str,
80    installed: &std::collections::HashSet<String>,
81    provided: &std::collections::HashSet<String>,
82) -> SandboxInfo {
83    let info = analyze_package_from_pkgbuild(name, pkgbuild_text, installed, provided);
84    let total_deps = info.depends.len()
85        + info.makedepends.len()
86        + info.checkdepends.len()
87        + info.optdepends.len();
88    tracing::info!(
89        "Parsed PKGBUILD for {}: {} total dependencies (depends={}, makedepends={}, checkdepends={}, optdepends={})",
90        name,
91        total_deps,
92        info.depends.len(),
93        info.makedepends.len(),
94        info.checkdepends.len(),
95        info.optdepends.len()
96    );
97    info
98}
99
100/// What: Process a single AUR package to resolve sandbox information.
101///
102/// Inputs:
103/// - `name`: Package name.
104/// - `client`: HTTP client for fetching.
105/// - `installed`: Installed packages set.
106/// - `provided`: Provided packages set.
107///
108/// Output:
109/// - `Some(SandboxInfo)` if resolved, `None` otherwise.
110///
111/// Details:
112/// - Tries .SRCINFO first, falls back to PKGBUILD if needed.
113async fn process_sandbox_package(
114    name: String,
115    client: reqwest::Client,
116    installed: std::collections::HashSet<String>,
117    provided: std::collections::HashSet<String>,
118) -> Option<SandboxInfo> {
119    match fetch_srcinfo_async(&client, &name).await {
120        Ok(srcinfo_text) => Some(handle_srcinfo_analysis(
121            &name,
122            &srcinfo_text,
123            &installed,
124            &provided,
125        )),
126        Err(e) => {
127            tracing::debug!(
128                "Failed to fetch .SRCINFO for {}: {}, trying PKGBUILD",
129                name,
130                e
131            );
132            let name_for_fallback = name.clone();
133            let installed_for_fallback = installed.clone();
134            let provided_for_fallback = provided.clone();
135            match tokio::task::spawn_blocking(move || {
136                crate::logic::files::fetch_pkgbuild_sync(&name_for_fallback)
137            })
138            .await
139            {
140                Ok(Ok(pkgbuild_text)) => {
141                    tracing::debug!(
142                        "Successfully fetched PKGBUILD for {}, parsing dependencies",
143                        name
144                    );
145                    Some(handle_pkgbuild_analysis(
146                        &name,
147                        &pkgbuild_text,
148                        &installed_for_fallback,
149                        &provided_for_fallback,
150                    ))
151                }
152                Ok(Err(e)) => {
153                    tracing::warn!("Failed to fetch PKGBUILD for {}: {}", name, e);
154                    tracing::info!(
155                        "Creating empty sandbox info for {} (both .SRCINFO and PKGBUILD fetch failed)",
156                        name
157                    );
158                    Some(create_empty_sandbox_info(name))
159                }
160                Err(e) => {
161                    tracing::warn!(
162                        "Failed to spawn blocking task for PKGBUILD fetch for {}: {}",
163                        name,
164                        e
165                    );
166                    tracing::info!(
167                        "Creating empty sandbox info for {} (spawn task failed)",
168                        name
169                    );
170                    Some(create_empty_sandbox_info(name))
171                }
172            }
173        }
174    }
175}
176
177/// What: Resolve sandbox information for AUR packages using async HTTP.
178///
179/// Inputs:
180/// - `items`: AUR packages to analyze.
181///
182/// Output:
183/// - Vector of `SandboxInfo` entries, one per AUR package.
184///
185/// Details:
186/// - Fetches `.SRCINFO` for each AUR package in parallel using async HTTP.
187/// - Parses dependencies and compares against host environment.
188/// - Returns empty vector if no AUR packages are present.
189pub async fn resolve_sandbox_info_async(items: &[PackageItem]) -> Vec<SandboxInfo> {
190    let aur_items: Vec<_> = items
191        .iter()
192        .filter(|i| matches!(i.source, crate::state::Source::Aur))
193        .collect();
194    let span = tracing::info_span!(
195        "resolve_sandbox_info",
196        stage = "sandbox",
197        item_count = aur_items.len()
198    );
199    let _guard = span.enter();
200    let start_time = std::time::Instant::now();
201
202    let installed = get_installed_packages();
203    let provided = crate::logic::deps::get_provided_packages(&installed);
204
205    // Fetch all .SRCINFO files in parallel
206    let client = reqwest::Client::builder()
207        .timeout(std::time::Duration::from_secs(10))
208        .build()
209        .unwrap_or_else(|_| reqwest::Client::new());
210
211    let mut fetch_futures = FuturesUnordered::new();
212    for item in items {
213        if matches!(item.source, crate::state::Source::Aur) {
214            let name = item.name.clone();
215            let installed_clone = installed.clone();
216            let provided_clone = provided.clone();
217            let client_clone = client.clone();
218            fetch_futures.push(process_sandbox_package(
219                name,
220                client_clone,
221                installed_clone,
222                provided_clone,
223            ));
224        }
225    }
226
227    // Collect all results as they complete
228    let mut results = Vec::new();
229    while let Some(result) = fetch_futures.next().await {
230        if let Some(info) = result {
231            results.push(info);
232        }
233    }
234
235    let elapsed = start_time.elapsed();
236    let duration_ms = u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX);
237    tracing::info!(
238        stage = "sandbox",
239        item_count = aur_items.len(),
240        result_count = results.len(),
241        duration_ms = duration_ms,
242        "Sandbox resolution complete"
243    );
244    results
245}
246
247/// What: Resolve sandbox information for AUR packages (synchronous wrapper for async version).
248///
249/// Inputs:
250/// - `items`: AUR packages to analyze.
251///
252/// Output:
253/// - Vector of `SandboxInfo` entries, one per AUR package.
254///
255/// # Panics
256/// - Panics if a tokio runtime cannot be created when no runtime handle is available
257///
258/// Details:
259/// - Wraps the async version for use in blocking contexts.
260#[must_use]
261pub fn resolve_sandbox_info(items: &[PackageItem]) -> Vec<SandboxInfo> {
262    // Use tokio runtime handle if available, otherwise create a new runtime
263    tokio::runtime::Handle::try_current().map_or_else(
264        |_| {
265            // No runtime available, create a new one
266            let rt = tokio::runtime::Runtime::new().unwrap_or_else(|e| {
267                tracing::error!(
268                    "Failed to create tokio runtime for sandbox resolution: {}",
269                    e
270                );
271                panic!("Cannot resolve sandbox info without tokio runtime");
272            });
273            rt.block_on(resolve_sandbox_info_async(items))
274        },
275        |handle| handle.block_on(resolve_sandbox_info_async(items)),
276    )
277}