aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/analysis/actions/FileWriteAction.java
blob: f2d14f13237ad21ebf0ebf10c7f06cb3070747ab (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
// 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.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.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
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}.
 */
@AutoCodec
@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) {
    this(
        owner,
        inputs,
        output,
        allowCompression == Compression.ALLOW
                && fileContents instanceof String
                && fileContents.length() > COMPRESS_CHARS_THRESHOLD
            ? new CompressedString((String) fileContents)
            : fileContents,
        makeExecutable);
  }

  @AutoCodec.VisibleForSerialization
  @AutoCodec.Instantiator
  FileWriteAction(
      ActionOwner owner,
      Iterable<Artifact> inputs,
      Artifact primaryOutput,
      CharSequence fileContents,
      boolean makeExecutable) {
    super(owner, inputs, primaryOutput, makeExecutable);
    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
   * ActionConstructionContext} will be maintained.
   *
   * @param context the action construction 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(
      ActionConstructionContext 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 void computeKey(ActionKeyContext actionKeyContext, Fingerprint fp) {
    fp.addString(GUID);
    fp.addString(String.valueOf(makeExecutable));
    fp.addString(getFileContents());
  }

  /**
   * 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;
  }
}