Skip to content

Commit

Permalink
Add execute-command-before BuildMutator
Browse files Browse the repository at this point in the history
- BuildMutator allows commands to be executed prior to the SCENARIO or BUILD phase. Enabled for all types of builds, Gradle, Bazel, Buck, and Maven.

Signed-off-by: Alex Beggs <[email protected]>
  • Loading branch information
spabeggs authored and AlexBeggs committed Feb 8, 2021
1 parent 394d202 commit d4c772c
Show file tree
Hide file tree
Showing 8 changed files with 534 additions and 66 deletions.
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ A scenario can define changes that should be applied to the source before each b
- `clear-project-cache-before`: Deletes the contents of the `.gradle` and `buildSrc/.gradle` project cache directories before the scenario is executed (`SCENARIO`), before cleanup (`CLEANUP`) or before the build is executed (`BUILD`).
- `clear-transform-cache-before`: Deletes the contents of the transform cache before the scenario is executed (`SCENARIO`), before cleanup (`CLEANUP`) or before the build is executed (`BUILD`).
- `clear-jars-cache-before`: Deletes the contents of the instrumented jars cache before the scenario is executed (`SCENARIO`), before cleanup (`CLEANUP`) or before the build is executed (`BUILD`).
- `execute-command-before`: Execute a command before the scenario is executed (SCENARIO) or before the build is executed (BUILD). This can be applied to Gradle, Bazel, Buck, or Maven builds.
- `git-checkout`: Checks out a specific commit for the build step, and a different one for the cleanup step.
- `git-revert`: Reverts a given set of commits before the build and resets it afterward.
- `iterations`: Number of builds to actually measure
Expand Down Expand Up @@ -325,11 +326,43 @@ You can compare Gradle against Bazel, Buck, and Maven by specifying their equiva

build_some_target {
tasks = ["assemble"]
execute-command-before = [
# Gradle specific executions
# Execute a command in the bash shell
{
schedule = BUILD
commands = ["/bin/bash","-c","echo helloworld"]
},
}

bazel {
# If empty, it will be infered from BAZEL_HOME environment variable
home = "/path/to/bazel/home"
targets = ["build" "//some/target"]
execute-command-before = [
# Bazel specific executions
# execute a command prior to the scenario running.
{
schedule = SCENARIO
# Note: Ensure that this is calling the same Bazel that is used in the benchmarks
commands = ["bazel","clean","--expunge"]
},
# Execute a command in the bash shell
{
schedule = BUILD
commands = ["/bin/bash","-c","echo helloworld"]
},
# Remove the contents of the remote cache Bazel bucket
{
schedule = BUILD
commands = ["gsutil","-o","Credentials:gs_service_key_file=bazel-benchmark-bucket.json","-m","rm","gs://bazel-benchmark-bucket/**"]
},
# Display the total size of the bucket
{
schedule = BUILD
commands = ["gsutil","-o","Credentials:gs_service_key_file=bazel-benchmark-bucket.json","du","-sh","gs://bazel-benchmark-bucket"]
},
]
}
}

Expand All @@ -344,6 +377,14 @@ You can compare Gradle against Bazel, Buck, and Maven by specifying their equiva
# If empty, it will be infered from BUCK_HOME environment variable
home = "/path/to/buck/home"
type = "android_binary" // can be a Buck build rule type or "all"
execute-command-before = [
# Buck specific executions
# Execute a command in the bash shell
{
schedule = BUILD
commands = ["/bin/bash","-c","echo helloworld"]
},
}
}
}
build_resources {
Expand Down
207 changes: 141 additions & 66 deletions src/main/java/org/gradle/profiler/ScenarioLoader.java

Large diffs are not rendered by default.

44 changes: 44 additions & 0 deletions src/main/java/org/gradle/profiler/ScenarioUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.gradle.profiler;

import static org.gradle.profiler.ScenarioLoader.BAZEL;
import static org.gradle.profiler.ScenarioLoader.BUCK;
import static org.gradle.profiler.ScenarioLoader.MAVEN;

import com.typesafe.config.Config;
import javax.annotation.Nullable;

public class ScenarioUtil {

/**
* Returns the specific config for type of build that is running,
* if the scenario doesn't define the build, then this default value is returned
*
* @param rootScenario the root scenario config
* @param settings the invocation settings that indicates the type of build selected
* @param defaultScenario if the scenario doesn't define the build, then this default value is returned, this can be null.
* @return if the build config exists or if the scenario doesn't define the build, then this default value is returned
*/
public static Config getBuildConfig(Config rootScenario, InvocationSettings settings, @Nullable
Config defaultScenario) {
Config scenario;
if (settings.isBazel()) {
scenario = getConfigOrDefault(rootScenario, BAZEL, defaultScenario);
} else if (settings.isBuck()) {
scenario = getConfigOrDefault(rootScenario, BUCK, defaultScenario);
} else if (settings.isMaven()) {
scenario = getConfigOrDefault(rootScenario, MAVEN, defaultScenario);
} else {
scenario = rootScenario;
}
return scenario;
}

private static Config getConfigOrDefault(Config rootScenario, String key, @Nullable Config defaultScenario) {
Config scenario;
if (rootScenario.hasPath(key)) {
scenario = rootScenario.getConfig(key);
} else {
scenario = defaultScenario;
} return scenario;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,17 @@
import org.gradle.profiler.InvocationSettings;

public interface BuildMutatorConfigurator {

/**
* This should only be called when {@see #enabled} returns true
*/
BuildMutator configure(Config scenario, String scenarioName, InvocationSettings settings, String key);

/**
* Returns true if this BuildMutator is enabled for targeted scenario. This can filter
* on specific build types.
*/
default boolean enabled(Config scenario, String scenarioName, InvocationSettings settings, String key) {
return scenario.hasPath(key);
}
}
16 changes: 16 additions & 0 deletions src/main/java/org/gradle/profiler/mutations/CommandInvoker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.gradle.profiler.mutations;

import java.util.List;

/**
* Interface which provides a means to execute a command
*/
public interface CommandInvoker {

/**
* @param command the command to execute
* @return the exit code of the result of executing the command
*/
int execute(List<String> command);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package org.gradle.profiler.mutations;

import static org.gradle.profiler.ScenarioUtil.getBuildConfig;

import com.google.common.annotations.VisibleForTesting;
import com.typesafe.config.Config;
import java.util.ArrayList;
import java.util.List;
import org.gradle.profiler.BuildContext;
import org.gradle.profiler.BuildMutator;
import org.gradle.profiler.CompositeBuildMutator;
import org.gradle.profiler.ConfigUtil;
import org.gradle.profiler.InvocationSettings;
import org.gradle.profiler.ScenarioContext;

public class ExecuteCommandBuildMutator implements BuildMutator {

private ExecuteCommandSchedule schedule;
private List<String> commands;
private CommandInvoker commandInvoker;

public ExecuteCommandBuildMutator(ExecuteCommandSchedule schedule,
List<String> commands, CommandInvoker commandInvoker) {
this.schedule = schedule;
this.commands = commands;
this.commandInvoker = commandInvoker;
}

@Override
public void beforeBuild(BuildContext context) {
if (schedule == ExecuteCommandSchedule.BUILD) {
execute();
}
}

@Override
public void beforeScenario(ScenarioContext context) {
if (schedule == ExecuteCommandSchedule.SCENARIO) {
execute();
}
}

protected void execute() {
String commandStr = String.join(" ", commands);
System.out.println(String.format("> Executing command `%s`", commandStr));
int result = commandInvoker.execute(commands);
if (result != 0) {
System.err.println(
String.format("Unexpected exit code %s for command `%s`", result, commandStr)
);
}
}

public static class Configurator implements BuildMutatorConfigurator {

private CommandInvoker commandInvoker;

public Configurator() {
this(new ProcessBuilderCommandInvoker());
}

@VisibleForTesting
Configurator(CommandInvoker commandInvoker) {
this.commandInvoker = commandInvoker;
}

private BuildMutator newInstance(Config scenario, String scenarioName,
InvocationSettings settings, String key,
CommandInvoker commandInvoker, ExecuteCommandSchedule schedule, List<String> commands) {
return new ExecuteCommandBuildMutator(schedule, commands, commandInvoker);
}

@Override
public BuildMutator configure(Config rootScenario, String scenarioName,
InvocationSettings settings, String key) {
Config scenario = getBuildConfig(rootScenario, settings, null);
final List<BuildMutator> mutators = new ArrayList<>();
final List<? extends Config> list = scenario.getConfigList(key);
for (Config config : list) {
final ExecuteCommandSchedule schedule = ConfigUtil
.enumValue(config, "schedule", ExecuteCommandSchedule.class, null);
if (schedule == null) {
throw new IllegalArgumentException(
"Schedule for executing commands is not specified");
}
List<String> commands = ConfigUtil.strings(config, "commands");
if (commands.isEmpty()) {
throw new IllegalArgumentException(
String.format(
"No commands specified for 'execute-command-before' in scenario %s",
scenarioName)
);
}
mutators.add(
newInstance(scenario, scenarioName, settings, key, commandInvoker, schedule,
commands));
}
return new CompositeBuildMutator(mutators);
}

public boolean enabled(Config rootScenario, String scenarioName, InvocationSettings settings, String key) {
Config scenario = getBuildConfig(rootScenario, settings, null);
return scenario != null && scenario.hasPath(key) && !scenario.getConfigList(key)
.isEmpty();
}
}

@Override
public String toString() {
return getClass().getSimpleName() + "(" + schedule + ")";
}

public enum ExecuteCommandSchedule {
SCENARIO, BUILD
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.gradle.profiler.mutations;

import java.io.IOException;
import java.util.List;

public class ProcessBuilderCommandInvoker implements CommandInvoker {

@Override
public int execute(List<String> command) {
try {
if (command == null || command.isEmpty()) {
throw new IllegalArgumentException(
String.format("command cannot be null or empty, was %s", command));
}
ProcessBuilder processBuilder = new ProcessBuilder(command)
.redirectOutput(ProcessBuilder.Redirect.INHERIT)
.redirectError(ProcessBuilder.Redirect.INHERIT);
Process process = processBuilder.start();
return process.waitFor();
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
}
}
Loading

0 comments on commit d4c772c

Please sign in to comment.