use std::path::PathBuf;
use std::process::Command;
use smol_str::SmolStr;
use crate::rindex::harvest::{is_valid_package_name, validate_attach_names};
use crate::rindex::libpaths::LibrarySearch;
pub const ENV_VAR: &str = "ARITY_ATTACH_PROBE";
const SENTINEL: &str = "ARITY-ATTACHES";
pub fn enabled_by_env() -> bool {
std::env::var(ENV_VAR).is_ok_and(|v| !v.is_empty() && v != "0")
}
pub fn probe_attaches(package: &str, search: &LibrarySearch) -> Option<Vec<SmolStr>> {
if !is_valid_package_name(package) {
return None;
}
let script = format!(
"before <- search(); \
suppressPackageStartupMessages(library({package})); \
cat(\"{SENTINEL}\\n\"); \
nw <- setdiff(search(), before); \
cat(sub(\"^package:\", \"\", grep(\"^package:\", nw, value = TRUE)), sep = \"\\n\")"
);
let output = Command::new("R")
.args(["--vanilla", "-s", "-e", &script])
.env("R_LIBS", join_path_list(search.dirs()))
.output()
.ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8(output.stdout).ok()?;
let names = parse_probe_output(&stdout)?;
let installed = |member: &str| search.find_package(member).is_some();
validate_attach_names(
&names.iter().map(String::as_str).collect::<Vec<_>>(),
package,
&installed,
)
}
fn parse_probe_output(stdout: &str) -> Option<Vec<String>> {
let mut lines = stdout.lines();
lines.find(|line| line.trim() == SENTINEL)?;
Some(
lines
.map(str::trim)
.filter(|l| !l.is_empty())
.map(String::from)
.collect(),
)
}
fn join_path_list(dirs: &[PathBuf]) -> String {
let sep = if cfg!(windows) { ";" } else { ":" };
dirs.iter()
.map(|d| d.to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join(sep)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_ignores_startup_noise_before_sentinel() {
let out = "Welcome to somepkg!\npackage:fake\nARITY-ATTACHES\ntools\nutils2\n";
assert_eq!(
parse_probe_output(out).unwrap(),
vec!["tools".to_string(), "utils2".to_string()]
);
}
#[test]
fn parse_without_sentinel_is_none() {
assert!(parse_probe_output("Error: package not found\n").is_none());
}
#[test]
fn parse_empty_diff_is_empty() {
assert_eq!(
parse_probe_output("ARITY-ATTACHES\n").unwrap(),
Vec::<String>::new()
);
}
}