From 2d4c2355224135e559b8d438a28291906df0fb96 Mon Sep 17 00:00:00 2001 From: 1233day <1233day@naver.com> Date: Tue, 24 Mar 2026 09:44:19 +0900 Subject: [PATCH 1/2] Add support for additional Homebrew home on macOS Add /opt/homebrew/bin as an additional macOS fallback location when starting external processes. The previous fallback assumed /usr/local/bin only, which can fail on Apple Silicon Homebrew setups in restricted PATH environments (for example, IDE or UI-launched processes). Update CredentialHelperTests to verify both macOS fallback paths are attempted. See gh-49721 Signed-off-by: 1233day <1233day@naver.com> --- .../configuration/CredentialHelper.java | 28 ++++++++++----- .../configuration/CredentialHelperTests.java | 4 ++- .../docker/compose/core/ProcessRunner.java | 25 ++++++++++--- ...DisabledIfProcessUnavailableCondition.java | 36 +++++++++++++------ 4 files changed, 68 insertions(+), 25 deletions(-) diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java index 6169667708d..44364674b17 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java @@ -41,6 +41,10 @@ class CredentialHelper { private static final String USR_LOCAL_BIN = "/usr/local/bin/"; + private static final String OPT_HOMEBREW_BIN = "/opt/homebrew/bin/"; + + private static final String[] MAC_OS_BIN_DIRECTORIES = { OPT_HOMEBREW_BIN, USR_LOCAL_BIN }; + private static final Set CREDENTIAL_NOT_FOUND_MESSAGES = Set.of("credentials not found in native keychain", "no credentials server URL", "no credentials username"); @@ -92,16 +96,22 @@ class CredentialHelper { if (!Platform.isMac()) { throw ex; } - try { - List command = new ArrayList<>(processBuilder.command()); - command.set(0, USR_LOCAL_BIN + command.get(0)); - return processBuilder.command(command).start(); - } - catch (Exception suppressed) { - // Suppresses the exception and rethrows the original exception - ex.addSuppressed(suppressed); - throw ex; + String executable = processBuilder.command().get(0); + for (String binDirectory : MAC_OS_BIN_DIRECTORIES) { + try { + List command = new ArrayList<>(processBuilder.command()); + if (executable.startsWith(binDirectory)) { + continue; + } + command.set(0, binDirectory + executable); + return processBuilder.command(command).start(); + } + catch (Exception suppressed) { + // Suppresses the exception and rethrows the original exception + ex.addSuppressed(suppressed); + } } + throw ex; } } diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java index 84a14ddd7f5..1c7ff4da54c 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java @@ -106,7 +106,9 @@ class CredentialHelperTests { .satisfies((ex) -> { if (Platform.isMac()) { assertThat(ex.getMessage()).doesNotContain("/usr/local/bin/"); - assertThat(ex.getSuppressed()).allSatisfy((suppressed) -> assertThat(suppressed) + assertThat(ex.getSuppressed()).anySatisfy((suppressed) -> assertThat(suppressed) + .hasMessageContaining("/opt/homebrew/bin/" + executable)); + assertThat(ex.getSuppressed()).anySatisfy((suppressed) -> assertThat(suppressed) .hasMessageContaining("/usr/local/bin/" + executable)); } }); diff --git a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java index 9a603aeceda..03a337367a7 100644 --- a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java +++ b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java @@ -44,6 +44,10 @@ class ProcessRunner { private static final String USR_LOCAL_BIN = "/usr/local/bin"; + private static final String OPT_HOMEBREW_BIN = "/opt/homebrew/bin"; + + private static final String[] MAC_OS_BIN_DIRECTORIES = { OPT_HOMEBREW_BIN, USR_LOCAL_BIN }; + private static final boolean MAC_OS = System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("mac"); private static final Log logger = LogFactory.getLog(ProcessRunner.class); @@ -108,11 +112,22 @@ class ProcessRunner { } catch (IOException ex) { String path = processBuilder.environment().get("PATH"); - if (MAC_OS && path != null && !path.contains(USR_LOCAL_BIN) - && !command[0].startsWith(USR_LOCAL_BIN + "/")) { - String[] localCommand = command.clone(); - localCommand[0] = USR_LOCAL_BIN + "/" + localCommand[0]; - return startProcess(localCommand); + if (MAC_OS && path != null) { + for (String binDirectory : MAC_OS_BIN_DIRECTORIES) { + if (path.contains(binDirectory) || command[0].startsWith(binDirectory + "/")) { + continue; + } + String[] localCommand = command.clone(); + localCommand[0] = binDirectory + "/" + command[0]; + ProcessBuilder localProcessBuilder = new ProcessBuilder(localCommand); + localProcessBuilder.directory(this.workingDirectory); + try { + return localProcessBuilder.start(); + } + catch (IOException suppressed) { + ex.addSuppressed(suppressed); + } + } } throw new ProcessStartException(command, ex); } diff --git a/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableCondition.java b/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableCondition.java index 6996e38c06d..397f73faf2e 100644 --- a/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableCondition.java +++ b/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableCondition.java @@ -43,6 +43,10 @@ class DisabledIfProcessUnavailableCondition implements ExecutionCondition { private static final String USR_LOCAL_BIN = "/usr/local/bin"; + private static final String OPT_HOMEBREW_BIN = "/opt/homebrew/bin"; + + private static final String[] MAC_OS_BIN_DIRECTORIES = { OPT_HOMEBREW_BIN, USR_LOCAL_BIN }; + private static final boolean MAC_OS = System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("mac"); @Override @@ -68,23 +72,35 @@ class DisabledIfProcessUnavailableCondition implements ExecutionCondition { private void check(String[] command) { ProcessBuilder processBuilder = new ProcessBuilder(command); try { - Process process = processBuilder.start(); - Assert.state(process.waitFor(30, TimeUnit.SECONDS), "Process did not exit within 30 seconds"); - Assert.state(process.exitValue() == 0, () -> "Process exited with %d".formatted(process.exitValue())); - process.destroy(); + check(processBuilder); } catch (Exception ex) { String path = processBuilder.environment().get("PATH"); - if (MAC_OS && path != null && !path.contains(USR_LOCAL_BIN) - && !command[0].startsWith(USR_LOCAL_BIN + "/")) { - String[] localCommand = command.clone(); - localCommand[0] = USR_LOCAL_BIN + "/" + localCommand[0]; - check(localCommand); - return; + if (MAC_OS && path != null) { + for (String binDirectory : MAC_OS_BIN_DIRECTORIES) { + if (path.contains(binDirectory) || command[0].startsWith(binDirectory + "/")) { + continue; + } + String[] localCommand = command.clone(); + localCommand[0] = binDirectory + "/" + command[0]; + try { + check(new ProcessBuilder(localCommand)); + return; + } + catch (Exception ignored) { + } + } } throw new RuntimeException( "Unable to start process '%s'".formatted(StringUtils.arrayToDelimitedString(command, " "))); } } + private void check(ProcessBuilder processBuilder) throws Exception { + Process process = processBuilder.start(); + Assert.state(process.waitFor(30, TimeUnit.SECONDS), "Process did not exit within 30 seconds"); + Assert.state(process.exitValue() == 0, () -> "Process exited with %d".formatted(process.exitValue())); + process.destroy(); + } + } From 1519bcf1e237a5430e8a6676fae41577c921eac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 3 Apr 2026 13:18:05 +0200 Subject: [PATCH 2/2] Polish "Add support for additional Homebrew home on macOS" See gh-49721 --- .../docker/configuration/CredentialHelper.java | 11 ++++------- .../docker/configuration/CredentialHelperTests.java | 9 +++++---- .../boot/docker/compose/core/ProcessRunner.java | 2 +- .../DisabledIfProcessUnavailableCondition.java | 5 +++-- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java index 44364674b17..e2786a90bb0 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java @@ -96,18 +96,15 @@ class CredentialHelper { if (!Platform.isMac()) { throw ex; } - String executable = processBuilder.command().get(0); + List originalCommand = List.copyOf(processBuilder.command()); + String executable = originalCommand.get(0); for (String binDirectory : MAC_OS_BIN_DIRECTORIES) { + List command = new ArrayList<>(originalCommand); + command.set(0, binDirectory + executable); try { - List command = new ArrayList<>(processBuilder.command()); - if (executable.startsWith(binDirectory)) { - continue; - } - command.set(0, binDirectory + executable); return processBuilder.command(command).start(); } catch (Exception suppressed) { - // Suppresses the exception and rethrows the original exception ex.addSuppressed(suppressed); } } diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java index 1c7ff4da54c..6a8b8251c5d 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java @@ -106,10 +106,11 @@ class CredentialHelperTests { .satisfies((ex) -> { if (Platform.isMac()) { assertThat(ex.getMessage()).doesNotContain("/usr/local/bin/"); - assertThat(ex.getSuppressed()).anySatisfy((suppressed) -> assertThat(suppressed) - .hasMessageContaining("/opt/homebrew/bin/" + executable)); - assertThat(ex.getSuppressed()).anySatisfy((suppressed) -> assertThat(suppressed) - .hasMessageContaining("/usr/local/bin/" + executable)); + assertThat(ex.getSuppressed()).satisfiesExactlyInAnyOrder( + (suppressed) -> assertThat(suppressed) + .hasMessageContaining("/opt/homebrew/bin/" + executable), + (suppressed) -> assertThat(suppressed) + .hasMessageContaining("/usr/local/bin/" + executable)); } }); } diff --git a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java index 03a337367a7..1e82bd68be4 100644 --- a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java +++ b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java @@ -114,7 +114,7 @@ class ProcessRunner { String path = processBuilder.environment().get("PATH"); if (MAC_OS && path != null) { for (String binDirectory : MAC_OS_BIN_DIRECTORIES) { - if (path.contains(binDirectory) || command[0].startsWith(binDirectory + "/")) { + if (path.contains(binDirectory) || command[0].startsWith("/")) { continue; } String[] localCommand = command.clone(); diff --git a/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableCondition.java b/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableCondition.java index 397f73faf2e..fa939a46d5c 100644 --- a/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableCondition.java +++ b/test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableCondition.java @@ -78,7 +78,7 @@ class DisabledIfProcessUnavailableCondition implements ExecutionCondition { String path = processBuilder.environment().get("PATH"); if (MAC_OS && path != null) { for (String binDirectory : MAC_OS_BIN_DIRECTORIES) { - if (path.contains(binDirectory) || command[0].startsWith(binDirectory + "/")) { + if (path.contains(binDirectory) || command[0].startsWith("/")) { continue; } String[] localCommand = command.clone(); @@ -87,7 +87,8 @@ class DisabledIfProcessUnavailableCondition implements ExecutionCondition { check(new ProcessBuilder(localCommand)); return; } - catch (Exception ignored) { + catch (Exception suppressed) { + ex.addSuppressed(suppressed); } } }