Skip to content

Commit 2d4c235

Browse files
froggy-hyunsnicoll
authored andcommitted
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>
1 parent cdbf74f commit 2d4c235

4 files changed

Lines changed: 68 additions & 25 deletions

File tree

buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ class CredentialHelper {
4141

4242
private static final String USR_LOCAL_BIN = "/usr/local/bin/";
4343

44+
private static final String OPT_HOMEBREW_BIN = "/opt/homebrew/bin/";
45+
46+
private static final String[] MAC_OS_BIN_DIRECTORIES = { OPT_HOMEBREW_BIN, USR_LOCAL_BIN };
47+
4448
private static final Set<String> CREDENTIAL_NOT_FOUND_MESSAGES = Set.of("credentials not found in native keychain",
4549
"no credentials server URL", "no credentials username");
4650

@@ -92,16 +96,22 @@ private Process start(ProcessBuilder processBuilder) throws IOException {
9296
if (!Platform.isMac()) {
9397
throw ex;
9498
}
95-
try {
96-
List<String> command = new ArrayList<>(processBuilder.command());
97-
command.set(0, USR_LOCAL_BIN + command.get(0));
98-
return processBuilder.command(command).start();
99-
}
100-
catch (Exception suppressed) {
101-
// Suppresses the exception and rethrows the original exception
102-
ex.addSuppressed(suppressed);
103-
throw ex;
99+
String executable = processBuilder.command().get(0);
100+
for (String binDirectory : MAC_OS_BIN_DIRECTORIES) {
101+
try {
102+
List<String> command = new ArrayList<>(processBuilder.command());
103+
if (executable.startsWith(binDirectory)) {
104+
continue;
105+
}
106+
command.set(0, binDirectory + executable);
107+
return processBuilder.command(command).start();
108+
}
109+
catch (Exception suppressed) {
110+
// Suppresses the exception and rethrows the original exception
111+
ex.addSuppressed(suppressed);
112+
}
104113
}
114+
throw ex;
105115
}
106116
}
107117

buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@ void getWhenExecutableDoesNotExistErrorThrowsException() {
106106
.satisfies((ex) -> {
107107
if (Platform.isMac()) {
108108
assertThat(ex.getMessage()).doesNotContain("/usr/local/bin/");
109-
assertThat(ex.getSuppressed()).allSatisfy((suppressed) -> assertThat(suppressed)
109+
assertThat(ex.getSuppressed()).anySatisfy((suppressed) -> assertThat(suppressed)
110+
.hasMessageContaining("/opt/homebrew/bin/" + executable));
111+
assertThat(ex.getSuppressed()).anySatisfy((suppressed) -> assertThat(suppressed)
110112
.hasMessageContaining("/usr/local/bin/" + executable));
111113
}
112114
});

core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ class ProcessRunner {
4444

4545
private static final String USR_LOCAL_BIN = "/usr/local/bin";
4646

47+
private static final String OPT_HOMEBREW_BIN = "/opt/homebrew/bin";
48+
49+
private static final String[] MAC_OS_BIN_DIRECTORIES = { OPT_HOMEBREW_BIN, USR_LOCAL_BIN };
50+
4751
private static final boolean MAC_OS = System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("mac");
4852

4953
private static final Log logger = LogFactory.getLog(ProcessRunner.class);
@@ -108,11 +112,22 @@ private Process startProcess(String[] command) {
108112
}
109113
catch (IOException ex) {
110114
String path = processBuilder.environment().get("PATH");
111-
if (MAC_OS && path != null && !path.contains(USR_LOCAL_BIN)
112-
&& !command[0].startsWith(USR_LOCAL_BIN + "/")) {
113-
String[] localCommand = command.clone();
114-
localCommand[0] = USR_LOCAL_BIN + "/" + localCommand[0];
115-
return startProcess(localCommand);
115+
if (MAC_OS && path != null) {
116+
for (String binDirectory : MAC_OS_BIN_DIRECTORIES) {
117+
if (path.contains(binDirectory) || command[0].startsWith(binDirectory + "/")) {
118+
continue;
119+
}
120+
String[] localCommand = command.clone();
121+
localCommand[0] = binDirectory + "/" + command[0];
122+
ProcessBuilder localProcessBuilder = new ProcessBuilder(localCommand);
123+
localProcessBuilder.directory(this.workingDirectory);
124+
try {
125+
return localProcessBuilder.start();
126+
}
127+
catch (IOException suppressed) {
128+
ex.addSuppressed(suppressed);
129+
}
130+
}
116131
}
117132
throw new ProcessStartException(command, ex);
118133
}

test-support/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/process/DisabledIfProcessUnavailableCondition.java

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ class DisabledIfProcessUnavailableCondition implements ExecutionCondition {
4343

4444
private static final String USR_LOCAL_BIN = "/usr/local/bin";
4545

46+
private static final String OPT_HOMEBREW_BIN = "/opt/homebrew/bin";
47+
48+
private static final String[] MAC_OS_BIN_DIRECTORIES = { OPT_HOMEBREW_BIN, USR_LOCAL_BIN };
49+
4650
private static final boolean MAC_OS = System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("mac");
4751

4852
@Override
@@ -68,23 +72,35 @@ private Stream<String[]> getAnnotationValue(AnnotatedElement testElement) {
6872
private void check(String[] command) {
6973
ProcessBuilder processBuilder = new ProcessBuilder(command);
7074
try {
71-
Process process = processBuilder.start();
72-
Assert.state(process.waitFor(30, TimeUnit.SECONDS), "Process did not exit within 30 seconds");
73-
Assert.state(process.exitValue() == 0, () -> "Process exited with %d".formatted(process.exitValue()));
74-
process.destroy();
75+
check(processBuilder);
7576
}
7677
catch (Exception ex) {
7778
String path = processBuilder.environment().get("PATH");
78-
if (MAC_OS && path != null && !path.contains(USR_LOCAL_BIN)
79-
&& !command[0].startsWith(USR_LOCAL_BIN + "/")) {
80-
String[] localCommand = command.clone();
81-
localCommand[0] = USR_LOCAL_BIN + "/" + localCommand[0];
82-
check(localCommand);
83-
return;
79+
if (MAC_OS && path != null) {
80+
for (String binDirectory : MAC_OS_BIN_DIRECTORIES) {
81+
if (path.contains(binDirectory) || command[0].startsWith(binDirectory + "/")) {
82+
continue;
83+
}
84+
String[] localCommand = command.clone();
85+
localCommand[0] = binDirectory + "/" + command[0];
86+
try {
87+
check(new ProcessBuilder(localCommand));
88+
return;
89+
}
90+
catch (Exception ignored) {
91+
}
92+
}
8493
}
8594
throw new RuntimeException(
8695
"Unable to start process '%s'".formatted(StringUtils.arrayToDelimitedString(command, " ")));
8796
}
8897
}
8998

99+
private void check(ProcessBuilder processBuilder) throws Exception {
100+
Process process = processBuilder.start();
101+
Assert.state(process.waitFor(30, TimeUnit.SECONDS), "Process did not exit within 30 seconds");
102+
Assert.state(process.exitValue() == 0, () -> "Process exited with %d".formatted(process.exitValue()));
103+
process.destroy();
104+
}
105+
90106
}

0 commit comments

Comments
 (0)