blob: c3daeb5e493ff334bdd879cede3799e5d2fa42d4 [file] [log] [blame]
// Copyright 2018 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.skydoc;
import com.google.common.base.Functions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.skylarkbuildapi.TopLevelBootstrap;
import com.google.devtools.build.lib.skylarkbuildapi.android.AndroidBootstrap;
import com.google.devtools.build.lib.skylarkbuildapi.apple.AppleBootstrap;
import com.google.devtools.build.lib.skylarkbuildapi.config.ConfigBootstrap;
import com.google.devtools.build.lib.skylarkbuildapi.cpp.CcBootstrap;
import com.google.devtools.build.lib.skylarkbuildapi.java.JavaBootstrap;
import com.google.devtools.build.lib.skylarkbuildapi.platform.PlatformBootstrap;
import com.google.devtools.build.lib.skylarkbuildapi.repository.RepositoryBootstrap;
import com.google.devtools.build.lib.skylarkbuildapi.test.TestingBootstrap;
import com.google.devtools.build.lib.syntax.BaseFunction;
import com.google.devtools.build.lib.syntax.BuildFileAST;
import com.google.devtools.build.lib.syntax.Environment;
import com.google.devtools.build.lib.syntax.Environment.Extension;
import com.google.devtools.build.lib.syntax.Environment.GlobalFrame;
import com.google.devtools.build.lib.syntax.MethodLibrary;
import com.google.devtools.build.lib.syntax.Mutability;
import com.google.devtools.build.lib.syntax.ParserInputSource;
import com.google.devtools.build.lib.syntax.Runtime;
import com.google.devtools.build.lib.syntax.SkylarkImport;
import com.google.devtools.build.skydoc.fakebuildapi.FakeActionsInfoProvider;
import com.google.devtools.build.skydoc.fakebuildapi.FakeBuildApiGlobals;
import com.google.devtools.build.skydoc.fakebuildapi.FakeConfigApi;
import com.google.devtools.build.skydoc.fakebuildapi.FakeDefaultInfoProvider;
import com.google.devtools.build.skydoc.fakebuildapi.FakeOutputGroupInfo.FakeOutputGroupInfoProvider;
import com.google.devtools.build.skydoc.fakebuildapi.FakeSkylarkAttrApi;
import com.google.devtools.build.skydoc.fakebuildapi.FakeSkylarkCommandLineApi;
import com.google.devtools.build.skydoc.fakebuildapi.FakeSkylarkNativeModuleApi;
import com.google.devtools.build.skydoc.fakebuildapi.FakeSkylarkRuleFunctionsApi;
import com.google.devtools.build.skydoc.fakebuildapi.FakeStructApi.FakeStructProviderApi;
import com.google.devtools.build.skydoc.fakebuildapi.android.FakeAndroidDeviceBrokerInfo.FakeAndroidDeviceBrokerInfoProvider;
import com.google.devtools.build.skydoc.fakebuildapi.android.FakeAndroidInstrumentationInfo.FakeAndroidInstrumentationInfoProvider;
import com.google.devtools.build.skydoc.fakebuildapi.android.FakeAndroidNativeLibsInfo.FakeAndroidNativeLibsInfoProvider;
import com.google.devtools.build.skydoc.fakebuildapi.android.FakeAndroidResourcesInfo.FakeAndroidResourcesInfoProvider;
import com.google.devtools.build.skydoc.fakebuildapi.android.FakeAndroidSkylarkCommon;
import com.google.devtools.build.skydoc.fakebuildapi.android.FakeApkInfo.FakeApkInfoProvider;
import com.google.devtools.build.skydoc.fakebuildapi.apple.FakeAppleCommon;
import com.google.devtools.build.skydoc.fakebuildapi.config.FakeConfigSkylarkCommon;
import com.google.devtools.build.skydoc.fakebuildapi.cpp.FakeCcModule;
import com.google.devtools.build.skydoc.fakebuildapi.java.FakeJavaCommon;
import com.google.devtools.build.skydoc.fakebuildapi.java.FakeJavaInfo.FakeJavaInfoProvider;
import com.google.devtools.build.skydoc.fakebuildapi.java.FakeJavaProtoCommon;
import com.google.devtools.build.skydoc.fakebuildapi.platform.FakePlatformCommon;
import com.google.devtools.build.skydoc.fakebuildapi.repository.FakeRepositoryModule;
import com.google.devtools.build.skydoc.fakebuildapi.test.FakeTestingModule;
import com.google.devtools.build.skydoc.rendering.MarkdownRenderer;
import com.google.devtools.build.skydoc.rendering.RuleInfo;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
/**
* Main entry point for the Skydoc binary.
*
* <p>Skydoc generates human-readable documentation for relevant details of skylark files by
* running a skylark interpreter with a fake implementation of the build API.</p>
*
* <p>Currently, Skydoc generates documentation for skylark rule definitions (discovered by
* invocations of the build API function {@code rule()}.</p>
*
* <p>Usage:</p>
* <pre>
* skydoc {target_skylark_file_label} {output_file} [symbol_name]...
* </pre>
* <p>
* Generates documentation for all exported symbols of the target skylark file that are
* specified in the list of symbol names. If no symbol names are supplied, outputs documentation
* for all exported symbols in the target skylark file.
* </p>
*/
public class SkydocMain {
private final EventHandler eventHandler = new SystemOutEventHandler();
private final LinkedHashSet<Path> pending = new LinkedHashSet<>();
private final Map<Path, Environment> loaded = new HashMap<>();
private final SkylarkFileAccessor fileAccessor;
public SkydocMain(SkylarkFileAccessor fileAccessor) {
this.fileAccessor = fileAccessor;
}
public static void main(String[] args)
throws IOException, InterruptedException, LabelSyntaxException {
if (args.length < 2) {
throw new IllegalArgumentException("Expected two or more arguments. Usage:\n"
+ "{skydoc_bin} {target_skylark_file_label} {output_file} [symbol_names]...");
}
String targetFileLabelString = args[0];
String outputPath = args[1];
Label targetFileLabel =
Label.parseAbsolute(targetFileLabelString, ImmutableMap.of());
ImmutableSet<String> symbolNames = getSymbolNames(args);
ImmutableMap.Builder<String, RuleInfo> ruleInfoMap = ImmutableMap.builder();
ImmutableList.Builder<RuleInfo> unknownNamedRules = ImmutableList.builder();
new SkydocMain(new FilesystemFileAccessor())
.eval(targetFileLabel, ruleInfoMap, unknownNamedRules);
MarkdownRenderer renderer = new MarkdownRenderer();
if (symbolNames.isEmpty()) {
try (PrintWriter printWriter = new PrintWriter(outputPath, "UTF-8")) {
printRuleInfos(printWriter, renderer, ruleInfoMap.build(), unknownNamedRules.build());
}
} else {
Map<String, RuleInfo> filteredRuleInfos = ImmutableMap.copyOf(
ruleInfoMap.build().entrySet().stream()
.filter(entry -> symbolNames.contains(entry.getKey()))
.collect(Collectors.toList()));
try (PrintWriter printWriter = new PrintWriter(outputPath, "UTF-8")) {
printRuleInfos(printWriter, renderer, filteredRuleInfos, ImmutableList.of());
}
}
}
private static ImmutableSet<String> getSymbolNames(String[] args) {
ImmutableSet.Builder<String> symbolNameSet = ImmutableSet.builder();
for (int argi = 2; argi < args.length; argi++) {
symbolNameSet.add(args[argi]);
}
return symbolNameSet.build();
}
private static void printRuleInfos(
PrintWriter printWriter,
MarkdownRenderer renderer,
Map<String, RuleInfo> ruleInfos,
List<RuleInfo> unknownNamedRules) throws IOException {
for (Entry<String, RuleInfo> ruleInfoEntry : ruleInfos.entrySet()) {
printRuleInfo(printWriter, renderer, ruleInfoEntry.getKey(), ruleInfoEntry.getValue());
printWriter.println();
}
for (RuleInfo unknownNamedRule : unknownNamedRules) {
printRuleInfo(printWriter, renderer, "<unknown name>", unknownNamedRule);
printWriter.println();
}
}
private static void printRuleInfo(
PrintWriter printWriter, MarkdownRenderer renderer,
String exportedName, RuleInfo ruleInfo) throws IOException {
printWriter.println(renderer.render(exportedName, ruleInfo));
}
/**
* Evaluates/interprets the skylark file at a given path and its transitive skylark dependencies
* using a fake build API and collects information about all rule definitions made in the
* root skylark file.
*
* @param label the label of the skylark file to evaluate
* @param ruleInfoMap a map builder to be populated with rule definition information for
* named rules. Keys are exported names of rules, and values are their {@link RuleInfo}
* rule descriptions. For example, 'my_rule = rule(...)' has key 'my_rule'
* @param unknownNamedRules a list builder to be populated with rule definition information
* for rules which were not exported as top level symbols
* @throws InterruptedException if evaluation is interrupted
*/
public Environment eval(
Label label,
ImmutableMap.Builder<String, RuleInfo> ruleInfoMap,
ImmutableList.Builder<RuleInfo> unknownNamedRules)
throws InterruptedException, IOException, LabelSyntaxException {
List<RuleInfo> ruleInfoList = new ArrayList<>();
Environment env = recursiveEval(label, ruleInfoList);
Map<BaseFunction, RuleInfo> ruleFunctions = ruleInfoList.stream()
.collect(Collectors.toMap(
RuleInfo::getIdentifierFunction,
Functions.identity()));
ImmutableSet.Builder<RuleInfo> handledRuleDefinitions = ImmutableSet.builder();
for (Entry<String, Object> envEntry : env.getGlobals().getBindings().entrySet()) {
if (ruleFunctions.containsKey(envEntry.getValue())) {
RuleInfo ruleInfo = ruleFunctions.get(envEntry.getValue());
ruleInfoMap.put(envEntry.getKey(), ruleInfo);
handledRuleDefinitions.add(ruleInfo);
}
}
unknownNamedRules.addAll(ruleFunctions.values().stream()
.filter(ruleInfo -> !handledRuleDefinitions.build().contains(ruleInfo)).iterator());
return env;
}
/**
* Recursively evaluates/interprets the skylark file at a given path and its transitive skylark
* dependencies using a fake build API and collects information about all rule definitions made
* in those files.
*
* @param label the label of the skylark file to evaluate
* @param ruleInfoList a collection of all rule definitions made so far (using rule()); this
* method will add to this list as it evaluates additional files
* @throws InterruptedException if evaluation is interrupted
*/
private Environment recursiveEval(
Label label, List<RuleInfo> ruleInfoList)
throws InterruptedException, IOException, LabelSyntaxException {
Path path = pathOfLabel(label);
if (pending.contains(path)) {
throw new IllegalStateException("cycle with " + path);
} else if (loaded.containsKey(path)) {
return loaded.get(path);
}
pending.add(path);
ParserInputSource parserInputSource = fileAccessor.inputSource(path.toString());
BuildFileAST buildFileAST = BuildFileAST.parseSkylarkFile(parserInputSource, eventHandler);
Map<String, Extension> imports = new HashMap<>();
for (SkylarkImport anImport : buildFileAST.getImports()) {
Label relativeLabel = label.getRelative(anImport.getImportString());
try {
Environment importEnv = recursiveEval(relativeLabel, ruleInfoList);
imports.put(anImport.getImportString(), new Extension(importEnv));
} catch (NoSuchFileException noSuchFileException) {
throw new IllegalStateException(
String.format("File %s imported '%s', yet %s was not found.",
path, anImport.getImportString(), pathOfLabel(relativeLabel)));
}
}
Environment env = evalSkylarkBody(buildFileAST, imports, ruleInfoList);
pending.remove(path);
env.mutability().freeze();
loaded.put(path, env);
return env;
}
private Path pathOfLabel(Label label) {
String workspacePrefix = label.getWorkspaceRoot().isEmpty()
? ""
: label.getWorkspaceRoot() + "/";
return Paths.get(workspacePrefix + label.toPathFragment());
}
/**
* Evaluates the AST from a single skylark file, given the already-resolved imports.
*/
private Environment evalSkylarkBody(
BuildFileAST buildFileAST,
Map<String, Extension> imports,
List<RuleInfo> ruleInfoList) throws InterruptedException {
Environment env = createEnvironment(
eventHandler,
globalFrame(ruleInfoList),
imports);
if (!buildFileAST.exec(env, eventHandler)) {
throw new RuntimeException("Error loading file");
}
env.mutability().freeze();
return env;
}
/**
* Initialize and return a global frame containing the fake build API.
*
* @param ruleInfoList the list of {@link RuleInfo} objects, to which rule() invocation
* information will be added
*/
private static GlobalFrame globalFrame(List<RuleInfo> ruleInfoList) {
TopLevelBootstrap topLevelBootstrap =
new TopLevelBootstrap(new FakeBuildApiGlobals(),
new FakeSkylarkAttrApi(),
new FakeSkylarkCommandLineApi(),
new FakeSkylarkNativeModuleApi(),
new FakeSkylarkRuleFunctionsApi(ruleInfoList),
new FakeStructProviderApi(),
new FakeOutputGroupInfoProvider(),
new FakeActionsInfoProvider(),
new FakeDefaultInfoProvider());
AndroidBootstrap androidBootstrap = new AndroidBootstrap(new FakeAndroidSkylarkCommon(),
new FakeApkInfoProvider(),
new FakeAndroidInstrumentationInfoProvider(),
new FakeAndroidDeviceBrokerInfoProvider(),
new FakeAndroidResourcesInfoProvider(),
new FakeAndroidNativeLibsInfoProvider());
AppleBootstrap appleBootstrap = new AppleBootstrap(new FakeAppleCommon());
ConfigBootstrap configBootstrap =
new ConfigBootstrap(new FakeConfigSkylarkCommon(), new FakeConfigApi());
CcBootstrap ccBootstrap = new CcBootstrap(new FakeCcModule());
JavaBootstrap javaBootstrap = new JavaBootstrap(new FakeJavaCommon(),
new FakeJavaInfoProvider(),
new FakeJavaProtoCommon());
PlatformBootstrap platformBootstrap = new PlatformBootstrap(new FakePlatformCommon());
RepositoryBootstrap repositoryBootstrap = new RepositoryBootstrap(new FakeRepositoryModule());
TestingBootstrap testingBootstrap = new TestingBootstrap(new FakeTestingModule());
ImmutableMap.Builder<String, Object> envBuilder = ImmutableMap.builder();
Runtime.addConstantsToBuilder(envBuilder);
MethodLibrary.addBindingsToBuilder(envBuilder);
topLevelBootstrap.addBindingsToBuilder(envBuilder);
androidBootstrap.addBindingsToBuilder(envBuilder);
appleBootstrap.addBindingsToBuilder(envBuilder);
ccBootstrap.addBindingsToBuilder(envBuilder);
configBootstrap.addBindingsToBuilder(envBuilder);
javaBootstrap.addBindingsToBuilder(envBuilder);
platformBootstrap.addBindingsToBuilder(envBuilder);
repositoryBootstrap.addBindingsToBuilder(envBuilder);
testingBootstrap.addBindingsToBuilder(envBuilder);
return GlobalFrame.createForBuiltins(envBuilder.build());
}
private static Environment createEnvironment(EventHandler eventHandler, GlobalFrame globals,
Map<String, Extension> imports) {
return Environment.builder(Mutability.create("Skydoc"))
.useDefaultSemantics()
.setGlobals(globals)
.setImportedExtensions(imports)
.setEventHandler(eventHandler)
.build();
}
}