blob: 7a72d634ec53f7a3eac5dcddd99f78caa7134b4f [file] [log] [blame]
// Copyright 2017 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.analysis.actions;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteStreams;
import com.google.devtools.build.lib.actions.ActionExecutionContext;
import com.google.devtools.build.lib.actions.ActionKeyContext;
import com.google.devtools.build.lib.actions.ActionOwner;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.Artifact.ArtifactExpander;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.TransitionMode;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.collect.nestedset.Order;
import com.google.devtools.build.lib.util.Fingerprint;
import com.google.devtools.build.lib.util.OS;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import javax.annotation.Nullable;
/** Action that writes a native {@code .exe} launcher for {java,sh,py}_binary rules on Windows. */
public final class LauncherFileWriteAction extends AbstractFileWriteAction {
// Generated with /usr/bin/uuidgen.
// This GUID doesn't have to be anything specific, we only use it to salt action cache keys so it
// just has to be unique among other actions.
private static final String GUID = "1f57afe7-f6f8-487c-9a8a-0a0286172fef";
private final LaunchInfo launchInfo;
private final Artifact launcher;
/** Creates a new {@link LauncherFileWriteAction}, registering it with the {@code ruleContext}. */
public static void createAndRegister(
RuleContext ruleContext, Artifact output, LaunchInfo launchInfo) {
ruleContext.registerAction(
new LauncherFileWriteAction(
ruleContext.getActionOwner(),
output,
ruleContext.getPrerequisiteArtifact("$launcher", TransitionMode.HOST),
launchInfo));
}
/** Creates a new {@code LauncherFileWriteAction}. */
private LauncherFileWriteAction(
ActionOwner owner, Artifact output, Artifact launcher, LaunchInfo launchInfo) {
super(
owner,
NestedSetBuilder.create(Order.STABLE_ORDER, Preconditions.checkNotNull(launcher)),
output,
/*makeExecutable=*/ true);
this.launcher = launcher; // already null-checked in the superclass c'tor
this.launchInfo = Preconditions.checkNotNull(launchInfo);
}
@Override
public DeterministicWriter newDeterministicWriter(ActionExecutionContext ctx) {
// TODO(laszlocsomor): make this code check for the execution platform, not the host platform,
// once Bazel supports distinguishing between the two.
// OS.getCurrent() returns the host platform, not the execution platform, which is fine in a
// single-machine execution environment, but problematic with remote execution.
Preconditions.checkState(OS.getCurrent() == OS.WINDOWS);
return out -> {
try (InputStream in = ctx.getInputPath(this.launcher).getInputStream()) {
ByteStreams.copy(in, out);
}
long dataLength = this.launchInfo.write(out);
ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
buffer.order(ByteOrder.LITTLE_ENDIAN); // All Windows versions are little endian.
buffer.putLong(dataLength);
out.write(buffer.array());
out.flush();
};
}
@Override
protected void computeKey(
ActionKeyContext actionKeyContext,
@Nullable ArtifactExpander artifactExpander,
Fingerprint fp) {
fp.addString(GUID);
fp.addPath(this.launcher.getExecPath());
fp.addString(this.launchInfo.fingerPrint);
}
/**
* Metadata that describes the payload of the native launcher binary.
*
* <p>This object constructs the binary metadata lazily, to save memory.
*/
public static final class LaunchInfo {
/** Precomputed fingerprint of this object. */
public final String fingerPrint;
private final ImmutableList<Entry> entries;
private LaunchInfo(ImmutableList<Entry> entries) {
this.entries = entries;
this.fingerPrint = computeKey(entries);
}
/** Creates a new {@link Builder}. */
public static Builder builder() {
return new Builder();
}
/** Writes this object's entries to {@code out}, returns the total written amount in bytes. */
@VisibleForTesting
long write(OutputStream out) throws IOException {
long len = 0;
for (Entry e : entries) {
len += e.write(out);
out.write('\0');
++len;
}
return len;
}
/** Computes the fingerprint of the {@code entries}. */
private static String computeKey(ImmutableList<Entry> entries) {
Fingerprint f = new Fingerprint();
for (Entry e : entries) {
e.addToFingerprint(f);
}
return f.hexDigestAndReset();
}
/** Writes {@code s} to {@code out} encoded as UTF-8, returns the written length in bytes. */
private static long writeString(String s, OutputStream out) throws IOException {
byte[] b = s.getBytes(StandardCharsets.UTF_8);
out.write(b);
return b.length;
}
/** Represents one entry in {@link LaunchInfo.entries}. */
private static interface Entry {
/** Writes this entry to {@code out}, returns the written length in bytes. */
long write(OutputStream out) throws IOException;
/** Adds this entry to the fingerprint computer {@code f}. */
void addToFingerprint(Fingerprint f);
}
/** A key-value pair entry. */
private static final class KeyValuePair implements Entry {
private final String key;
@Nullable private final String value;
public KeyValuePair(String key, @Nullable String value) {
this.key = Preconditions.checkNotNull(key);
this.value = value;
}
@Override
public long write(OutputStream out) throws IOException {
long len = writeString(key, out);
len += writeString("=", out);
if (value != null && !value.isEmpty()) {
len += writeString(value, out);
}
return len;
}
@Override
public void addToFingerprint(Fingerprint f) {
f.addString(key);
f.addString(value != null ? value : "");
}
}
/** A pair of a key and a delimiter-joined list of values. */
private static final class JoinedValues implements Entry {
private final String key;
private final String delimiter;
@Nullable private final Iterable<String> values;
public JoinedValues(String key, String delimiter, @Nullable Iterable<String> values) {
this.key = Preconditions.checkNotNull(key);
this.delimiter = Preconditions.checkNotNull(delimiter);
this.values = values;
}
@Override
public long write(OutputStream out) throws IOException {
long len = writeString(key, out);
len += writeString("=", out);
if (values != null) {
boolean first = true;
for (String v : values) {
if (first) {
first = false;
} else {
len += writeString(delimiter, out);
}
len += writeString(v, out);
}
}
return len;
}
@Override
public void addToFingerprint(Fingerprint f) {
f.addString(key);
if (values != null) {
for (String v : values) {
f.addString(v != null ? v : "");
}
}
}
}
/** Builder for {@link LaunchInfo} instances. */
public static final class Builder {
private ImmutableList.Builder<Entry> entries = ImmutableList.builder();
/** Builds a {@link LaunchInfo} from this builder. This builder may be reused. */
public LaunchInfo build() {
return new LaunchInfo(entries.build());
}
/**
* Adds a key-value pair entry.
*
* <p>Examples:
*
* <ul>
* <li>{@code key} is "foo" and {@code value} is "bar", the written value is "foo=bar\0"
* <li>{@code key} is "foo" and {@code value} is null or empty, the written value is
* "foo=\0"
* </ul>
*/
public Builder addKeyValuePair(String key, @Nullable String value) {
Preconditions.checkNotNull(key);
if (!key.isEmpty()) {
entries.add(new KeyValuePair(key, value));
}
return this;
}
/**
* Adds a key and list of lazily-joined values.
*
* <p>Examples:
*
* <ul>
* <li>{@code key} is "foo", {@code delimiter} is ";", {@code values} is ["bar", "baz",
* "qux"], the written value is "foo=bar;baz;qux\0"
* <li>{@code key} is "foo", {@code delimiter} is irrelevant, {@code value} is null or
* empty, the written value is "foo=\0"
* </ul>
*/
public Builder addJoinedValues(
String key, String delimiter, @Nullable Iterable<String> values) {
Preconditions.checkNotNull(key);
Preconditions.checkNotNull(delimiter);
if (!key.isEmpty()) {
entries.add(new JoinedValues(key, delimiter, values));
}
return this;
}
}
}
}