Merge pull request #49721 from froggy-hyun

* froggy-hyun/main:
  Polish "Add support for additional Homebrew home on macOS"
  Add support for additional Homebrew home on macOS

Closes gh-49721
This commit is contained in:
Stéphane Nicoll
2026-04-03 16:31:39 +02:00
4 changed files with 68 additions and 26 deletions
@@ -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<String> CREDENTIAL_NOT_FOUND_MESSAGES = Set.of("credentials not found in native keychain",
"no credentials server URL", "no credentials username");
@@ -92,16 +96,19 @@ class CredentialHelper {
if (!Platform.isMac()) {
throw ex;
}
try {
List<String> 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;
List<String> originalCommand = List.copyOf(processBuilder.command());
String executable = originalCommand.get(0);
for (String binDirectory : MAC_OS_BIN_DIRECTORIES) {
List<String> command = new ArrayList<>(originalCommand);
command.set(0, binDirectory + executable);
try {
return processBuilder.command(command).start();
}
catch (Exception suppressed) {
ex.addSuppressed(suppressed);
}
}
throw ex;
}
}
@@ -106,8 +106,11 @@ class CredentialHelperTests {
.satisfies((ex) -> {
if (Platform.isMac()) {
assertThat(ex.getMessage()).doesNotContain("/usr/local/bin/");
assertThat(ex.getSuppressed()).allSatisfy((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));
}
});
}
@@ -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("/")) {
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);
}
@@ -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,36 @@ 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("/")) {
continue;
}
String[] localCommand = command.clone();
localCommand[0] = binDirectory + "/" + command[0];
try {
check(new ProcessBuilder(localCommand));
return;
}
catch (Exception suppressed) {
ex.addSuppressed(suppressed);
}
}
}
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();
}
}