// 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. * *

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 entries; private LaunchInfo(ImmutableList 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 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 values; public JoinedValues(String key, String delimiter, @Nullable Iterable 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 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. * *

Examples: * *

*/ 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. * *

Examples: * *

*/ public Builder addJoinedValues( String key, String delimiter, @Nullable Iterable values) { Preconditions.checkNotNull(key); Preconditions.checkNotNull(delimiter); if (!key.isEmpty()) { entries.add(new JoinedValues(key, delimiter, values)); } return this; } } } }