Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* (c) Copyright 2025 Palantir Technologies Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.palantir.gradle.utils.exec;

/**
* Exception thrown when a process execution fails with a non-zero exit code.
*/
public class ExecFailedException extends RuntimeException {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make ExecFailedException a checked exception, so that we force people to wrap it in SafeRuntimeException or UnsafeRuntimeException with their own context?

Not sure if the context added is worth the extra code it requires. But since this is a library, it might be a good idea?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm torn on this, I think FallibleProvider must use RuntimeException because it extends Gradle's Provider<T> interface, which doesn't declare checked exceptions on get(), so we would need to wrap ourselves in GradleExec. but it would be nice to force / encourage users to provider there own context

public ExecFailedException(String executable, ExecResultWithOutput execResult) {
super(buildMessage(executable, execResult));
}

private static String buildMessage(String executable, ExecResultWithOutput execResult) {
StringBuilder message = new StringBuilder();
message.append("Process '")
.append(executable != null ? executable : "<unknown>")
.append("' failed with exit code: ")
.append(execResult.result().getExitValue());

String stdErr = execResult.stdErr().trim();
String stdOut = execResult.stdOut().trim();

if (!stdErr.isEmpty()) {
message.append("\n\nStandard Error:\n").append(stdErr);
}

if (!stdOut.isEmpty()) {
message.append("\n\nStandard Output:\n").append(stdOut);
}

return message.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* (c) Copyright 2025 Palantir Technologies Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.palantir.gradle.utils.exec;

import org.gradle.process.ExecResult;
import org.immutables.value.Value;

@Value.Immutable
public interface ExecResultWithOutput {
String stdOut();

String stdErr();

ExecResult result();

static ExecResultWithOutput of(String stdOut, String stdErr, ExecResult result) {
return ImmutableExecResultWithOutput.builder()
.stdOut(stdOut)
.stdErr(stdErr)
.result(result)
.build();
}
}
47 changes: 19 additions & 28 deletions exec/src/main/java/com/palantir/gradle/utils/exec/GradleExec.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
*/
package com.palantir.gradle.utils.exec;

import com.palantir.gradle.utils.providers.DefaultFailableProvider;
import com.palantir.gradle.utils.providers.FailableProvider;
import com.palantir.gradle.utils.providers.Zipper;
import java.util.concurrent.atomic.AtomicReference;
import javax.inject.Inject;
import org.gradle.api.Action;
import org.gradle.api.provider.Provider;
Expand All @@ -24,7 +27,6 @@
import org.gradle.process.ExecOutput;
import org.gradle.process.ExecResult;
import org.gradle.process.ExecSpec;
import org.immutables.value.Value;

public abstract class GradleExec {

Expand All @@ -37,21 +39,22 @@ public abstract class GradleExec {
protected abstract Zipper getZip();

/**
* Executes a process using the provided {@link Action} to configure the {@link ExecSpec}.
* Executes a process and returns a {@link FailableProvider} that captures stdout, stderr, and exit code.
* <p>
* This method always sets {@code ignoreExitValue} to {@code true} on the {@code ExecSpec},
* ensuring that the build does not fail regardless of the process exit code. Callers do need
* to handle exit codes manually.
* <p>
* The result includes the process's standard output, standard error, and the {@link ExecResult}.
* The returned provider throws {@link ExecFailedException} on non-zero exit codes when {@code .get()}
* is called, but allows custom error handling via {@code .mapFailure()} or {@code .fold()}.
*
* @param action an action to configure the {@link ExecSpec} for the process to be executed
* @return a Provider of {@link ExecResultWithOutput} containing the standard output, standard error,
* and execution result
* @param action configures the {@link ExecSpec} for the process to execute
* @return a FailableProvider containing the execution result with captured output
*/
public Provider<ExecResultWithOutput> exec(Action<? super ExecSpec> action) {
public FailableProvider<ExecResultWithOutput> exec(Action<? super ExecSpec> action) {
// Capture the executable for error messages
AtomicReference<String> executableHolder = new AtomicReference<>();

Action<ExecSpec> wrappedAction = spec -> {
action.execute(spec);
executableHolder.set(spec.getExecutable());
// Always ignore exit value to capture output
spec.setIgnoreExitValue(true);
};

Expand All @@ -61,27 +64,15 @@ public Provider<ExecResultWithOutput> exec(Action<? super ExecSpec> action) {
Provider<String> stderrProvider = execOutput.getStandardError().getAsText();
Provider<ExecResult> resultProvider = execOutput.getResult();

return getZip().zip3(
Provider<ExecResultWithOutput> combinedProvider = getZip().zip3(
resultProvider,
stdoutProvider,
stderrProvider,
(result, stdout, stderr) -> ExecResultWithOutput.of(stdout, stderr, result));
}

@Value.Immutable
public interface ExecResultWithOutput {
String stdOut();

String stdErr();

ExecResult result();

static ExecResultWithOutput of(String stdOut, String stdErr, ExecResult result) {
return ImmutableExecResultWithOutput.builder()
.stdOut(stdOut)
.stdErr(stdErr)
.result(result)
.build();
}
return new DefaultFailableProvider<>(
combinedProvider,
result -> result.result().getExitValue() != 0,
result -> new ExecFailedException(executableHolder.get(), result));
}
}
Loading