blob: d42c1dd19eeaced0b87527c5efa53519476a4e59 [file]
// Wsifier, a tool to parse BUILD files and bzl files, generate tests cases and
// documentation.
package main
import (
"fmt"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"regexp"
"sort"
"strings"
"github.com/urfave/cli"
)
var defaultPlatforms = []string{"linux", "windows", "macos"}
var ciPlatforms = []string{"ubuntu1804", "windows", "macos"}
var ciPlatformsMap = map[string][]string{
"linux": []string{"ubuntu1604", "ubuntu1804", "rbe_ubuntu1604", "rbe_ubuntu1804"},
"windows": []string{"windows"},
"macos": []string{"macos"},
}
func main() {
app := cli.NewApp()
app.Name = "rulegen"
app.Flags = []cli.Flag{
&cli.StringFlag{
Name: "dir",
Usage: "Directory to scan",
Value: ".",
},
&cli.StringFlag{
Name: "header",
Usage: "Template for the main readme header",
Value: "tools/rulegen/README.header.md",
},
&cli.StringFlag{
Name: "footer",
Usage: "Template for the main readme footer",
Value: "tools/rulegen/README.footer.md",
},
&cli.StringFlag{
Name: "ref",
Usage: "Version ref to use for main readme",
Value: "{GIT_COMMIT_ID}",
},
&cli.StringFlag{
Name: "sha256",
Usage: "Sha256 value to use for main readme",
Value: "{ARCHIVE_TAR_GZ_SHA256}",
},
&cli.StringFlag{
Name: "github_url",
Usage: "URL for github download",
Value: "https://github.com/rules-proto-grpc/rules_proto_grpc/archive/{ref}.tar.gz",
},
&cli.StringFlag{
Name: "available_tests",
Usage: "File containing the list of available routeguide tests",
Value: "available_tests.txt",
},
}
app.Action = func(c *cli.Context) error {
err := action(c)
if err != nil {
return cli.NewExitError("%v", 1)
}
return nil
}
app.Run(os.Args)
}
func action(c *cli.Context) error {
dir := c.String("dir")
if dir == "" {
return fmt.Errorf("--dir required")
}
ref := c.String("ref")
sha256 := c.String("sha256")
githubURL := c.String("github_url")
// Autodetermine sha256 if we have a real commit and templated sha256 value
if ref != "{GIT_COMMIT_ID}" && sha256 == "{ARCHIVE_TAR_GZ_SHA256}" {
sha256 = mustGetSha256(strings.Replace(githubURL, "{ref}", ref, 1))
}
languages := []*Language{
makeAndroid(),
makeClosure(),
makeCpp(),
makeCsharp(),
makeD(),
makeGo(),
makeJava(),
makeNode(),
makeObjc(),
makePhp(),
makePython(),
makeRuby(),
makeRust(),
makeScala(),
makeSwift(),
makeGogo(),
makeGrpcGateway(),
makeGithubComGrpcGrpcWeb(),
}
for _, lang := range languages {
mustWriteLanguageReadme(dir, lang)
mustWriteLanguageDefs(dir, lang)
mustWriteLanguageRules(dir, lang)
mustWriteLanguageExamples(dir, lang)
}
mustWriteReadme(dir, c.String("header"), c.String("footer"), struct {
Ref, Sha256 string
}{
Ref: ref,
Sha256: sha256,
}, languages)
mustWriteBazelciPresubmitYml(dir, languages, []string{}, c.String("available_tests"))
mustWriteExamplesMakefile(dir, languages)
mustWriteTestWorkspacesMakefile(dir)
mustWriteHttpArchiveTestWorkspace(dir, ref, sha256)
return nil
}
func mustWriteLanguageRules(dir string, lang *Language) {
for _, rule := range lang.Rules {
mustWriteLanguageRule(dir, lang, rule)
}
}
func mustWriteLanguageRule(dir string, lang *Language, rule *Rule) {
out := &LineWriter{}
out.t(rule.Implementation, &ruleData{lang, rule})
out.ln()
out.MustWrite(filepath.Join(dir, lang.Dir, rule.Name+".bzl"))
}
func mustWriteLanguageExamples(dir string, lang *Language) {
for _, rule := range lang.Rules {
exampleDir := filepath.Join(dir, "example", lang.Dir, rule.Name)
err := os.MkdirAll(exampleDir, os.ModePerm)
if err != nil {
log.Fatalf("FAILED to create %s: %v", exampleDir, err)
}
mustWriteLanguageExampleWorkspace(exampleDir, lang, rule)
mustWriteLanguageExampleBuildFile(exampleDir, lang, rule)
mustWriteLanguageExampleBazelrcFile(exampleDir, lang, rule)
}
}
func mustWriteLanguageExampleWorkspace(dir string, lang *Language, rule *Rule) {
out := &LineWriter{}
depth := strings.Split(lang.Dir, "/")
// +2 as we are in the example/{rule} subdirectory
relpath := strings.Repeat("../", len(depth)+2)
out.w(`local_repository(
name = "rules_proto_grpc",
path = "%s",
)
load("@rules_proto_grpc//:repositories.bzl", "rules_proto_grpc_toolchains", "rules_proto_grpc_repos")
rules_proto_grpc_toolchains()
rules_proto_grpc_repos()
load("@rules_proto//proto:repositories.bzl", "rules_proto_dependencies", "rules_proto_toolchains")
rules_proto_dependencies()
rules_proto_toolchains()`, relpath)
out.ln()
out.t(rule.WorkspaceExample, &ruleData{lang, rule})
out.ln()
out.MustWrite(filepath.Join(dir, "WORKSPACE"))
}
func mustWriteLanguageExampleBuildFile(dir string, lang *Language, rule *Rule) {
out := &LineWriter{}
out.t(rule.BuildExample, &ruleData{lang, rule})
out.ln()
out.MustWrite(filepath.Join(dir, "BUILD.bazel"))
}
func mustWriteLanguageExampleBazelrcFile(dir string, lang *Language, rule *Rule) {
out := &LineWriter{}
for _, f := range lang.Flags {
if f.Description != "" {
out.w("# %s", f.Description)
} else {
out.w("#")
}
out.w("%s --%s=%s", f.Category, f.Name, f.Value)
}
for _, f := range rule.Flags {
if f.Description != "" {
out.w("# %s", f.Description)
} else {
out.w("#")
}
out.w("%s --%s=%s", f.Category, f.Name, f.Value)
}
out.ln()
out.MustWrite(filepath.Join(dir, ".bazelrc"))
}
func mustWriteLanguageDefs(dir string, lang *Language) {
out := &LineWriter{}
out.w("# Aggregate all `%s` rules to one loadable file", lang.Name)
for _, rule := range lang.Rules {
out.w(`load(":%s.bzl", _%s="%s")`, rule.Name, rule.Name, rule.Name)
}
out.ln()
for _, rule := range lang.Rules {
out.w(`%s = _%s`, rule.Name, rule.Name)
}
out.ln()
if len(lang.Aliases) > 0 {
out.w(`# Aliases`)
aliases := make([]string, 0, len(lang.Aliases))
for alias := range lang.Aliases {
aliases = append(aliases, alias)
}
sort.Strings(aliases)
for _, alias := range aliases {
out.w(`%s = _%s`, alias, lang.Aliases[alias])
}
out.ln()
}
out.MustWrite(filepath.Join(dir, lang.Dir, "defs.bzl"))
}
func mustWriteLanguageReadme(dir string, lang *Language) {
out := &LineWriter{}
out.w("# %s rules", lang.DisplayName)
out.ln()
if lang.Notes != nil {
out.t(lang.Notes, lang)
out.ln()
}
out.w("| Rule | Description |")
out.w("| ---: | :--- |")
for _, rule := range lang.Rules {
out.w("| [%s](#%s) | %s |", rule.Name, rule.Name, rule.Doc)
}
out.ln()
for _, rule := range lang.Rules {
out.w(`---`)
out.ln()
out.w("## `%s`", rule.Name)
out.ln()
if rule.Experimental {
out.w(`> NOTE: this rule is EXPERIMENTAL. It may not work correctly or even compile!`)
out.ln()
}
out.w(rule.Doc)
out.ln()
out.w("### `WORKSPACE`")
out.ln()
out.w("```starlark")
out.t(rule.WorkspaceExample, &ruleData{lang, rule})
out.w("```")
out.ln()
out.w("### `BUILD.bazel`")
out.ln()
out.w("```starlark")
out.t(rule.BuildExample, &ruleData{lang, rule})
out.w("```")
out.ln()
if len(rule.Flags) > 0 {
out.w("### `Flags`")
out.ln()
out.w("| Category | Flag | Value | Description |")
out.w("| --- | --- | --- | --- |")
for _, f := range rule.Flags {
out.w("| %s | %s | %s | %s |", f.Category, f.Name, f.Value, f.Description)
}
out.ln()
}
out.w("### Attributes")
out.ln()
out.w("| Name | Type | Mandatory | Default | Description |")
out.w("| ---: | :--- | --------- | ------- | ----------- |")
for _, attr := range rule.Attrs {
out.w("| `%s` | `%s` | %t | `%s` | %s |", attr.Name, attr.Type, attr.Mandatory, attr.Default, attr.Doc)
}
out.ln()
}
out.MustWrite(filepath.Join(dir, lang.Dir, "README.md"))
}
func mustWriteReadme(dir, header, footer string, data interface{}, languages []*Language) {
out := &LineWriter{}
out.tpl(header, data)
out.ln()
out.w("## Rules")
out.ln()
out.w("| Language | Rule | Description")
out.w("| ---: | :--- | :--- |")
for _, lang := range languages {
for _, rule := range lang.Rules {
dirLink := fmt.Sprintf("[%s](/%s)", lang.DisplayName, lang.Dir)
ruleLink := fmt.Sprintf("[%s](/%s#%s)", rule.Name, lang.Dir, rule.Name)
exampleLink := fmt.Sprintf("[example](/example/%s/%s)", lang.Dir, rule.Name)
out.w("| %s | %s | %s (%s) |", dirLink, ruleLink, rule.Doc, exampleLink)
}
}
out.ln()
out.tpl(footer, data)
out.MustWrite(filepath.Join(dir, "README.md"))
}
func mustWriteBazelciPresubmitYml(dir string, languages []*Language, envVars []string, availableTestsPath string) {
// Read available tests
content, err := ioutil.ReadFile(availableTestsPath)
if err != nil {
log.Fatal(err)
}
availableTestLabels := strings.Split(string(content), "\n")
// Write header
out := &LineWriter{}
out.w("---")
out.w("tasks:")
//
// Write tasks for main code
//
for _, ciPlatform := range ciPlatforms {
// Skip windows, due to issues with 'undeclared inclusion'
if ciPlatform == "windows" {
continue
}
out.w(" main_%s:", ciPlatform)
out.w(" name: build & test all")
out.w(" platform: %s", ciPlatform)
out.w(" environment:")
out.w(` CC: clang`)
if ciPlatform == "macos" {
out.w(" build_flags:")
out.w(` - "--copt=-DGRPC_BAZEL_BUILD"`) // https://github.com/bazelbuild/bazel/issues/4341 required for macos
}
out.w(" build_targets:")
for _, lang := range languages {
// Skip experimental or excluded
if doTestOnPlatform(lang, nil, ciPlatform) {
out.w(` - "//%s/..."`, lang.Dir)
}
}
out.w(" test_flags:")
if ciPlatform == "macos" {
out.w(` - "--copt=-DGRPC_BAZEL_BUILD"`) // https://github.com/bazelbuild/bazel/issues/4341 required for macos
}
out.w(` - "--test_output=errors"`)
out.w(" test_targets:")
for _, clientLang := range languages {
for _, serverLang := range languages {
if doTestOnPlatform(clientLang, nil, ciPlatform) && doTestOnPlatform(serverLang, nil, ciPlatform) && stringInSlice(fmt.Sprintf("//example/routeguide:%s_%s", clientLang.Name, serverLang.Name), availableTestLabels) {
out.w(` - "//example/routeguide:%s_%s"`, clientLang.Name, serverLang.Name)
}
}
}
}
//
// Write tasks for examples
//
for _, lang := range languages {
for _, rule := range lang.Rules {
exampleDir := path.Join(dir, "example", lang.Dir, rule.Name)
for _, ciPlatform := range ciPlatforms {
if !doTestOnPlatform(lang, rule, ciPlatform) {
continue
}
out.w(" %s_%s_%s:", lang.Name, rule.Name, ciPlatform)
out.w(" name: '%s: %s'", lang.Name, rule.Name)
out.w(" platform: %s", ciPlatform)
if ciPlatform == "macos" {
out.w(" build_flags:")
out.w(` - "--copt=-DGRPC_BAZEL_BUILD"`) // https://github.com/bazelbuild/bazel/issues/4341 required for macos
}
out.w(" build_targets:")
out.w(` - "//..."`)
out.w(" working_directory: %s", exampleDir)
if len(lang.PresubmitEnvVars) > 0 || len(rule.PresubmitEnvVars) > 0 {
out.w(" environment:")
for k, v := range lang.PresubmitEnvVars {
out.w(" %s: %s", k, v)
}
for k, v := range rule.PresubmitEnvVars {
out.w(" %s: %s", k, v)
}
}
}
}
}
// Add test workspaces
for _, testWorkspace := range findTestWorkspaceNames(dir) {
for _, ciPlatform := range ciPlatforms {
if ciPlatform == "windows" && (testWorkspace == "python3_grpc" || testWorkspace == "python_deps") {
continue // Don't run python grpc test workspaces on windows
}
out.w(" test_workspace_%s_%s:", testWorkspace, ciPlatform)
out.w(" name: 'test workspace: %s'", testWorkspace)
out.w(" platform: %s", ciPlatform)
if ciPlatform == "macos" {
out.w(" build_flags:")
out.w(` - "--copt=-DGRPC_BAZEL_BUILD"`) // https://github.com/bazelbuild/bazel/issues/4341 required for macos
}
out.w(" test_flags:")
if ciPlatform == "macos" {
out.w(` - "--copt=-DGRPC_BAZEL_BUILD"`) // https://github.com/bazelbuild/bazel/issues/4341 required for macos
}
out.w(` - "--test_output=errors"`)
out.w(" test_targets:")
out.w(` - "//..."`)
out.w(" working_directory: %s", path.Join(dir, "test_workspaces", testWorkspace))
}
}
out.ln()
out.MustWrite(filepath.Join(dir, ".bazelci", "presubmit.yml"))
}
func mustWriteExamplesMakefile(dir string, languages []*Language) {
out := &LineWriter{}
slashRegex := regexp.MustCompile("/")
var allNames []string
for _, lang := range languages {
var langNames []string
// Calculate depth of lang dir
langDepth := len(slashRegex.FindAllStringIndex(lang.Dir, -1))
// Create rules for each example
for _, rule := range lang.Rules {
exampleDir := path.Join(dir, "example", lang.Dir, rule.Name)
var name = fmt.Sprintf("%s_%s_example", lang.Name, rule.Name)
allNames = append(allNames, name)
langNames = append(langNames, name)
out.w(".PHONY: %s", name)
out.w("%s:", name)
out.w(" cd %s; \\", exampleDir)
out.w(" bazel --batch build --verbose_failures --disk_cache=%s../../bazel-disk-cache //...", strings.Repeat("../", langDepth))
out.ln()
}
// Create grouped rules for each language
targetName := fmt.Sprintf("%s_examples", lang.Name)
out.w(".PHONY: %s", targetName)
out.w("%s: %s", targetName, strings.Join(langNames, " "))
out.ln()
}
// Write all examples rule
out.w(".PHONY: all_examples")
out.w("all_examples: %s", strings.Join(allNames, " "))
out.ln()
out.MustWrite(filepath.Join(dir, "example", "Makefile.mk"))
}
func mustWriteTestWorkspacesMakefile(dir string) {
out := &LineWriter{}
// For each test workspace, add makefile rule
var allNames []string
for _, testWorkspace := range findTestWorkspaceNames(dir) {
var name = fmt.Sprintf("test_workspace_%s", testWorkspace)
allNames = append(allNames, name)
out.w(".PHONY: %s", name)
out.w("%s:", name)
out.w(" cd %s; \\", path.Join(dir, "test_workspaces", testWorkspace))
out.w(" bazel --batch test --verbose_failures --disk_cache=../bazel-disk-cache --test_output=errors //...")
out.ln()
}
// Write all test workspaces rule
out.w(".PHONY: all_test_workspaces")
out.w("all_test_workspaces: %s", strings.Join(allNames, " "))
out.ln()
out.MustWrite(filepath.Join(dir, "test_workspaces", "Makefile.mk"))
}
func mustWriteHttpArchiveTestWorkspace(dir, ref, sha256 string) {
out := &LineWriter{}
out.w(`load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name = "rules_proto_grpc",
urls = ["https://github.com/rules-proto-grpc/rules_proto_grpc/archive/%s.tar.gz"],
sha256 = "%s",
strip_prefix = "rules_proto_grpc-%s",
)
`, ref, sha256, ref)
out.MustWrite(filepath.Join(dir, "test_workspaces", "readme_http_archive", "WORKSPACE"))
}
func findTestWorkspaceNames(dir string) []string {
files, err := ioutil.ReadDir(filepath.Join(dir, "test_workspaces"))
if err != nil {
log.Fatal(err)
}
var testWorkspaces []string
for _, file := range files {
if file.IsDir() && !strings.HasPrefix(file.Name(), ".") && !strings.HasPrefix(file.Name(), "bazel-") {
testWorkspaces = append(testWorkspaces, file.Name())
}
}
return testWorkspaces
}