From b416e528b0c5440ca9845d690f40d3956e195a09 Mon Sep 17 00:00:00 2001 From: Kyle Bolen Date: Sun, 2 Aug 2026 18:12:53 +0000 Subject: [PATCH] Fix tool resolution for packaged .app bundle .app bundles launch with minimal PATH (/usr/bin:/bin only), missing Homebrew paths. ExternalTools.resolve() searches: - /usr/local/bin (Homebrew Intel) - /opt/homebrew/bin (Homebrew Apple Silicon) - ~/.local/bin (pipx installs) Updated: exiftool (ConnectScreen + Copier), adb (AndroidConnector). This fixes 'exiftool not installed' warning when launching as .app. --- release.sh | 11 +++- .../photophetch/device/AndroidConnector.kt | 2 +- .../bolenpad/photophetch/transfer/Copier.kt | 2 +- .../bolenpad/photophetch/ui/ConnectScreen.kt | 4 +- .../photophetch/util/ExternalTools.kt | 56 +++++++++++++++++++ 5 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/com/bolenpad/photophetch/util/ExternalTools.kt diff --git a/release.sh b/release.sh index de81b35..0ca3bdd 100755 --- a/release.sh +++ b/release.sh @@ -39,9 +39,16 @@ fi NOTES=$(cat "$NOTES_FILE") echo "→ Release notes: $NOTES_FILE" -# ── Gitea token ─────────────────────────────────────────────────────────────── +# ── Gitea token — from macOS Keychain, env var, or prompt ──────────────────── if [ -z "${GITEA_TOKEN:-}" ]; then - read -rsp "GITEA_TOKEN not set. Enter token: " GITEA_TOKEN + # Try macOS Keychain first + GITEA_TOKEN=$(security find-generic-password -a kbolen -s gitea-photophetch -w 2>/dev/null || echo "") +fi +if [ -z "${GITEA_TOKEN:-}" ]; then + echo "No token found in Keychain or environment." + echo "Store it once with:" + echo " security add-generic-password -a kbolen -s gitea-photophetch -w YOUR_TOKEN" + read -rsp "Or enter token now: " GITEA_TOKEN echo fi diff --git a/src/main/kotlin/com/bolenpad/photophetch/device/AndroidConnector.kt b/src/main/kotlin/com/bolenpad/photophetch/device/AndroidConnector.kt index f99b43e..48ca4bf 100644 --- a/src/main/kotlin/com/bolenpad/photophetch/device/AndroidConnector.kt +++ b/src/main/kotlin/com/bolenpad/photophetch/device/AndroidConnector.kt @@ -19,7 +19,7 @@ import kotlin.io.path.absolutePathString * Install via: brew install android-platform-tools */ class AndroidConnector( - private val adbBin: String = "adb", + private val adbBin: String = com.bolenpad.photophetch.util.ExternalTools.resolve("adb"), ) : DeviceConnector { private val log = LoggerFactory.getLogger(AndroidConnector::class.java) diff --git a/src/main/kotlin/com/bolenpad/photophetch/transfer/Copier.kt b/src/main/kotlin/com/bolenpad/photophetch/transfer/Copier.kt index d52acb1..857c3b8 100644 --- a/src/main/kotlin/com/bolenpad/photophetch/transfer/Copier.kt +++ b/src/main/kotlin/com/bolenpad/photophetch/transfer/Copier.kt @@ -247,7 +247,7 @@ class Copier( */ private fun readExifMake(file: Path): String? { return try { - val process = ProcessBuilder("exiftool", "-Make", "-s3", file.absolutePathString()) + val process = ProcessBuilder(com.bolenpad.photophetch.util.ExternalTools.resolve("exiftool"), "-Make", "-s3", file.absolutePathString()) .redirectErrorStream(false) // keep stderr separate .start() val stdout = process.inputStream.bufferedReader().readText() diff --git a/src/main/kotlin/com/bolenpad/photophetch/ui/ConnectScreen.kt b/src/main/kotlin/com/bolenpad/photophetch/ui/ConnectScreen.kt index 658d7e1..b40cd70 100644 --- a/src/main/kotlin/com/bolenpad/photophetch/ui/ConnectScreen.kt +++ b/src/main/kotlin/com/bolenpad/photophetch/ui/ConnectScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.bolenpad.photophetch.model.DeviceInfo +import com.bolenpad.photophetch.util.ExternalTools import com.bolenpad.photophetch.model.DeviceType @Composable @@ -49,7 +50,8 @@ fun ConnectScreen(state: AppState) { // exiftool warning — checked once per session val exiftoolMissing = remember { try { - val p = ProcessBuilder("exiftool", "-ver").redirectErrorStream(true).start() + val p = ProcessBuilder(ExternalTools.resolve("exiftool"), "-ver") + .redirectErrorStream(true).start() p.inputStream.readBytes() p.waitFor() != 0 } catch (_: Exception) { true } diff --git a/src/main/kotlin/com/bolenpad/photophetch/util/ExternalTools.kt b/src/main/kotlin/com/bolenpad/photophetch/util/ExternalTools.kt new file mode 100644 index 0000000..7e8402c --- /dev/null +++ b/src/main/kotlin/com/bolenpad/photophetch/util/ExternalTools.kt @@ -0,0 +1,56 @@ +package com.bolenpad.photophetch.util + +import java.io.File + +/** + * Resolves paths for external command-line tools (exiftool, adb, python3, etc.) + * + * macOS .app bundles launch with a minimal PATH (/usr/bin:/bin:/usr/sbin:/sbin) + * that doesn't include Homebrew. This utility searches common Homebrew install + * locations so tools work whether launched from the command line or as a .app. + */ +object ExternalTools { + + /** Common binary locations to search, in priority order */ + private val searchPaths = listOf( + "/usr/local/bin", // Homebrew on Intel Mac + "/opt/homebrew/bin", // Homebrew on Apple Silicon + "/usr/bin", // System tools + "/bin", + "${System.getProperty("user.home")}/.local/bin", // pipx and user installs + "/usr/local/opt/exiftool/bin", // Homebrew formula-specific + ) + + /** + * Find the full path to a tool binary. + * Returns the full path if found in a known location, or just the bare + * name (relying on PATH) if not found — which works for command-line launches. + */ + fun resolve(toolName: String): String { + for (dir in searchPaths) { + val file = File(dir, toolName) + if (file.exists() && file.canExecute()) { + return file.absolutePath + } + } + return toolName // fall back to PATH lookup + } + + /** + * Check if a tool is available (either in known paths or on PATH). + */ + fun isAvailable(toolName: String): Boolean { + val path = resolve(toolName) + if (path != toolName) return true // found at explicit path + + // Try running it to check PATH + return try { + val p = ProcessBuilder(toolName, "--version") + .redirectErrorStream(true).start() + p.inputStream.readBytes() + p.waitFor() == 0 || true // any exit = it exists + } catch (_: Exception) { + false + } + } +}