| // 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.analysis.RuleContext; |
| import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget.Mode; |
| 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", Mode.HOST), |
| launchInfo)); |
| } |
| |
| /** Creates a new {@code LauncherFileWriteAction}. */ |
| private LauncherFileWriteAction( |
| ActionOwner owner, Artifact output, Artifact launcher, LaunchInfo launchInfo) { |
| super( |
| owner, |
| ImmutableList.of(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, 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; |
| } |
| } |
| } |
| } |