blob: 2434bff695bbb9f3a4d15ad9563239f9070251c4 [file] [log] [blame]
// Copyright 2014 The Bazel Authors. 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.google.devtools.build.lib.bazel;
import static com.google.common.base.StandardSystemProperty.USER_NAME;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.joining;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.eventbus.Subscribe;
import com.google.devtools.build.lib.actions.ActionExecutionContext;
import com.google.devtools.build.lib.actions.ActionExecutionException;
import com.google.devtools.build.lib.actions.ActionKeyContext;
import com.google.devtools.build.lib.actions.ActionOwner;
import com.google.devtools.build.lib.actions.ActionResult;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.ArtifactFactory;
import com.google.devtools.build.lib.actions.ArtifactOwner;
import com.google.devtools.build.lib.actions.ArtifactRoot;
import com.google.devtools.build.lib.actions.ExecutionStrategy;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.analysis.BuildInfo;
import com.google.devtools.build.lib.analysis.BuildInfoEvent;
import com.google.devtools.build.lib.analysis.WorkspaceStatusAction;
import com.google.devtools.build.lib.analysis.WorkspaceStatusAction.Key;
import com.google.devtools.build.lib.analysis.WorkspaceStatusAction.KeyType;
import com.google.devtools.build.lib.buildtool.BuildRequest;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.exec.ExecutorBuilder;
import com.google.devtools.build.lib.runtime.BlazeModule;
import com.google.devtools.build.lib.runtime.BlazeRuntime;
import com.google.devtools.build.lib.runtime.Command;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.runtime.GotOptionsEvent;
import com.google.devtools.build.lib.runtime.WorkspaceBuilder;
import com.google.devtools.build.lib.shell.BadExitStatusException;
import com.google.devtools.build.lib.shell.CommandException;
import com.google.devtools.build.lib.shell.CommandResult;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
import com.google.devtools.build.lib.util.CommandBuilder;
import com.google.devtools.build.lib.util.Fingerprint;
import com.google.devtools.build.lib.util.NetUtil;
import com.google.devtools.build.lib.vfs.FileSystem;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.common.options.OptionsBase;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.UUID;
/**
* Provides information about the workspace (e.g. source control context, current machine, current
* user, etc).
*
* <p>Note that the <code>equals()</code> method is necessary so that Skyframe knows when to
* invalidate the node representing the workspace status action.
*/
public class BazelWorkspaceStatusModule extends BlazeModule {
@AutoCodec
@AutoCodec.VisibleForSerialization
static class BazelWorkspaceStatusAction extends WorkspaceStatusAction {
private final Artifact stableStatus;
private final Artifact volatileStatus;
private final Options options;
private final String username;
private final String hostname;
private final com.google.devtools.build.lib.shell.Command getWorkspaceStatusCommand;
private final ImmutableMap<String, String> clientEnv;
@SuppressWarnings("unused") // Read by serialization.
private final Path workspace;
@AutoCodec.VisibleForSerialization
BazelWorkspaceStatusAction(
WorkspaceStatusAction.Options options,
ImmutableMap<String, String> clientEnv,
Path workspace,
Artifact stableStatus,
Artifact volatileStatus,
String hostname) {
super(
ActionOwner.SYSTEM_ACTION_OWNER,
Artifact.NO_ARTIFACTS,
ImmutableList.of(stableStatus, volatileStatus));
this.options = Preconditions.checkNotNull(options);
this.stableStatus = stableStatus;
this.volatileStatus = volatileStatus;
this.username = USER_NAME.value();
this.hostname = hostname;
this.clientEnv = clientEnv;
this.getWorkspaceStatusCommand =
options.workspaceStatusCommand.equals(PathFragment.EMPTY_FRAGMENT)
? null
: new CommandBuilder()
.addArgs(options.workspaceStatusCommand.toString())
// Pass client env, because certain SCM client(like
// perforce, git) relies on environment variables to work
// correctly.
.setEnv(clientEnv)
.setWorkingDir(workspace)
.useShell(true)
.build();
this.workspace = workspace;
}
private String getAdditionalWorkspaceStatus(ActionExecutionContext actionExecutionContext)
throws ActionExecutionException {
try {
if (this.getWorkspaceStatusCommand != null) {
actionExecutionContext
.getEventHandler()
.handle(
Event.progress(
"Getting additional workspace status by running "
+ options.workspaceStatusCommand));
CommandResult result = this.getWorkspaceStatusCommand.execute();
if (result.getTerminationStatus().success()) {
return new String(result.getStdout(), UTF_8);
}
throw new BadExitStatusException(
this.getWorkspaceStatusCommand,
result,
"workspace status command failed: " + result.getTerminationStatus());
}
} catch (BadExitStatusException e) {
String errorMessage = e.getMessage();
try {
actionExecutionContext.getFileOutErr().getOutputStream().write(
e.getResult().getStdout());
actionExecutionContext.getFileOutErr().getErrorStream().write(e.getResult().getStderr());
} catch (IOException e2) {
errorMessage = errorMessage + " and could not get stdout/stderr: " + e2.getMessage();
}
throw new ActionExecutionException(errorMessage, e, this, true);
} catch (CommandException e) {
throw new ActionExecutionException(e, this, true);
}
return "";
}
private static boolean isStableKey(String key) {
return key.startsWith("STABLE_");
}
private static Map<String, String> parseWorkspaceStatus(String input) {
TreeMap<String, String> result = new TreeMap<>();
for (String line : input.trim().split("\n")) {
String[] splitLine = line.split(" ", 2);
if (splitLine.length >= 2) {
result.put(splitLine[0], splitLine[1]);
}
}
return result;
}
private static byte[] printStatusMap(Map<String, String> map) {
String s =
map.entrySet()
.stream()
.map(entry -> entry.getKey() + " " + entry.getValue())
.collect(joining("\n"));
s += "\n";
return s.getBytes(StandardCharsets.UTF_8);
}
@Override
public void prepare(FileSystem fileSystem, Path execRoot) throws IOException {
// The default implementation of this method deletes all output files; override it to keep
// the old stableStatus around. This way we can reuse the existing file (preserving its mtime)
// if the contents haven't changed.
deleteOutput(fileSystem, volatileStatus);
}
@Override
public ActionResult execute(ActionExecutionContext actionExecutionContext)
throws ActionExecutionException {
try {
Map<String, String> statusMap = parseWorkspaceStatus(
getAdditionalWorkspaceStatus(actionExecutionContext));
Map<String, String> volatileMap = new TreeMap<>();
Map<String, String> stableMap = new TreeMap<>();
for (Map.Entry<String, String> entry : statusMap.entrySet()) {
if (isStableKey(entry.getKey())) {
stableMap.put(entry.getKey(), entry.getValue());
} else {
volatileMap.put(entry.getKey(), entry.getValue());
}
}
stableMap.put(BuildInfo.BUILD_EMBED_LABEL, options.embedLabel);
stableMap.put(BuildInfo.BUILD_HOST, hostname);
stableMap.put(BuildInfo.BUILD_USER, username);
volatileMap.put(BuildInfo.BUILD_TIMESTAMP, Long.toString(getCurrentTimeMillis() / 1000));
Map<String, String> overallMap = new TreeMap<>();
overallMap.putAll(volatileMap);
overallMap.putAll(stableMap);
actionExecutionContext.getEventBus().post(new BuildInfoEvent(overallMap));
// Only update the stableStatus contents if they are different than what we have on disk.
// This is to preserve the old file's mtime so that we do not generate an unnecessary dirty
// file on each incremental build.
FileSystemUtils.maybeUpdateContent(
actionExecutionContext.getInputPath(stableStatus), printStatusMap(stableMap));
// Contrary to the stableStatus, write the contents of volatileStatus unconditionally
// because we know it will be different. This output file is marked as "constant metadata"
// so its dirtiness will be ignored anyway.
FileSystemUtils.writeContent(
actionExecutionContext.getInputPath(volatileStatus), printStatusMap(volatileMap));
} catch (IOException e) {
throw new ActionExecutionException(
"Failed to run workspace status command " + options.workspaceStatusCommand,
e,
this,
true);
}
return ActionResult.EMPTY;
}
/**
* This method returns the current time for stamping, using SOURCE_DATE_EPOCH
* (https://reproducible-builds.org/specs/source-date-epoch/) if provided.
*/
private long getCurrentTimeMillis() {
if (clientEnv.containsKey("SOURCE_DATE_EPOCH")) {
String value = clientEnv.get("SOURCE_DATE_EPOCH").trim();
if (!value.isEmpty()) {
try {
return Long.parseLong(value) * 1000;
} catch (NumberFormatException ex) {
// Fall-back to use the current time if SOURCE_DATE_EPOCH is not a long.
}
}
}
return System.currentTimeMillis();
}
@Override
public boolean equals(Object o) {
if (!(o instanceof BazelWorkspaceStatusAction)) {
return false;
}
// We consider clientEnv in equality because we pass it when executing the workspace status
// command
BazelWorkspaceStatusAction that = (BazelWorkspaceStatusAction) o;
return this.clientEnv.equals(that.clientEnv)
&& this.stableStatus.equals(that.stableStatus)
&& this.volatileStatus.equals(that.volatileStatus)
&& this.options.equals(that.options);
}
@Override
public int hashCode() {
return Objects.hash(clientEnv, stableStatus, volatileStatus, options);
}
@Override
public String getMnemonic() {
return "BazelWorkspaceStatusAction";
}
@Override
protected void computeKey(ActionKeyContext actionKeyContext, Fingerprint fp) {}
@Override
public boolean executeUnconditionally() {
return true;
}
@Override
public boolean isVolatile() {
return true;
}
@Override
public Artifact getVolatileStatus() {
return volatileStatus;
}
@Override
public Artifact getStableStatus() {
return stableStatus;
}
}
private class BazelStatusActionFactory implements WorkspaceStatusAction.Factory {
@Override
public Map<String, String> createDummyWorkspaceStatus() {
return ImmutableMap.of();
}
@Override
public WorkspaceStatusAction createWorkspaceStatusAction(
ArtifactFactory factory, ArtifactOwner artifactOwner, Supplier<UUID> buildId,
String workspaceName) {
ArtifactRoot root = env.getDirectories().getBuildDataDirectory(workspaceName);
Artifact stableArtifact = factory.getDerivedArtifact(
PathFragment.create("stable-status.txt"), root, artifactOwner);
Artifact volatileArtifact = factory.getConstantMetadataArtifact(
PathFragment.create("volatile-status.txt"), root, artifactOwner);
return new BazelWorkspaceStatusAction(
options,
ImmutableMap.copyOf(env.getClientEnv()),
env.getDirectories().getWorkspace(),
stableArtifact,
volatileArtifact,
getHostname());
}
/**
* Returns cached short hostname.
*
* <p>Hostname lookup performs reverse DNS lookup which in bad cases can take seconds. To
* speedup builds we only lookup hostname once and cache the result. Therefore if hostname
* changes during bazel server lifetime, bazel will not see the change.
*/
private String getHostname() {
return NetUtil.getCachedShortHostName();
}
}
@ExecutionStrategy(contextType = WorkspaceStatusAction.Context.class)
private static final class BazelWorkspaceStatusActionContext
implements WorkspaceStatusAction.Context {
private final WorkspaceStatusAction.Options options;
private BazelWorkspaceStatusActionContext(WorkspaceStatusAction.Options options) {
this.options = options;
}
@Override
public ImmutableMap<String, Key> getStableKeys() {
ImmutableMap.Builder<String, Key> builder = ImmutableMap.builder();
builder.put(
BuildInfo.BUILD_EMBED_LABEL, Key.of(KeyType.STRING, options.embedLabel, "redacted"));
builder.put(BuildInfo.BUILD_HOST, Key.of(KeyType.STRING, "hostname", "redacted"));
builder.put(BuildInfo.BUILD_USER, Key.of(KeyType.STRING, "username", "redacted"));
return builder.build();
}
@Override
public ImmutableMap<String, Key> getVolatileKeys() {
return ImmutableMap.of(
BuildInfo.BUILD_TIMESTAMP,
Key.of(KeyType.INTEGER, "0", "0"),
BuildInfo.BUILD_SCM_REVISION,
Key.of(KeyType.STRING, "0", "0"),
BuildInfo.BUILD_SCM_STATUS,
Key.of(KeyType.STRING, "", "redacted"));
}
}
private CommandEnvironment env;
private WorkspaceStatusAction.Options options;
@Override
public void beforeCommand(CommandEnvironment env) {
this.env = env;
env.getEventBus().register(this);
}
@Override
public void afterCommand() {
this.env = null;
this.options = null;
}
@Override
public Iterable<Class<? extends OptionsBase>> getCommandOptions(Command command) {
return "build".equals(command.name())
? ImmutableList.<Class<? extends OptionsBase>>of(WorkspaceStatusAction.Options.class)
: ImmutableList.<Class<? extends OptionsBase>>of();
}
@Subscribe
public void gotOptionsEvent(GotOptionsEvent event) {
options = event.getOptions().getOptions(WorkspaceStatusAction.Options.class);
}
@Override
public void workspaceInit(
BlazeRuntime runtime, BlazeDirectories directories, WorkspaceBuilder builder) {
builder.setWorkspaceStatusActionFactory(new BazelStatusActionFactory());
}
@Override
public void executorInit(CommandEnvironment env, BuildRequest request, ExecutorBuilder builder) {
builder.addActionContext(new BazelWorkspaceStatusActionContext(options));
}
}