Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src-tauri/src/audio/recorder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,10 +365,10 @@ impl AudioRecorder {
}

pub fn check_dependencies() -> (bool, String) {
if crate::utils::swift_binary::ensure_swift_binary("record", "scripts/record.swift") {
if crate::utils::swift_binary::get_binary_path("record").exists() {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor / FYI — right call for the log spam, but note the behavior change: check_dependencies no longer compiles, so on first launch it reports false for the window while ensure_swift_binary_async (lib.rs:2203) is still building the helper. Transient false negative in the health panel / tray until the async compile lands. Acceptable as-is, just shouldn't be a surprise later.

(true, "native audio recorder ready".into())
} else {
(false, "Audio recorder unavailable. Install Xcode Command Line Tools: xcode-select --install".into())
(false, "Audio recorder binary not found. Reinstall Echo or install Xcode Command Line Tools: xcode-select --install".into())
}
}

Expand Down
27 changes: 23 additions & 4 deletions src-tauri/src/transcription/whisper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,13 @@ pub async fn build_binary(progress_cb: impl Fn(&str) + Send + 'static) -> Result
return Ok(());
}

if !cmd_exists("git") {
return Err("git is not installed. Install Xcode Command Line Tools: xcode-select --install".into());
}
if !cmd_exists("cmake") {
return Err("cmake is not installed. Install it with: brew install cmake".into());
}

let tmp_dir = std::env::temp_dir().join("echo-whisper-build");
fs::create_dir_all(bin_dir()).map_err(|e| e.to_string())?;
fs::create_dir_all(&tmp_dir).map_err(|e| e.to_string())?;
Expand Down Expand Up @@ -443,15 +450,27 @@ pub async fn build_binary(progress_cb: impl Fn(&str) + Send + 'static) -> Result
Ok(())
}

fn run_cmd(cmd: &str, args: &[&str], cwd: &Path) -> Result<(), String> {
let path_env = format!(
fn build_path_env() -> String {
format!(
"{}:/opt/homebrew/bin:/usr/local/bin",
std::env::var("PATH").unwrap_or_default()
);
)
}

fn cmd_exists(name: &str) -> bool {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker — this ignores the PATH augmentation that every other subprocess spawn in the codebase does, so the new guard will falsely block working builds.

run_cmd directly below adds /opt/homebrew/bin:/usr/local/bin (same in parakeet.rs:283, refinement/cli.rs:10, codebase/analyzer.rs:62) precisely because a Finder-launched .app inherits /usr/bin:/bin:/usr/sbin:/sbin. cmd_exists inherits that bare PATH, so a Homebrew cmake at /opt/homebrew/bin/cmake is invisible and the check returns "cmake is not installed" on a machine where run_cmd would have found it fine. That's a regression, and it lands on exactly the packaged users this PR targets.

Hoist the PATH the file already builds:

fn build_path_env() -> String {
    format!("{}:/opt/homebrew/bin:/usr/local/bin", std::env::var("PATH").unwrap_or_default())
}

fn cmd_exists(name: &str) -> bool {
    Command::new(name)
        .arg("--version")
        .env("PATH", build_path_env())
        .output()
        .map(|o| o.status.success())
        .unwrap_or(false)
}

…and have run_cmd use build_path_env() too.

Nit while here: on a Mac without CLT, cmd_exists("git") runs the /usr/bin/git shim, which pops the system "install command line tools" dialog as a side effect of the check. Probably fine — arguably helpful — just worth knowing it isn't a silent probe.

Command::new(name)
.arg("--version")
.env("PATH", build_path_env())
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}

fn run_cmd(cmd: &str, args: &[&str], cwd: &Path) -> Result<(), String> {
let output = Command::new(cmd)
.args(args)
.current_dir(cwd)
.env("PATH", &path_env)
.env("PATH", build_path_env())
.output()
.map_err(|e| format!("{} failed: {}", cmd, e))?;

Expand Down
28 changes: 25 additions & 3 deletions src-tauri/src/utils/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ pub fn to_user_facing_error(err: &str) -> String {
let lower = err.to_lowercase();

if lower.contains("audio recorder") || lower.contains("record.swift") || lower.contains("rec: command not found") {
return "Audio recording is unavailable. Grant microphone access in System Settings → Privacy & Security → Microphone (and install Xcode Command Line Tools if prompted).".into();
if lower.contains("unavailable") || lower.contains("not found") || lower.contains("command not found") || lower.contains("no such file") {
return "Audio recorder binary is missing. Reinstall Echo or install Xcode Command Line Tools (xcode-select --install) and restart.".into();
}
return "Audio recording failed. Grant microphone access in System Settings → Privacy & Security → Microphone.".into();
}
if lower.contains("whisper") && (lower.contains("not found") || lower.contains("not ready")) {
return "Whisper is not set up. Open Settings and build/download Whisper.".into();
Expand Down Expand Up @@ -47,8 +50,27 @@ mod tests {
// Mirrors tests/errors.test.ts. The TS side wraps input in `new Error(...)`
// and extracts `.message`; the Rust API takes the message string directly.
#[test]
fn maps_audio_recorder_errors() {
assert!(to_user_facing_error("rec: command not found").contains("Audio recording"));
fn maps_audio_recorder_missing_binary() {
let result = to_user_facing_error("rec: command not found");
assert!(result.contains("Audio recorder binary is missing"), "got: {}", result);
}

#[test]
fn maps_audio_recorder_binary_unavailable() {
let result = to_user_facing_error("Native audio recorder unavailable (failed to compile record.swift)");
assert!(result.contains("missing"), "got: {}", result);
}

#[test]
fn maps_audio_recorder_spawn_no_such_file() {
let result = to_user_facing_error("Failed to start native audio recorder: No such file or directory (os error 2)");
assert!(result.contains("missing"), "got: {}", result);
}

#[test]
fn maps_audio_recorder_permission_errors() {
let result = to_user_facing_error("audio recorder permission denied");
assert!(result.contains("Microphone"), "got: {}", result);
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/utils/swift_binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ pub fn ensure_swift_binary(binary_name: &str, source_relative_path: &str) -> boo
Some(p) => p,
None => {
if binary_path.exists() { return true; }
log::warn!("[swift-binary] Source not found for {}", binary_name);
log::debug!("[swift-binary] Source not found for {}", binary_name);
return false;
}
};
Expand Down