use std::env;
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use colored::Colorize;
use comfy_table::{modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL, Cell, Color, Table};
#[derive(Debug, Clone)]
pub struct Platform {
pub os: Os,
pub arch: Arch,
pub distro: Option<Distro>,
pub pkg_managers: Vec<PkgManager>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Os {
Linux,
MacOs,
Windows,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Arch {
X86_64,
Aarch64,
Unknown,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Distro {
pub id: String, pub id_like: String, pub name: String, }
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PkgManager {
Apt,
Dnf,
Pacman,
Zypper,
Brew,
Cargo,
Port, }
impl std::fmt::Display for PkgManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PkgManager::Apt => write!(f, "apt"),
PkgManager::Dnf => write!(f, "dnf"),
PkgManager::Pacman => write!(f, "pacman"),
PkgManager::Zypper => write!(f, "zypper"),
PkgManager::Brew => write!(f, "brew"),
PkgManager::Cargo => write!(f, "cargo"),
PkgManager::Port => write!(f, "port"),
}
}
}
impl std::fmt::Display for Os {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Os::Linux => write!(f, "Linux"),
Os::MacOs => write!(f, "macOS"),
Os::Windows => write!(f, "Windows"),
Os::Unknown => write!(f, "Unknown"),
}
}
}
impl std::fmt::Display for Arch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Arch::X86_64 => write!(f, "x86_64"),
Arch::Aarch64 => write!(f, "aarch64"),
Arch::Unknown => write!(f, "unknown"),
}
}
}
pub fn detect_platform() -> Platform {
let os = detect_os();
let arch = detect_arch();
let distro = if os == Os::Linux {
detect_linux_distro()
} else {
None
};
let pkg_managers = detect_pkg_managers();
Platform {
os,
arch,
distro,
pkg_managers,
}
}
fn detect_os() -> Os {
match env::consts::OS {
"linux" => Os::Linux,
"macos" => Os::MacOs,
"windows" => Os::Windows,
_ => Os::Unknown,
}
}
fn detect_arch() -> Arch {
match env::consts::ARCH {
"x86_64" => Arch::X86_64,
"aarch64" => Arch::Aarch64,
_ => Arch::Unknown,
}
}
fn detect_linux_distro() -> Option<Distro> {
let contents = fs::read_to_string("/etc/os-release").ok()?;
let mut id = String::new();
let mut id_like = String::new();
let mut pretty_name = String::new();
for line in contents.lines() {
if let Some(val) = line.strip_prefix("ID=") {
id = val.trim_matches('"').to_lowercase();
} else if let Some(val) = line.strip_prefix("ID_LIKE=") {
id_like = val.trim_matches('"').to_lowercase();
} else if let Some(val) = line.strip_prefix("PRETTY_NAME=") {
pretty_name = val.trim_matches('"').to_string();
}
}
if id.is_empty() && pretty_name.is_empty() {
return None;
}
Some(Distro {
id,
id_like,
name: pretty_name,
})
}
fn detect_linux_libc() -> &'static str {
if let Ok(output) = Command::new("ldd").arg("--version").output() {
let stdout = String::from_utf8_lossy(&output.stdout).to_lowercase();
let stderr = String::from_utf8_lossy(&output.stderr).to_lowercase();
if stdout.contains("musl") || stderr.contains("musl") {
return "musl";
}
}
if Path::new("/lib/ld-musl-x86_64.so.1").exists()
|| Path::new("/lib/ld-musl-aarch64.so.1").exists()
{
return "musl";
}
if let Ok(contents) = fs::read_to_string("/etc/os-release") {
for line in contents.lines() {
if let Some(val) = line.strip_prefix("ID=") {
if val.trim_matches('"').to_lowercase() == "alpine" {
return "musl";
}
}
}
}
"gnu"
}
fn detect_pkg_managers() -> Vec<PkgManager> {
let checks: &[(&str, PkgManager)] = &[
("apt", PkgManager::Apt),
("dnf", PkgManager::Dnf),
("pacman", PkgManager::Pacman),
("zypper", PkgManager::Zypper),
("brew", PkgManager::Brew),
("cargo", PkgManager::Cargo),
("port", PkgManager::Port),
];
checks
.iter()
.filter(|(bin, _)| which_exists(bin))
.map(|(_, pm)| *pm)
.collect()
}
pub fn print_platform_info(platform: &Platform) {
println!("\n{}", "Platform Detection".bright_cyan().bold());
println!(" OS: {}", platform.os.to_string().bold());
println!(" Architecture: {}", platform.arch.to_string().bold());
if let Some(ref d) = platform.distro {
println!(" Distro: {}", d.name.bold());
if !d.id_like.is_empty() {
println!(" Family: {}", d.id_like.dimmed());
}
}
let pm_names: Vec<String> = platform.pkg_managers.iter().map(|p| p.to_string()).collect();
println!(
" Pkg Managers: {}",
if pm_names.is_empty() {
"(none detected)".dimmed().to_string()
} else {
pm_names.join(", ").green().to_string()
}
);
}
#[derive(Debug)]
pub struct ToolStatus {
pub name: String,
pub binary: String,
pub available: bool,
pub version: Option<String>,
pub install_hint: String,
pub required_for: String,
}
struct ToolDescriptor {
name: &'static str,
binary: &'static str,
version_args: &'static [&'static str],
install_hint: &'static str,
required_for: &'static str,
}
fn tool_descriptors() -> Vec<ToolDescriptor> {
vec![
ToolDescriptor {
name: "apkeep",
binary: "apkeep",
version_args: &["--version"],
install_hint: "cargo install apkeep\n OR download from https://github.com/EFForg/apkeep/releases",
required_for: "APK downloading from Play Store / APKPure",
},
ToolDescriptor {
name: "jadx",
binary: "jadx",
version_args: &["--version"],
install_hint: "Download from https://github.com/skylot/jadx/releases\n Extract and add bin/ to PATH. Requires Java.",
required_for: "APK decompilation to Java source",
},
ToolDescriptor {
name: "Java Runtime",
binary: "java",
version_args: &["--version"],
install_hint: "sudo apt install default-jre\n OR install from https://adoptium.net",
required_for: "Required by jadx for decompilation",
},
ToolDescriptor {
name: "noseyparker",
binary: "noseyparker",
version_args: &["--version"],
install_hint: "cargo install noseyparker\n OR brew install noseyparker\n OR download from https://github.com/praetorian-inc/noseyparker/releases",
required_for: "Secret scanning of decompiled sources",
},
]
}
fn check_tool(desc: &ToolDescriptor) -> ToolStatus {
let available = which_exists(desc.binary);
let version = if available {
get_version(desc.binary, desc.version_args)
} else {
None
};
ToolStatus {
name: desc.name.to_string(),
binary: desc.binary.to_string(),
available,
version,
install_hint: desc.install_hint.to_string(),
required_for: desc.required_for.to_string(),
}
}
pub fn check_all() -> Vec<ToolStatus> {
tool_descriptors().iter().map(check_tool).collect()
}
#[allow(dead_code)]
pub fn all_available(statuses: &[ToolStatus]) -> bool {
statuses.iter().all(|s| s.available)
}
pub fn print_status_table(statuses: &[ToolStatus]) {
println!("\n{}", "Dependency Status".bright_cyan().bold());
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.apply_modifier(UTF8_ROUND_CORNERS);
table.set_header(vec![
Cell::new("Tool").fg(Color::Cyan),
Cell::new("Status"),
Cell::new("Version"),
Cell::new("Required For"),
]);
for s in statuses {
let (status_text, status_color) = if s.available {
("FOUND", Color::Green)
} else {
("MISSING", Color::Red)
};
table.add_row(vec![
Cell::new(&s.name).fg(Color::White),
Cell::new(status_text).fg(status_color),
Cell::new(s.version.as_deref().unwrap_or("—")),
Cell::new(&s.required_for),
]);
}
println!("{table}");
}
pub fn print_install_instructions(statuses: &[ToolStatus]) {
let missing: Vec<&ToolStatus> = statuses.iter().filter(|s| !s.available).collect();
if missing.is_empty() {
println!(
"\n{}",
"All dependencies are available. You're ready to go!"
.green()
.bold()
);
return;
}
println!(
"\n{} {} missing tool(s):",
"Setup Required:".yellow().bold(),
missing.len()
);
for tool in &missing {
println!();
println!(" {} {}", "▸".yellow(), tool.name.bold());
println!(" Binary: {}", tool.binary.dimmed());
println!(" Install:");
for line in tool.install_hint.lines() {
println!(" {}", line);
}
}
println!();
println!(
" {} Run {} to attempt automatic installation.",
"Tip:".yellow().bold(),
"flintbase setup --install".bold()
);
}
pub fn run_setup_check() {
let platform = detect_platform();
print_platform_info(&platform);
let statuses = check_all();
print_status_table(&statuses);
print_install_instructions(&statuses);
}
pub fn ensure_apk_tools_available() -> Result<(), String> {
let statuses = check_all();
let missing: Vec<&ToolStatus> = statuses.iter().filter(|s| !s.available).collect();
if missing.is_empty() {
return Ok(());
}
let names: Vec<&str> = missing.iter().map(|s| s.binary.as_str()).collect();
Err(format!(
"Missing required tools: {}. Run `flintbase setup --install` to auto-install, or `flintbase setup` for manual instructions.",
names.join(", ")
))
}
fn user_bin_dir() -> PathBuf {
let home = env::var("HOME").unwrap_or_else(|_| ".".to_string());
let dir = PathBuf::from(home).join(".local").join("bin");
let _ = fs::create_dir_all(&dir);
dir
}
fn user_opt_dir() -> PathBuf {
let home = env::var("HOME").unwrap_or_else(|_| ".".to_string());
let dir = PathBuf::from(home)
.join(".local")
.join("share")
.join("flintbase");
let _ = fs::create_dir_all(&dir);
dir
}
fn is_in_path(dir: &Path) -> bool {
if let Ok(path_var) = env::var("PATH") {
path_var
.split(':')
.any(|p| Path::new(p) == dir)
} else {
false
}
}
fn prompt_yes_no(question: &str) -> bool {
print!(" {} [Y/n] ", question);
let _ = io::stdout().flush();
let mut input = String::new();
if io::stdin().read_line(&mut input).is_err() {
return false;
}
let trimmed = input.trim().to_lowercase();
trimmed.is_empty() || trimmed == "y" || trimmed == "yes"
}
fn detect_shell_rc() -> (String, PathBuf) {
let shell = env::var("SHELL").unwrap_or_default();
let home = env::var("HOME").unwrap_or_else(|_| ".".to_string());
if shell.contains("zsh") {
("~/.zshrc".to_string(), PathBuf::from(&home).join(".zshrc"))
} else if shell.contains("fish") {
(
"~/.config/fish/config.fish".to_string(),
PathBuf::from(&home).join(".config/fish/config.fish"),
)
} else {
("~/.bashrc".to_string(), PathBuf::from(&home).join(".bashrc"))
}
}
fn rc_already_has_path(rc_path: &Path, dir: &Path) -> bool {
if let Ok(contents) = fs::read_to_string(rc_path) {
let dir_str = dir.to_string_lossy();
contents.contains(&*dir_str)
} else {
false
}
}
fn offer_add_to_path(dir: &Path) -> bool {
if is_in_path(dir) {
return false;
}
let (rc_display, rc_path) = detect_shell_rc();
if rc_already_has_path(&rc_path, dir) {
println!(
" {} {} is already in {} but not active in this shell.",
"Note:".yellow().bold(),
dir.display(),
rc_display
);
println!(
" Reload your shell or run: {}",
format!("source {}", rc_display).dimmed()
);
return false;
}
println!();
println!(
" {} {} is not in your PATH.",
"Note:".yellow().bold(),
dir.display()
);
let shell = env::var("SHELL").unwrap_or_default();
let is_fish = shell.contains("fish");
if prompt_yes_no(&format!("Add {} to your PATH in {}?", dir.display(), rc_display)) {
let line = if is_fish {
format!(
"\n# Added by flintbase setup\nfish_add_path {}\n",
dir.display()
)
} else {
format!(
"\n# Added by flintbase setup\nexport PATH=\"{}:$PATH\"\n",
dir.display()
)
};
if let Some(parent) = rc_path.parent() {
let _ = fs::create_dir_all(parent);
}
match fs::OpenOptions::new()
.create(true)
.append(true)
.open(&rc_path)
{
Ok(mut f) => {
if write!(f, "{}", line).is_ok() {
println!(
" {} Appended to {}",
"✓".green(),
rc_display
);
println!(
" Reload your shell or run: {}",
format!("source {}", rc_display).dimmed()
);
return true;
}
println!(
" {} Failed to write to {}",
"✗".red(),
rc_display
);
}
Err(e) => {
println!(
" {} Could not open {}: {}",
"✗".red(),
rc_display,
e
);
}
}
} else {
if is_fish {
println!(
" To add manually: {}",
format!("fish_add_path {}", dir.display()).dimmed()
);
} else {
println!(
" To add manually: {}",
format!(
"echo 'export PATH=\"{}:$PATH\"' >> {}",
dir.display(),
rc_display
)
.dimmed()
);
}
}
false
}
pub fn run_auto_install() {
let platform = detect_platform();
print_platform_info(&platform);
let statuses = check_all();
print_status_table(&statuses);
let missing: Vec<&ToolStatus> = statuses.iter().filter(|s| !s.available).collect();
if missing.is_empty() {
println!(
"\n{}",
"All dependencies are already installed. Nothing to do!"
.green()
.bold()
);
return;
}
println!(
"\n{} Attempting to install {} missing tool(s)...",
"Auto-Install:".bright_cyan().bold(),
missing.len()
);
let missing_names: Vec<String> = missing.iter().map(|s| s.binary.clone()).collect();
let order = ["java", "jadx", "apkeep", "noseyparker"];
let mut any_path_hint = false;
for tool_name in &order {
if !missing_names.iter().any(|n| n == tool_name) {
continue;
}
println!(
"\n{} {}",
"Installing:".bold(),
tool_name.bright_cyan().bold()
);
println!("{}", "─".repeat(50));
let result = match *tool_name {
"java" => install_java(&platform),
"jadx" => install_jadx(&platform),
"apkeep" => install_apkeep(&platform),
"noseyparker" => install_noseyparker(&platform),
_ => Err(format!("No auto-install handler for {}", tool_name)),
};
match result {
Ok(path_hint) => {
if which_exists(tool_name) {
println!(
" {} {} installed successfully!",
"✓".green().bold(),
tool_name
);
} else if let Some(ref dir) = path_hint {
println!(
" {} {} installed to {}",
"✓".green().bold(),
tool_name,
dir.display()
);
if offer_add_to_path(dir) {
any_path_hint = true;
}
} else {
println!(
" {} {} install command succeeded but binary not found in PATH",
"⚠".yellow(),
tool_name
);
}
}
Err(e) => {
println!(
" {} Failed to install {}: {}",
"✗".red().bold(),
tool_name,
e
);
}
}
}
let home = env::var("HOME").unwrap_or_else(|_| ".".to_string());
let common_dirs = [
PathBuf::from(&home).join(".cargo/bin"),
PathBuf::from(&home).join(".local/bin"),
];
for dir in &common_dirs {
if dir.exists() && !is_in_path(dir) {
if offer_add_to_path(dir) {
any_path_hint = true;
}
}
}
println!("\n{}", "─".repeat(50));
println!("{}", "Post-Install Verification".bright_cyan().bold());
let final_statuses = check_all();
print_status_table(&final_statuses);
let still_missing: Vec<&ToolStatus> =
final_statuses.iter().filter(|s| !s.available).collect();
if still_missing.is_empty() && !any_path_hint {
println!(
"\n{}",
"All tools installed successfully! You're ready to use `flintbase apk`."
.green()
.bold()
);
} else if still_missing.is_empty() && any_path_hint {
println!(
"\n{}",
"All tools installed! Reload your shell (or update PATH as shown above) and you're ready."
.green()
.bold()
);
} else {
let names: Vec<&str> = still_missing.iter().map(|s| s.binary.as_str()).collect();
println!(
"\n{} Still missing: {}",
"Warning:".yellow().bold(),
names.join(", ").red()
);
println!(" Please install them manually using the hints above.");
}
}
fn unzip_install_hint(platform: &Platform) -> &'static str {
if platform.has_pm(PkgManager::Apt) {
"sudo apt install unzip"
} else if platform.has_pm(PkgManager::Dnf) {
"sudo dnf install unzip"
} else if platform.has_pm(PkgManager::Pacman) {
"sudo pacman -S unzip"
} else if platform.has_pm(PkgManager::Zypper) {
"sudo zypper install unzip"
} else if platform.has_pm(PkgManager::Brew) {
"brew install unzip"
} else {
"install the 'unzip' package with your system package manager"
}
}
fn install_java(platform: &Platform) -> Result<Option<PathBuf>, String> {
match platform.os {
Os::Linux => {
if platform.has_pm(PkgManager::Apt) {
run_sudo_cmd("apt", &["install", "-y", "default-jre"])
} else if platform.has_pm(PkgManager::Dnf) {
run_sudo_cmd("dnf", &["install", "-y", "java-latest-openjdk"])
} else if platform.has_pm(PkgManager::Pacman) {
run_sudo_cmd("pacman", &["-S", "--noconfirm", "jre-openjdk"])
} else if platform.has_pm(PkgManager::Zypper) {
run_sudo_cmd("zypper", &["install", "-y", "java-21-openjdk"])
} else if platform.has_pm(PkgManager::Brew) {
run_cmd("brew", &["install", "openjdk"])
} else {
Err("No supported package manager found. Install Java from https://adoptium.net".into())
}
}
Os::MacOs => {
if platform.has_pm(PkgManager::Brew) {
run_cmd("brew", &["install", "openjdk"])
} else {
Err("Install Homebrew first (https://brew.sh), then: brew install openjdk".into())
}
}
_ => Err("Auto-install for Java not supported on this OS. Install from https://adoptium.net".into()),
}
}
fn install_jadx(platform: &Platform) -> Result<Option<PathBuf>, String> {
let opt_dir = user_opt_dir();
let jadx_dir = opt_dir.join("jadx");
let bin_dir = user_bin_dir();
println!(" Fetching latest jadx version from GitHub...");
let version = fetch_github_latest_tag("skylot", "jadx")
.map_err(|e| format!("Failed to query GitHub: {}", e))?;
let version_num = version.trim_start_matches('v');
let zip_name = format!("jadx-{}.zip", version_num);
let download_url = format!(
"https://github.com/skylot/jadx/releases/download/{}/{}",
version, zip_name
);
println!(" Downloading jadx {}...", version.bold());
let tmp_zip = opt_dir.join(&zip_name);
download_file(&download_url, &tmp_zip)?;
println!(" Extracting to {}...", jadx_dir.display());
if jadx_dir.exists() {
let _ = fs::remove_dir_all(&jadx_dir);
}
fs::create_dir_all(&jadx_dir).map_err(|e| e.to_string())?;
let unzip_ok = if which_exists("unzip") {
Command::new("unzip")
.arg("-o")
.arg("-q")
.arg(&tmp_zip)
.arg("-d")
.arg(&jadx_dir)
.status()
.map(|s| s.success())
.unwrap_or(false)
} else {
false
};
if !unzip_ok {
if which_exists("python3") {
let ok = Command::new("python3")
.arg("-m")
.arg("zipfile")
.arg("-e")
.arg(&tmp_zip)
.arg(&jadx_dir)
.status()
.map(|s| s.success())
.unwrap_or(false);
if !ok {
let _ = fs::remove_file(&tmp_zip);
return Err(format!(
"Failed to extract jadx ZIP. Install `unzip`: {}",
unzip_install_hint(platform)
));
}
} else {
let _ = fs::remove_file(&tmp_zip);
return Err(format!(
"No unzip or python3 available. Install `unzip`: {}",
unzip_install_hint(platform)
));
}
}
let _ = fs::remove_file(&tmp_zip);
let jadx_bin = jadx_dir.join("bin").join("jadx");
if jadx_bin.exists() {
let _ = set_executable(&jadx_bin);
let _ = set_executable(&jadx_dir.join("bin").join("jadx-gui"));
}
let link_path = bin_dir.join("jadx");
let _ = fs::remove_file(&link_path);
#[cfg(unix)]
{
std::os::unix::fs::symlink(&jadx_bin, &link_path)
.map_err(|e| format!("Failed to symlink: {}", e))?;
}
println!(
" Installed jadx {} → {}",
version,
link_path.display()
);
if platform.os == Os::Linux || platform.os == Os::MacOs {
Ok(Some(bin_dir))
} else {
Ok(None)
}
}
fn install_apkeep(platform: &Platform) -> Result<Option<PathBuf>, String> {
if platform.has_pm(PkgManager::Cargo) {
println!(" Installing via cargo install apkeep...");
let status = Command::new("cargo")
.arg("install")
.arg("apkeep")
.status()
.map_err(|e| e.to_string())?;
if status.success() {
return Ok(None); }
println!(
" {} cargo install failed, trying binary download...",
"⚠".yellow()
);
}
let target = match (platform.os, platform.arch) {
(Os::Linux, Arch::X86_64) => "x86_64-unknown-linux-gnu",
(Os::Linux, Arch::Aarch64) => "aarch64-unknown-linux-gnu",
(Os::MacOs, Arch::X86_64) => "x86_64-apple-darwin",
(Os::MacOs, Arch::Aarch64) => "aarch64-apple-darwin",
_ => return Err("No pre-built apkeep binary for this platform. Install Rust and use: cargo install apkeep".into()),
};
let version = fetch_github_latest_tag("EFForg", "apkeep")
.map_err(|e| format!("Failed to query GitHub: {}", e))?;
let version_num = version.trim_start_matches('v');
let asset_name = format!("apkeep-{}", target);
let download_url = format!(
"https://github.com/EFForg/apkeep/releases/download/{}/{}",
version_num, asset_name
);
println!(" Downloading apkeep {} for {}...", version_num.bold(), target);
let bin_dir = user_bin_dir();
let dest = bin_dir.join("apkeep");
download_file(&download_url, &dest)?;
set_executable(&dest)?;
println!(" Installed apkeep → {}", dest.display());
Ok(Some(bin_dir))
}
fn install_noseyparker(platform: &Platform) -> Result<Option<PathBuf>, String> {
if platform.has_pm(PkgManager::Brew) {
println!(" Installing via brew install noseyparker...");
let status = Command::new("brew")
.arg("install")
.arg("noseyparker")
.status()
.map_err(|e| e.to_string())?;
if status.success() {
return Ok(None);
}
println!(
" {} brew install failed, trying binary download...",
"⚠".yellow()
);
}
let target = match (platform.os, platform.arch) {
(Os::Linux, Arch::X86_64) => {
let libc = detect_linux_libc();
if libc == "musl" {
"x86_64-unknown-linux-musl"
} else {
"x86_64-unknown-linux-gnu"
}
}
(Os::Linux, Arch::Aarch64) => {
let libc = detect_linux_libc();
if libc == "musl" {
"aarch64-unknown-linux-musl"
} else {
"aarch64-unknown-linux-gnu"
}
}
(Os::MacOs, Arch::X86_64) => "x86_64-apple-darwin",
(Os::MacOs, Arch::Aarch64) => "aarch64-apple-darwin",
_ => {
return Err(
"No pre-built noseyparker binary for this platform. \
Install manually via: brew install noseyparker \
or build from source: https://github.com/praetorian-inc/noseyparker"
.into(),
)
}
};
let version = fetch_github_latest_tag("praetorian-inc", "noseyparker")
.map_err(|e| format!("Failed to query GitHub releases: {}", e))?;
let asset_name = format!("noseyparker-{}-{}.tar.gz", version, target);
let download_url = format!(
"https://github.com/praetorian-inc/noseyparker/releases/download/{}/{}",
version, asset_name
);
println!(
" Downloading noseyparker {} for {} ...",
version.bold(),
target
);
let opt_dir = user_opt_dir();
let tmp_tar = opt_dir.join(&asset_name);
download_file(&download_url, &tmp_tar)?;
let np_dir = opt_dir.join("noseyparker");
if np_dir.exists() {
let _ = fs::remove_dir_all(&np_dir);
}
let _ = fs::create_dir_all(&np_dir);
let extract_ok = Command::new("tar")
.arg("xzf")
.arg(&tmp_tar)
.arg("-C")
.arg(&np_dir)
.arg("--strip-components=1")
.status()
.map(|s| s.success())
.unwrap_or(false);
let _ = fs::remove_file(&tmp_tar);
if !extract_ok {
return Err("Failed to extract noseyparker tarball.".into());
}
let bin_dir = user_bin_dir();
let np_bin = find_binary_in_dir(&np_dir, "noseyparker");
if let Some(src) = np_bin {
set_executable(&src)?;
let dest = bin_dir.join("noseyparker");
let _ = fs::remove_file(&dest);
#[cfg(unix)]
{
std::os::unix::fs::symlink(&src, &dest)
.map_err(|e| format!("Failed to symlink: {}", e))?;
}
println!(
" {} Installed noseyparker → {}",
"✓".green(),
dest.display()
);
Ok(Some(bin_dir))
} else {
Err("Could not locate noseyparker binary in extracted archive.".into())
}
}
impl Platform {
fn has_pm(&self, pm: PkgManager) -> bool {
self.pkg_managers.contains(&pm)
}
}
fn which_exists(binary: &str) -> bool {
Command::new("which")
.arg(binary)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn get_version(binary: &str, args: &[&str]) -> Option<String> {
Command::new(binary)
.args(args)
.output()
.ok()
.and_then(|o| {
let out = String::from_utf8_lossy(&o.stdout).to_string();
let err = String::from_utf8_lossy(&o.stderr).to_string();
let combined = if out.trim().is_empty() { err } else { out };
combined.lines().next().map(|l| l.trim().to_string())
})
.filter(|s| !s.is_empty())
}
fn run_sudo_cmd(bin: &str, args: &[&str]) -> Result<Option<PathBuf>, String> {
println!(" Running: sudo {} {}", bin, args.join(" "));
let status = Command::new("sudo")
.arg(bin)
.args(args)
.status()
.map_err(|e| format!("Failed to run sudo {}: {}", bin, e))?;
if status.success() {
Ok(None)
} else {
Err(format!(
"sudo {} {} exited with code {:?}",
bin,
args.join(" "),
status.code()
))
}
}
fn run_cmd(bin: &str, args: &[&str]) -> Result<Option<PathBuf>, String> {
println!(" Running: {} {}", bin, args.join(" "));
let status = Command::new(bin)
.args(args)
.status()
.map_err(|e| format!("Failed to run {}: {}", bin, e))?;
if status.success() {
Ok(None)
} else {
Err(format!(
"{} {} exited with code {:?}",
bin,
args.join(" "),
status.code()
))
}
}
fn fetch_github_latest_tag(owner: &str, repo: &str) -> Result<String, String> {
let url = format!(
"https://api.github.com/repos/{}/{}/releases/latest",
owner, repo
);
let client = reqwest::blocking::Client::builder()
.user_agent("flintbase-setup/0.1")
.timeout(std::time::Duration::from_secs(15))
.build()
.map_err(|e| e.to_string())?;
let resp = client.get(&url).send().map_err(|e| e.to_string())?;
if !resp.status().is_success() {
return Err(format!("GitHub API returned {}", resp.status()));
}
let data: serde_json::Value = resp.json().map_err(|e| e.to_string())?;
data.get("tag_name")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| "No tag_name in GitHub release response".into())
}
fn download_file(url: &str, dest: &Path) -> Result<(), String> {
if which_exists("curl") {
let status = Command::new("curl")
.arg("-L") .arg("-f") .arg("-#") .arg("-o")
.arg(dest)
.arg(url)
.status()
.map_err(|e| format!("Failed to run curl: {}", e))?;
if status.success() {
return Ok(());
}
}
let client = reqwest::blocking::Client::builder()
.user_agent("flintbase-setup/0.1")
.timeout(std::time::Duration::from_secs(120))
.build()
.map_err(|e| e.to_string())?;
let resp = client
.get(url)
.send()
.map_err(|e| format!("Download failed: {}", e))?;
if !resp.status().is_success() {
return Err(format!("Download returned HTTP {}", resp.status()));
}
let bytes = resp.bytes().map_err(|e| e.to_string())?;
fs::write(dest, &bytes).map_err(|e| format!("Failed to write {}: {}", dest.display(), e))?;
Ok(())
}
#[cfg(unix)]
fn set_executable(path: &Path) -> Result<(), String> {
use std::os::unix::fs::PermissionsExt;
let meta = fs::metadata(path).map_err(|e| e.to_string())?;
let mut perms = meta.permissions();
perms.set_mode(perms.mode() | 0o755);
fs::set_permissions(path, perms).map_err(|e| e.to_string())
}
#[cfg(not(unix))]
fn set_executable(_path: &Path) -> Result<(), String> {
Ok(()) }
fn find_binary_in_dir(dir: &Path, name: &str) -> Option<PathBuf> {
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(found) = find_binary_in_dir(&path, name) {
return Some(found);
}
} else if let Some(fname) = path.file_name().and_then(|n| n.to_str()) {
if fname == name {
return Some(path);
}
}
}
}
None
}