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")
|
NOTES=$(cat "$NOTES_FILE")
|
||||||
echo "→ Release notes: $NOTES_FILE"
|
echo "→ Release notes: $NOTES_FILE"
|
||||||
|
|
||||||
# ── Gitea token ───────────────────────────────────────────────────────────────
|
# ── Gitea token — from macOS Keychain, env var, or prompt ────────────────────
|
||||||
if [ -z "${GITEA_TOKEN:-}" ]; then
|
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
|
echo
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import kotlin.io.path.absolutePathString
|
|||||||
* Install via: brew install android-platform-tools
|
* Install via: brew install android-platform-tools
|
||||||
*/
|
*/
|
||||||
class AndroidConnector(
|
class AndroidConnector(
|
||||||
private val adbBin: String = "adb",
|
private val adbBin: String = com.bolenpad.photophetch.util.ExternalTools.resolve("adb"),
|
||||||
) : DeviceConnector {
|
) : DeviceConnector {
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(AndroidConnector::class.java)
|
private val log = LoggerFactory.getLogger(AndroidConnector::class.java)
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ class Copier(
|
|||||||
*/
|
*/
|
||||||
private fun readExifMake(file: Path): String? {
|
private fun readExifMake(file: Path): String? {
|
||||||
return try {
|
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
|
.redirectErrorStream(false) // keep stderr separate
|
||||||
.start()
|
.start()
|
||||||
val stdout = process.inputStream.bufferedReader().readText()
|
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.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.bolenpad.photophetch.model.DeviceInfo
|
import com.bolenpad.photophetch.model.DeviceInfo
|
||||||
|
import com.bolenpad.photophetch.util.ExternalTools
|
||||||
import com.bolenpad.photophetch.model.DeviceType
|
import com.bolenpad.photophetch.model.DeviceType
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -49,7 +50,8 @@ fun ConnectScreen(state: AppState) {
|
|||||||
// exiftool warning — checked once per session
|
// exiftool warning — checked once per session
|
||||||
val exiftoolMissing = remember {
|
val exiftoolMissing = remember {
|
||||||
try {
|
try {
|
||||||
val p = ProcessBuilder("exiftool", "-ver").redirectErrorStream(true).start()
|
val p = ProcessBuilder(ExternalTools.resolve("exiftool"), "-ver")
|
||||||
|
.redirectErrorStream(true).start()
|
||||||
p.inputStream.readBytes()
|
p.inputStream.readBytes()
|
||||||
p.waitFor() != 0
|
p.waitFor() != 0
|
||||||
} catch (_: Exception) { true }
|
} 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