@@ -3,7 +3,7 @@ use std::fs;
33use std:: io:: { BufRead , BufReader , Read } ;
44use std:: path:: { Path , PathBuf } ;
55use std:: process:: { Command , Stdio } ;
6- use std:: sync:: Mutex ;
6+ use std:: sync:: { Mutex , OnceLock } ;
77use tauri:: { AppHandle , Emitter , Manager } ;
88
99// ── Shared helpers ─────────────────────────────────────────────────────────────
@@ -135,26 +135,82 @@ pub struct UploadMeta {
135135 language : String ,
136136}
137137
138- /// Confirm the `ia` CLI is installed, returning a friendly error otherwise.
139- fn ensure_ia ( ) -> Result < ( ) , String > {
140- Command :: new ( "ia" )
138+ /// Locate the `ia` executable. GUI apps launched from Finder/Dock on macOS do
139+ /// not inherit the user's shell `PATH`, so a bare `Command::new("ia")` fails to
140+ /// find `ia` when it lives somewhere like `~/.local/bin` or a Homebrew prefix.
141+ /// Resolve the absolute path once: prefer whatever a login shell reports (it
142+ /// sources the user's profile and full PATH), then fall back to common install
143+ /// locations. Returns `None` only if `ia` genuinely can't be found.
144+ fn ia_bin ( ) -> Option < & ' static str > {
145+ static CACHE : OnceLock < Option < String > > = OnceLock :: new ( ) ;
146+ CACHE . get_or_init ( resolve_ia) . as_deref ( )
147+ }
148+
149+ fn resolve_ia ( ) -> Option < String > {
150+ // 1. Already on PATH (e.g. `cargo tauri dev` launched from a terminal).
151+ let on_path = Command :: new ( "ia" )
141152 . arg ( "--version" )
142153 . stdout ( Stdio :: null ( ) )
143154 . stderr ( Stdio :: null ( ) )
144155 . status ( )
145- . map_err ( |_| {
146- "The 'ia' CLI is not installed.\n Install it with: pip install internetarchive"
147- . to_string ( )
148- } ) ?;
149- Ok ( ( ) )
156+ . map ( |s| s. success ( ) )
157+ . unwrap_or ( false ) ;
158+ if on_path {
159+ return Some ( "ia" . to_string ( ) ) ;
160+ }
161+
162+ // 2. Ask the user's login shell, which loads their profile and real PATH.
163+ if let Ok ( shell) = std:: env:: var ( "SHELL" ) {
164+ if let Ok ( out) = Command :: new ( & shell) . args ( [ "-lic" , "command -v ia" ] ) . output ( ) {
165+ // Profile scripts may print noise; take the last line that is a
166+ // real, existing path.
167+ let resolved = String :: from_utf8_lossy ( & out. stdout )
168+ . lines ( )
169+ . map ( str:: trim)
170+ . filter ( |l| !l. is_empty ( ) )
171+ . rfind ( |l| Path :: new ( l) . is_file ( ) )
172+ . map ( str:: to_string) ;
173+ if resolved. is_some ( ) {
174+ return resolved;
175+ }
176+ }
177+ }
178+
179+ // 3. Probe common install locations directly.
180+ let home = std:: env:: var ( "HOME" ) . unwrap_or_default ( ) ;
181+ let mut candidates = vec ! [
182+ format!( "{home}/.local/bin/ia" ) ,
183+ "/opt/homebrew/bin/ia" . to_string( ) ,
184+ "/usr/local/bin/ia" . to_string( ) ,
185+ "/usr/bin/ia" . to_string( ) ,
186+ ] ;
187+ // pip `--user` installs land under ~/Library/Python/<ver>/bin on macOS.
188+ if let Ok ( entries) = fs:: read_dir ( format ! ( "{home}/Library/Python" ) ) {
189+ for e in entries. flatten ( ) {
190+ candidates. push ( e. path ( ) . join ( "bin" ) . join ( "ia" ) . to_string_lossy ( ) . to_string ( ) ) ;
191+ }
192+ }
193+ candidates. into_iter ( ) . find ( |p| Path :: new ( p) . is_file ( ) )
194+ }
195+
196+ /// A `Command` for the resolved `ia` binary, or a friendly error if it's missing.
197+ fn ia_command ( ) -> Result < Command , String > {
198+ let bin = ia_bin ( ) . ok_or (
199+ "The 'ia' CLI is not installed.\n Install it with: pip install internetarchive" ,
200+ ) ?;
201+ Ok ( Command :: new ( bin) )
202+ }
203+
204+ /// Confirm the `ia` CLI can be found, returning a friendly error otherwise.
205+ fn ensure_ia ( ) -> Result < ( ) , String > {
206+ ia_command ( ) . map ( |_| ( ) )
150207}
151208
152209/// Sign in once per batch: write the archive.org S3 keys via `ia configure`.
153210#[ tauri:: command]
154211pub async fn configure_account ( username : String , password : String ) -> Result < ( ) , String > {
155212 tauri:: async_runtime:: spawn_blocking ( move || {
156- ensure_ia ( ) ?;
157- let cfg = Command :: new ( "ia" )
213+ let cfg = ia_command ( ) ?
158214 . args ( [
159215 "configure" ,
160216 & format ! ( "--username={}" , username) ,
@@ -249,12 +305,11 @@ pub struct ItemInfo {
249305#[ tauri:: command]
250306pub async fn inspect_item ( identifier : String ) -> Result < ItemInfo , String > {
251307 tauri:: async_runtime:: spawn_blocking ( move || {
252- ensure_ia ( ) ?;
253308 let id = identifier. trim ( ) ;
254309 if id. is_empty ( ) {
255310 return Err ( "No identifier provided." . to_string ( ) ) ;
256311 }
257- let out = Command :: new ( "ia" )
312+ let out = ia_command ( ) ?
258313 . args ( [ "metadata" , id] )
259314 . output ( )
260315 . map_err ( |e| format ! ( "ia metadata failed: {e}" ) ) ?;
@@ -376,7 +431,7 @@ fn upload_blocking(app: &AppHandle, meta: &UploadMeta, files: &[String]) -> Resu
376431 args. push ( "--checksum" . into ( ) ) ;
377432 args. push ( "--retries=10" . into ( ) ) ;
378433
379- let mut child = Command :: new ( "ia" )
434+ let mut child = ia_command ( ) ?
380435 . args ( & args)
381436 . stdout ( Stdio :: piped ( ) )
382437 . stderr ( Stdio :: piped ( ) )
0 commit comments