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.
This commit is contained in:
11
release.sh
11
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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user