// 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.analysis.actions; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.annotations.VisibleForTesting; import com.google.devtools.build.lib.actions.ActionExecutionContext; 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.concurrent.ThreadSafety.Immutable; import com.google.devtools.build.lib.util.Fingerprint; import com.google.devtools.build.lib.util.LazyString; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; /** * Action to write a file whose contents are known at analysis time. * * <p>The output is always UTF-8 encoded. * * <p>TODO(bazel-team): Choose a better name to distinguish this class from {@link * BinaryFileWriteAction}. */ @Immutable // if fileContents is immutable public final class FileWriteAction extends AbstractFileWriteAction { /** Whether or not transparent compression is possible. */ public static enum Compression { /** No compression, ever. */ DISALLOW, /** May compress. */ ALLOW; /** Maps true/false to allow/disallow respectively. */ public static Compression fromBoolean(boolean allow) { return allow ? ALLOW : DISALLOW; } } private static final String GUID = "332877c7-ca9f-4731-b387-54f620408522"; /** * The contents may be lazily computed or compressed. * * <p>If the object representing the contents is a {@code String}, its length is greater than * {@code COMPRESS_CHARS_THRESHOLD}, and compression is enabled, then the gzipped bytestream of * the contents will be stored in place of the string itself. This compression is transparent and * does not affect the output file. * * <p>Otherwise, if the object represents a lazy computation, it will not be forced until {@link * #getFileContents()} is called. An example where this may come in handy is if the contents are * the concatenation of the string representations of a series of artifacts. Then the client code * can wrap a {@code List<Artifact>} in a {@code LazyString}, which saves memory since the * artifacts are shared objects whereas a string is not. */ private final CharSequence fileContents; /** Minimum length (in chars) for content to be eligible for compression. */ private static final int COMPRESS_CHARS_THRESHOLD = 256; private FileWriteAction( ActionOwner owner, Iterable<Artifact> inputs, Artifact output, CharSequence fileContents, boolean makeExecutable, Compression allowCompression) { super(owner, inputs, output, makeExecutable); if (allowCompression == Compression.ALLOW && fileContents instanceof String && fileContents.length() > COMPRESS_CHARS_THRESHOLD) { fileContents = new CompressedString((String) fileContents); } this.fileContents = fileContents; } /** * Creates a new FileWriteAction instance with inputs and empty content. * * <p>This is useful for producing an artifact that, if built, will ensure that the generating * actions for its inputs are run. The output file is non-executable. * * @param owner the action owner * @param inputs the Artifacts that this Action depends on * @param output the Artifact that will be created by executing this Action */ public static FileWriteAction createEmptyWithInputs( ActionOwner owner, Iterable<Artifact> inputs, Artifact output) { return new FileWriteAction(owner, inputs, output, "", false, Compression.DISALLOW); } /** * Creates a new FileWriteAction instance with direct control over whether or not transparent * compression may be used. * * @param owner the action owner * @param output the Artifact that will be created by executing this Action * @param fileContents the contents to be written to the file * @param makeExecutable whether the output file is made executable * @param allowCompression whether (transparent) compression is enabled */ public static FileWriteAction create( ActionOwner owner, Artifact output, CharSequence fileContents, boolean makeExecutable, Compression allowCompression) { return new FileWriteAction( owner, Artifact.NO_ARTIFACTS, output, fileContents, makeExecutable, allowCompression); } /** * Creates a new FileWriteAction instance. * * <p>There are no inputs. Transparent compression is controlled by the {@code * --experimental_transparent_compression} flag. No reference to the {@link RuleContext} will be * maintained. * * @param context the rule context * @param output the Artifact that will be created by executing this Action * @param fileContents the contents to be written to the file * @param makeExecutable whether the output file is made executable */ public static FileWriteAction create( RuleContext context, Artifact output, CharSequence fileContents, boolean makeExecutable) { return new FileWriteAction( context.getActionOwner(), Artifact.NO_ARTIFACTS, output, fileContents, makeExecutable, context.getConfiguration().transparentCompression()); } private static final class CompressedString extends LazyString { final byte[] bytes; final int uncompressedSize; CompressedString(String chars) { byte[] dataToCompress = chars.getBytes(UTF_8); ByteArrayOutputStream byteStream = new ByteArrayOutputStream(dataToCompress.length); try (GZIPOutputStream zipStream = new GZIPOutputStream(byteStream)) { zipStream.write(dataToCompress); } catch (IOException e) { // This should be impossible since we're writing to a byte array. throw new RuntimeException(e); } this.uncompressedSize = dataToCompress.length; this.bytes = byteStream.toByteArray(); } @Override public String toString() { byte[] uncompressedBytes = new byte[uncompressedSize]; try (GZIPInputStream zipStream = new GZIPInputStream(new ByteArrayInputStream(bytes))) { int read; int totalRead = 0; while (totalRead < uncompressedSize && (read = zipStream.read(uncompressedBytes, totalRead, uncompressedSize - totalRead)) != -1) { totalRead += read; } if (totalRead != uncompressedSize) { // This should be impossible. throw new RuntimeException("Corrupt byte buffer in FileWriteAction."); } } catch (IOException e) { // This should be impossible since we're reading from a byte array. throw new RuntimeException(e); } return new String(uncompressedBytes, UTF_8); } } @VisibleForTesting boolean usesCompression() { return fileContents instanceof CompressedString; } /** * Returns the string contents to be written. * * <p>Note that if the string is lazily computed or compressed, calling this method will force its * computation or decompression. No attempt is made by FileWriteAction to cache the result. */ public String getFileContents() { return fileContents.toString(); } @Override public String getSkylarkContent() { return getFileContents(); } /** * Create a DeterministicWriter for the content of the output file as provided by * {@link #getFileContents()}. */ @Override public DeterministicWriter newDeterministicWriter(ActionExecutionContext ctx) { return new DeterministicWriter() { @Override public void writeOutputFile(OutputStream out) throws IOException { out.write(getFileContents().getBytes(UTF_8)); } }; } /** * Computes the Action key for this action by computing the fingerprint for * the file contents. */ @Override protected String computeKey() { Fingerprint f = new Fingerprint(); f.addString(GUID); f.addString(String.valueOf(makeExecutable)); f.addString(getFileContents()); return f.hexDigestAndReset(); } /** * Creates a FileWriteAction to write contents to the resulting artifact fileName in the genfiles * root underneath the package path. * * @param ruleContext the ruleContext that will own the action of creating this file * @param fileName name of the file to create * @param contents data to write to file * @param executable flags that file should be marked executable * @return Artifact describing the file to create */ public static Artifact createFile( RuleContext ruleContext, String fileName, CharSequence contents, boolean executable) { Artifact scriptFileArtifact = ruleContext.getPackageRelativeArtifact( fileName, ruleContext.getConfiguration().getGenfilesDirectory( ruleContext.getRule().getRepository())); ruleContext.registerAction( FileWriteAction.create(ruleContext, scriptFileArtifact, contents, executable)); return scriptFileArtifact; } }