| // 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.skyframe; | |
| import com.google.common.collect.ImmutableMap; | |
| import com.google.common.hash.HashFunction; | |
| import com.google.devtools.build.lib.actions.FileValue; | |
| import com.google.devtools.build.lib.cmdline.BazelCompileContext; | |
| import com.google.devtools.build.lib.cmdline.Label; | |
| import com.google.devtools.build.lib.events.Event; | |
| import com.google.devtools.build.lib.packages.AutoloadSymbols; | |
| import com.google.devtools.build.lib.packages.BazelStarlarkEnvironment; | |
| import com.google.devtools.build.lib.vfs.FileSystemUtils; | |
| import com.google.devtools.build.lib.vfs.Path; | |
| import com.google.devtools.build.lib.vfs.RootedPath; | |
| import com.google.devtools.build.skyframe.SkyFunction; | |
| import com.google.devtools.build.skyframe.SkyFunctionException; | |
| import com.google.devtools.build.skyframe.SkyFunctionException.Transience; | |
| import com.google.devtools.build.skyframe.SkyKey; | |
| import com.google.devtools.build.skyframe.SkyValue; | |
| import java.io.IOException; | |
| import javax.annotation.Nullable; | |
| import net.starlark.java.eval.Module; | |
| import net.starlark.java.eval.StarlarkSemantics; | |
| import net.starlark.java.syntax.FileOptions; | |
| import net.starlark.java.syntax.ParserInput; | |
| import net.starlark.java.syntax.Program; | |
| import net.starlark.java.syntax.StarlarkFile; | |
| import net.starlark.java.syntax.SyntaxError; | |
| /** | |
| * A Skyframe function that compiles the .bzl file denoted by a Label. | |
| * | |
| * <p>Given a {@link Label} referencing a Starlark file, BzlCompileFunction loads, parses, resolves, | |
| * and compiles it. The Label must be absolute, and must not reference the special {@code external} | |
| * package. If the file (or the package containing it) doesn't exist, the function doesn't fail, but | |
| * instead returns a specific {@code NO_FILE} {@link BzlCompileValue}. | |
| */ | |
| // TODO(adonovan): actually compile. The name is a step ahead of the implementation. | |
| public class BzlCompileFunction implements SkyFunction { | |
| private final BazelStarlarkEnvironment bazelStarlarkEnvironment; | |
| private final HashFunction hashFunction; | |
| public BzlCompileFunction( | |
| BazelStarlarkEnvironment bazelStarlarkEnvironment, HashFunction hashFunction) { | |
| this.bazelStarlarkEnvironment = bazelStarlarkEnvironment; | |
| this.hashFunction = hashFunction; | |
| } | |
| @Override | |
| public SkyValue compute(SkyKey skyKey, Environment env) | |
| throws SkyFunctionException, InterruptedException { | |
| try { | |
| return computeInline( | |
| (BzlCompileValue.Key) skyKey.argument(), env, bazelStarlarkEnvironment, hashFunction); | |
| } catch (FailedIOException e) { | |
| throw new FunctionException(e); | |
| } | |
| } | |
| @Nullable | |
| static BzlCompileValue computeInline( | |
| BzlCompileValue.Key key, | |
| Environment env, | |
| BazelStarlarkEnvironment bazelStarlarkEnvironment, | |
| HashFunction hashFunction) | |
| throws FailedIOException, InterruptedException { | |
| byte[] bytes; | |
| byte[] digest; | |
| String inputName; | |
| if (key.kind == BzlCompileValue.Kind.EMPTY_PRELUDE) { | |
| // Default prelude is empty. | |
| bytes = new byte[] {}; | |
| digest = null; | |
| inputName = "<default prelude>"; | |
| } else { | |
| // Obtain the file. | |
| RootedPath rootedPath = RootedPath.toRootedPath(key.root, key.label.toPathFragment()); | |
| SkyKey fileSkyKey = FileValue.key(rootedPath); | |
| FileValue fileValue = null; | |
| try { | |
| fileValue = (FileValue) env.getValueOrThrow(fileSkyKey, IOException.class); | |
| } catch (IOException e) { | |
| throw new FailedIOException(e, Transience.PERSISTENT); | |
| } | |
| if (fileValue == null) { | |
| return null; | |
| } | |
| if (fileValue.exists()) { | |
| if (!fileValue.isFile()) { | |
| return fileValue.isDirectory() | |
| ? BzlCompileValue.noFile("cannot load '%s': is a directory", key.label) | |
| : BzlCompileValue.noFile( | |
| "cannot load '%s': not a regular file (dangling link?)", key.label); | |
| } | |
| // Read the file. | |
| Path path = rootedPath.asPath(); | |
| try { | |
| bytes = | |
| fileValue.isSpecialFile() | |
| ? FileSystemUtils.readContent(path) | |
| : FileSystemUtils.readWithKnownFileSize(path, fileValue.getSize()); | |
| } catch (IOException e) { | |
| throw new FailedIOException(e, Transience.TRANSIENT); | |
| } | |
| digest = fileValue.getDigest(); // may be null | |
| inputName = path.toString(); | |
| } else { | |
| if (key.kind == BzlCompileValue.Kind.PRELUDE) { | |
| // A non-existent prelude is fine. | |
| bytes = new byte[] {}; | |
| digest = null; | |
| inputName = "<default prelude>"; | |
| } else { | |
| return BzlCompileValue.noFile("cannot load '%s': no such file", key.label); | |
| } | |
| } | |
| } | |
| // Compute digest if we didn't already get it from a fileValue. | |
| if (digest == null) { | |
| digest = hashFunction.hashBytes(bytes).asBytes(); | |
| } | |
| StarlarkSemantics semantics = PrecomputedValue.STARLARK_SEMANTICS.get(env); | |
| if (semantics == null) { | |
| return null; | |
| } | |
| ImmutableMap<String, Object> predeclared; | |
| if (key.isSclDialect()) { | |
| predeclared = bazelStarlarkEnvironment.getStarlarkGlobals().getSclToplevels(); | |
| } else if (key.kind == BzlCompileValue.Kind.BUILTINS) { | |
| predeclared = bazelStarlarkEnvironment.getBuiltinsBzlEnv(); | |
| } else { | |
| // Use the predeclared environment for BUILD-loaded bzl files, ignoring injection. It is not | |
| // the right env for the actual evaluation of BUILD-loaded bzl files because it doesn't | |
| // map to the injected symbols. But the names of the symbols are the same, and the names are | |
| // all we need to do symbol resolution. | |
| // | |
| // For WORKSPACE-loaded bzl files, the env isn't quite right not because of injection but | |
| // because the "native" object is different. But A) that will be fixed with #11954, and B) we | |
| // don't care for the same reason as above. | |
| // Takes into account --incompatible_autoload_externally, similarly to the comment above, this | |
| // only defines the correct set of symbols, but does not load them yet. | |
| AutoloadSymbols autoloadSymbols = AutoloadSymbols.AUTOLOAD_SYMBOLS.get(env); | |
| if (autoloadSymbols == null) { | |
| return null; | |
| } | |
| predeclared = autoloadSymbols.getUninjectedBuildBzlEnv(key.getLabel()); | |
| } | |
| // We have all deps. Parse, resolve, and return. | |
| ParserInput input = ParserInput.fromLatin1(bytes, inputName); | |
| FileOptions options = | |
| FileOptions.builder() | |
| // By default, Starlark load statements create file-local bindings. | |
| // However, the BUILD prelude typically contains nothing but load | |
| // statements whose bindings are intended to be visible in all BUILD | |
| // files. The loadBindsGlobally flag allows us to retrieve them. | |
| .loadBindsGlobally(key.isBuildPrelude()) | |
| // .scl files should be ASCII-only in string literals. | |
| // TODO(bazel-team): It'd be nice if we could intercept non-ASCII errors from the lexer, | |
| // and modify the displayed message to clarify to the user that the string would be | |
| // permitted in a .bzl file. But there's no easy way to do that short of either string | |
| // matching the error message or reworking the interpreter API to put more structured | |
| // detail in errors (i.e. new fields or error subclasses). | |
| .stringLiteralsAreAsciiOnly(key.isSclDialect()) | |
| .build(); | |
| StarlarkFile file = StarlarkFile.parse(input, options); | |
| // compile | |
| final Module module; | |
| if (key.kind == BzlCompileValue.Kind.EMPTY_PRELUDE) { | |
| // The empty prelude has no label, so we can't use it to filter the predeclareds. | |
| // This doesn't matter since the empty prelude doesn't attempt to access any predeclareds | |
| // anyway. | |
| module = Module.withPredeclared(semantics, predeclared); | |
| } else { | |
| // The BazelCompileContext holds additional contextual info to be associated with the Module | |
| // The information is used to filter predeclareds | |
| BazelCompileContext bazelCompileContext = | |
| BazelCompileContext.create(key.label, file.getName()); | |
| module = Module.withPredeclaredAndData(semantics, predeclared, bazelCompileContext); | |
| } | |
| try { | |
| Program prog = Program.compileFile(file, module); | |
| return BzlCompileValue.withProgram(prog, digest); | |
| } catch (SyntaxError.Exception ex) { | |
| Event.replayEventsOn(env.getListener(), ex.errors()); | |
| return BzlCompileValue.noFile( | |
| "compilation of module '%s'%s failed", | |
| key.label.toPathFragment(), | |
| StarlarkBuiltinsValue.isBuiltinsRepo(key.label.getRepository()) ? " (internal)" : ""); | |
| } | |
| } | |
| static final class FailedIOException extends Exception { | |
| private final Transience transience; | |
| private FailedIOException(IOException cause, Transience transience) { | |
| super(cause.getMessage(), cause); | |
| this.transience = transience; | |
| } | |
| Transience getTransience() { | |
| return transience; | |
| } | |
| } | |
| private static final class FunctionException extends SkyFunctionException { | |
| private FunctionException(FailedIOException cause) { | |
| super(cause, cause.transience); | |
| } | |
| } | |
| } |