From 3cadf0a12cc3eb4fccb41666da34dd93f69ed64b Mon Sep 17 00:00:00 2001 From: Han-Wen Nienhuys Date: Fri, 9 Oct 2015 16:30:46 +0000 Subject: RELNOTES: Go rules for Bazel. -- MOS_MIGRATED_REVID=105062625 --- tools/build_rules/go/BUILD | 3 + tools/build_rules/go/README.md | 78 +++++ tools/build_rules/go/def.bzl | 331 +++++++++++++++++++++ tools/build_rules/go/toolchain/BUILD | 13 + tools/build_rules/go/toolchain/BUILD.go-toolchain | 12 + .../go/toolchain/WORKSPACE.go-toolchain | 12 + tools/build_rules/go/tools/BUILD | 9 + tools/build_rules/go/tools/generate_test_main.go | 91 ++++++ 8 files changed, 549 insertions(+) create mode 100644 tools/build_rules/go/BUILD create mode 100644 tools/build_rules/go/README.md create mode 100644 tools/build_rules/go/def.bzl create mode 100644 tools/build_rules/go/toolchain/BUILD create mode 100644 tools/build_rules/go/toolchain/BUILD.go-toolchain create mode 100644 tools/build_rules/go/toolchain/WORKSPACE.go-toolchain create mode 100644 tools/build_rules/go/tools/BUILD create mode 100644 tools/build_rules/go/tools/generate_test_main.go (limited to 'tools/build_rules/go') diff --git a/tools/build_rules/go/BUILD b/tools/build_rules/go/BUILD new file mode 100644 index 0000000000..917ca91a75 --- /dev/null +++ b/tools/build_rules/go/BUILD @@ -0,0 +1,3 @@ +package( + visibility = ["//visibility:public"], +) diff --git a/tools/build_rules/go/README.md b/tools/build_rules/go/README.md new file mode 100644 index 0000000000..af5d416267 --- /dev/null +++ b/tools/build_rules/go/README.md @@ -0,0 +1,78 @@ +Go rules +-------- + +The rules should be considered experimental. They support: + +* libraries +* binaries +* tests +* vendoring + +They currently do not support (in order of importance): + +* Darwin +* //+build tags +* auto generated BUILD files. +* C/C++ interoperation (cgo, swig etc.) +* race detector +* coverage +* test sharding + +Setup +----- + +* Decide on the name of your package, eg. "github.com/joe/project" + +* Copy tools/build_rules/go/toolchain/WORKSPACE.go-toolchain to WORKSPACE + +* Add a BUILD file to the top of your workspace, containing + + load("/tools/build_rules/go/def", "go_prefix") + go_prefix("github.com/joe/project") + +* For a library "github.com/joe/project/lib", create lib/BUILD, containing + + load("/tools/build_rules/go/def", "go_library") + go_library( + name = "go_default_library", + srcs = ["file.go"]) + +* Inside your project, you can use this library by declaring a dependency + + go_binary( ... + deps = ["//lib:go_default_library"]) + +* In this case, import the library as "github.com/joe/project/lib". + +* For vendored libraries, you may depend on + "//lib/vendor/github_com/user/project:go_default_library". Vendored + libraries should have BUILD files like normal libraries. + +* To declare a test, + + go_test( + name = "mytest", + srcs = ["file_test.go"], + library = ":go_default_library") + +FAQ +--- + +# Can I still use the =go= tool? + +Yes, this setup was deliberately chosen to be compatible with the =go= +tool. Make sure your workspace appears under + + $GOROOT/src/github.com/joe/project/ + +eg. + + mkdir -p $GOROOT/src/github.com/joe/ + ln -s my/bazel/workspace $GOROOT/src/github.com/joe/project + +and it should work. + +Disclaimer +---------- + +These rules are not supported by Google's Go team. diff --git a/tools/build_rules/go/def.bzl b/tools/build_rules/go/def.bzl new file mode 100644 index 0000000000..a6c22c6045 --- /dev/null +++ b/tools/build_rules/go/def.bzl @@ -0,0 +1,331 @@ +# 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. + +"""These are bare-bones Go rules. + +In order of priority: + +- No support for build tags + +- BUILD file must be written by hand. + +- No C++ interop (SWIG, cgo). + +- No test sharding or test XML. + +""" + +_DEFAULT_LIB = "go_default_library" +_VENDOR_PREFIX = "/vendor/" + +go_filetype = FileType([".go"]) + +################ + +# In Go, imports are always fully qualified with a URL, +# eg. github.com/user/project. Hence, a label //foo:bar from within a +# Bazel workspace must be referred to as +# "github.com/user/project/foo/bar". To make this work, each rule must +# know the repository's URL. This is achieved, by having all go rules +# depend on a globally unique target that has a "go_prefix" transitive +# info provider. + +def _go_prefix_impl(ctx): + """go_prefix_impl provides the go prefix to use as a transitive info provider.""" + return struct(go_prefix = ctx.attr.prefix) + +def _go_prefix(ctx): + """slash terminated go-prefix""" + prefix = ctx.attr.go_prefix.go_prefix + if not prefix.endswith("/"): + prefix = prefix + "/" + return prefix + +_go_prefix_rule = rule( + _go_prefix_impl, + attrs = { + "prefix": attr.string(mandatory=True), + }, + ) + +def go_prefix(prefix): + """go_prefix sets the Go import name to be used for this workspace.""" + _go_prefix_rule(name = "go_prefix", + prefix = prefix, + visibility = ["//visibility:public" ] + ) + +################ + +# TODO(bazel-team): it would be nice if Bazel had this built-in. +def symlink_tree_commands(dest_dir, artifact_dict): + """Symlink_tree_commands returns a list of commands to create the + dest_dir, and populate it according to the given dict. + + Args: + dest_dir: The destination directory, a string. + artifact_dict: The mapping of exec-path => path in the dest_dir. + + Returns: + A list of commands that will setup the symlink tree. + """ + cmds = [ + "rm -rf " + dest_dir, + "mkdir -p " + dest_dir, + ] + + for old_path, new_path in artifact_dict.items(): + new_dir = new_path[:new_path.rfind('/')] + up = (new_dir.count('/') + 1 + + dest_dir.count('/') + 1) + cmds += [ + "mkdir -p %s/%s" % (dest_dir, new_dir), + "ln -s %s%s %s/%s" % ('../' * up, old_path, dest_dir, new_path), + ] + return cmds + + + +def emit_go_compile_action(ctx, sources, deps, out_lib): + """Construct the command line for compiling Go code. + Constructs a symlink tree to accomodate for workspace name. + + Args: + ctx: The skylark Context. + sources: an iterable of source code artifacts (or CTs? or labels?) + deps: an iterable of dependencies. Each dependency d should have an + artifact in d.go_library_object representing an imported library. + out_lib: the artifact (configured target?) that should be produced + """ + config_strip = len(ctx.configuration.bin_dir.path) + 1 + + out_dir = out_lib.path + ".dir" + out_depth = out_dir.count('/') + 1 + tree_layout = {} + inputs = [] + prefix = _go_prefix(ctx) + import_map = {} + for d in deps: + library_artifact_path = d.go_library_object.path[config_strip:] + tree_layout[d.go_library_object.path] = prefix + library_artifact_path + inputs += [d.go_library_object] + + source_import = prefix + d.label.package + "/" + d.label.name + actual_import = prefix + d.label.package + "/" + d.label.name + if d.label.name == _DEFAULT_LIB: + source_import = prefix + d.label.package + + if source_import.rfind(_VENDOR_PREFIX) != -1: + source_import = source_import[len(_VENDOR_PREFIX) + source_import.rfind(_VENDOR_PREFIX):] + + if source_import != actual_import: + if source_import in import_map: + fail("duplicate import %s: adding %s and have %s" + % (source_import, actual_import, import_map[source_import])) + import_map[source_import] = actual_import + + inputs += list(sources) + for s in sources: + tree_layout[s.path] = prefix + s.path + + cmds = symlink_tree_commands(out_dir, tree_layout) + args = [ + "cd ", out_dir, "&&", + ('../' * out_depth) + ctx.file.go_tool.path, + "tool", "compile", + "-o", ('../' * out_depth) + out_lib.path, "-pack", + + # Import path. + "-I", "."] + [ + "-importmap=%s=%s" % (k,v) for k, v in import_map.items() + ] + + # Set -p to the import path of the library, ie. + # (ctx.label.package + "/" ctx.label.name) for now. + cmds += [ "export GOROOT=$(pwd)/" + ctx.file.go_tool.dirname + "/..", + ' '.join(args + cmd_helper.template(sources, prefix + "%{path}"))] + + ctx.action( + inputs = inputs + ctx.files.toolchain, + outputs = [out_lib], + mnemonic = "GoCompile", + command = " && ".join(cmds)) + + +def go_library_impl(ctx): + """Implements the go_library() rule.""" + + sources = set(ctx.files.srcs) + deps = ctx.attr.deps + if ctx.attr.library: + sources += ctx.attr.library.go_sources + deps += ctx.attr.library.direct_deps + + if not sources: + fail("may not be empty", "srcs") + + out_lib = ctx.outputs.lib + emit_go_compile_action(ctx, set(sources), deps, out_lib) + + transitive_libs = set([out_lib]) + for dep in ctx.attr.deps: + transitive_libs += dep.transitive_go_library_object + + return struct( + label = ctx.label, + files = set([out_lib]), + direct_deps = deps, + go_sources = sources, + go_library_object = out_lib, + transitive_go_library_object = transitive_libs) + + +def emit_go_link_action(ctx, transitive_libs, lib, executable): + """Sets up a symlink tree to libraries to link together.""" + out_dir = executable.path + ".dir" + out_depth = out_dir.count('/') + 1 + tree_layout = {} + + config_strip = len(ctx.configuration.bin_dir.path) + 1 + prefix = _go_prefix(ctx) + + for l in transitive_libs: + library_artifact_path = l.path[config_strip:] + tree_layout[l.path] = prefix + library_artifact_path + + tree_layout[lib.path] = prefix + lib.path[config_strip:] + tree_layout[executable.path] = prefix + executable.path[config_strip:] + + cmds = symlink_tree_commands(out_dir, tree_layout) + cmds += [ + "export GOROOT=$(pwd)/" + ctx.file.go_tool.dirname + "/..", + "cd " + out_dir, + ' '.join([ + ('../' * out_depth) + ctx.file.go_tool.path, + "tool", "link", "-L", ".", + "-o", prefix + executable.path[config_strip:], + prefix + lib.path[config_strip:]])] + + ctx.action( + inputs = list(transitive_libs) + [lib] + ctx.files.toolchain, + outputs = [executable], + command = ' && '.join(cmds), + mnemonic = "GoLink") + + +def go_binary_impl(ctx): + """go_binary_impl emits actions for compiling and linking a go executable.""" + lib_result = go_library_impl(ctx) + executable = ctx.outputs.executable + lib_out = ctx.outputs.lib + + emit_go_link_action( + ctx, lib_result.transitive_go_library_object, lib_out, executable) + return struct(files = set([executable]) + lib_result.files) + + +def go_test_impl(ctx): + """go_test_impl implements go testing. + + It emits an action to run the test generator, and then compiles the + test into a binary.""" + + lib_result = go_library_impl(ctx) + main_go = ctx.outputs.main_go + prefix = _go_prefix(ctx) + + go_import = prefix + ctx.label.package + "/" + ctx.label.name + + args = (["--package", go_import, "--output", ctx.outputs.main_go.path] + + cmd_helper.template(lib_result.go_sources, "%{path}")) + + inputs = list(lib_result.go_sources) + list(ctx.files.toolchain) + ctx.action( + inputs = inputs, + executable = ctx.executable.test_generator, + outputs = [main_go], + mnemonic = "GoTestGenTest", + arguments = args) + + emit_go_compile_action( + ctx, set([main_go]), ctx.attr.deps + [lib_result], ctx.outputs.main_lib) + + emit_go_link_action( + ctx, lib_result.transitive_go_library_object, + ctx.outputs.main_lib, ctx.outputs.executable) + + # TODO(bazel-team): the Go tests should do a chdir to the directory + # holding the data files, so open-source go tests continue to work + # without code changes. + runfiles = ctx.runfiles(collect_data = True, files = [ctx.outputs.executable]) + return struct(runfiles=runfiles) + +go_library_attrs = { + "data": attr.label_list(allow_files=True, cfg=DATA_CFG), + "srcs": attr.label_list(allow_files=go_filetype), + "deps": attr.label_list( + providers=[ + "direct_deps", + "go_library_object", + "transitive_go_library_object", + ]), + "toolchain": attr.label( + default=Label("//tools/build_rules/go/toolchain:toolchain"), + allow_files=True, + cfg=HOST_CFG), + "go_tool": attr.label( + default=Label("//tools/build_rules/go/toolchain:go_tool"), + single_file = True, + allow_files=True, + cfg=HOST_CFG), + "library": attr.label( + providers=["go_sources"]), + "go_prefix": attr.label( + providers = [ "go_prefix" ], + default=Label("//external:go_prefix"), + allow_files=False, cfg=HOST_CFG), + } + +go_library_outputs = { + "lib": "%{name}.a", +} + +go_library = rule( + go_library_impl, + attrs = go_library_attrs, + outputs = go_library_outputs) + +go_binary = rule( + go_binary_impl, + executable = True, + attrs = go_library_attrs + { + "stamp": attr.bool(default=False), + }, + outputs = go_library_outputs) + +go_test = rule( + go_test_impl, + executable = True, + test = True, + attrs = go_library_attrs + { + "test_generator": attr.label( + executable=True, + default=Label("//tools/build_rules/go/tools:generate_test_main"), + cfg=HOST_CFG), + }, + outputs = { + "lib" : "%{name}.a", + "main_lib": "%{name}_main_test.a", + "main_go": "%{name}_main_test.go", +}) diff --git a/tools/build_rules/go/toolchain/BUILD b/tools/build_rules/go/toolchain/BUILD new file mode 100644 index 0000000000..71b2bfec8c --- /dev/null +++ b/tools/build_rules/go/toolchain/BUILD @@ -0,0 +1,13 @@ +# TODO(bazel-team): support darwin. + +filegroup( + name = "toolchain", + srcs = ["@golang-linux-amd64//:toolchain"], + visibility = ["//visibility:public"], +) + +filegroup( + name = "go_tool", + srcs = ["@golang-linux-amd64//:go_tool"], + visibility = ["//visibility:public"], +) diff --git a/tools/build_rules/go/toolchain/BUILD.go-toolchain b/tools/build_rules/go/toolchain/BUILD.go-toolchain new file mode 100644 index 0000000000..a32c134590 --- /dev/null +++ b/tools/build_rules/go/toolchain/BUILD.go-toolchain @@ -0,0 +1,12 @@ +package( + default_visibility = [ "//visibility:public" ]) + +filegroup( + name = "toolchain", + srcs = glob(["go/bin/*", "go/pkg/linux_amd64/**", "go/pkg/tool/**", "go/pkg/include/**"]), +) + +filegroup( + name = "go_tool", + srcs = [ "go/bin/go" ], +) diff --git a/tools/build_rules/go/toolchain/WORKSPACE.go-toolchain b/tools/build_rules/go/toolchain/WORKSPACE.go-toolchain new file mode 100644 index 0000000000..5eed0f046c --- /dev/null +++ b/tools/build_rules/go/toolchain/WORKSPACE.go-toolchain @@ -0,0 +1,12 @@ +# TODO(bazel-team): support darwin + +bind(name = "go_prefix", + actual = "//:go_prefix", +) + +new_http_archive( + name= "golang-linux-amd64", + url = "https://storage.googleapis.com/golang/go1.5.1.linux-amd64.tar.gz", + build_file = "tools/build_rules/go/toolchain/BUILD.go-toolchain", + sha256 = "2593132ca490b9ee17509d65ee2cd078441ff544899f6afb97a03d08c25524e7" +) diff --git a/tools/build_rules/go/tools/BUILD b/tools/build_rules/go/tools/BUILD new file mode 100644 index 0000000000..1bf9bfe159 --- /dev/null +++ b/tools/build_rules/go/tools/BUILD @@ -0,0 +1,9 @@ +package(default_visibility = ["//visibility:public"]) + +load("/tools/build_rules/go/def", "go_binary") + +# This binary is used implicitly by go_test(). +go_binary( + name = "generate_test_main", + srcs = ["generate_test_main.go"], +) diff --git a/tools/build_rules/go/tools/generate_test_main.go b/tools/build_rules/go/tools/generate_test_main.go new file mode 100644 index 0000000000..6316802dde --- /dev/null +++ b/tools/build_rules/go/tools/generate_test_main.go @@ -0,0 +1,91 @@ +// Bare bones Go testing support for Bazel. + +package main + +import ( + "flag" + "go/ast" + "go/parser" + "go/token" + "log" + "os" + "strings" + "text/template" +) + +// Cases holds template data. +type Cases struct { + Package string + Names []string +} + +func main() { + pkg := flag.String("package", "", "package from which to import test methods.") + out := flag.String("output", "", "output file to write. Defaults to stdout.") + flag.Parse() + + if *pkg == "" { + log.Fatal("must set --package.") + } + + outFile := os.Stdout + if *out != "" { + var err error + outFile, err = os.Create(*out) + if err != nil { + log.Fatalf("os.Create(%q): %v", *out, err) + } + defer outFile.Close() + } + + cases := Cases{ + Package: *pkg, + } + testFileSet := token.NewFileSet() + for _, f := range flag.Args() { + parse, err := parser.ParseFile(testFileSet, f, nil, parser.ParseComments) + if err != nil { + log.Fatalf("ParseFile(%q): %v", f, err) + } + + for _, d := range parse.Decls { + fn, ok := d.(*ast.FuncDecl) + if !ok { + continue + } + if fn.Recv != nil { + continue + } + if !strings.HasPrefix(fn.Name.Name, "Test") { + continue + } + cases.Names = append(cases.Names, fn.Name.Name) + } + } + + tpl := template.Must(template.New("source").Parse(` +package main +import ( + "testing" + + undertest "{{.Package}}" +) + +func everything(pat, str string) (bool, error) { + return true, nil +} + +var tests = []testing.InternalTest{ +{{range .Names}} + {"{{.}}", undertest.{{.}} }, +{{end}} +} + +func main() { + testing.Main(everything, tests, nil, nil) +} +`)) + if err := tpl.Execute(outFile, &cases); err != nil { + log.Fatalf("template.Execute(%v): %v", cases, err) + } +} -- cgit v1.2.3