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 6169667708d1..44364674b17f 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 @@ private Process start(ProcessBuilder processBuilder) throws IOException { 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 84a14ddd7f5b..1c7ff4da54cb 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 @@ void getWhenExecutableDoesNotExistErrorThrowsException() { .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 9a603aecedae..03a337367a7f 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 @@ private Process startProcess(String[] command) { } 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 6996e38c06df..397f73faf2ed 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 @@ private Stream getAnnotationValue(AnnotatedElement testElement) { 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(); + } + }