diff --git a/README.md b/README.md index dca1940..cb3e067 100644 --- a/README.md +++ b/README.md @@ -19,3 +19,6 @@ version is documented in a series of blog posts at * **v4**: Moves most of the implementation out of Starlark into a "builder" binary. Described in [Moving logic to execution](https://jayconrod.com/posts/109/writing-bazel-rules--moving-logic-to-execution). +* **v5**: Downloads the Go distribution and registers a Bazel toolchain. + Described in + [Repository rules](https://jayconrod.com/posts/110/writing-bazel-rules--repository-rules). diff --git a/WORKSPACE b/WORKSPACE index c494d72..00e016f 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -1,12 +1,51 @@ # The WORKSPACE file should appear in the root directory of the repository. -# It's job is to configure external repositories, which are declared -# with repository rules. This file is only evaluated for builds in *this* -# repository, not for builds in other repositories that depend on this one. -# For this reason, we declare dependencies in a macro that can be loaded -# here *and* in other repositories' WORKSPACE files. +# Its job is to configure external repositories, which are declared +# with repository rules. We also register toolchains here. +# +# This file is only evaluated for builds in *this* repository, not for builds in +# other repositories that depend on this one. +# Each workspace should set a canonical name. This is the name other workspaces +# may use to import it (via an http_archive rule or something similar). +# It's also the name used in labels that refer to this workspace +# (for example @rules_go_simple//v5:deps.bzl). workspace(name = "rules_go_simple") -load("@rules_go_simple//v4:deps.bzl", "go_rules_dependencies") +load( + "@rules_go_simple//v5:deps.bzl", + "go_download", + "go_rules_dependencies", +) +# go_rules_dependencies declares the dependencies of rules_go_simple. Any +# project that depends on rules_go_simple should call this. go_rules_dependencies() + +# These rules download Go distributions for macOS and Linux. +# They are lazily evaluated, so they won't actually download anything until +# we depend on a target inside these workspaces. We register toolchains +# below though, so that forces both downloads. We could be more clever +# about registering only the toolchain we need. +go_download( + name = "go_darwin_amd64", + goarch = "amd64", + goos = "darwin", + sha256 = "a9088c44a984c4ba64179619606cc65d9d0cb92988012cfc94fbb29ca09edac7", + urls = ["https://dl.google.com/go/go1.13.4.darwin-amd64.tar.gz"], +) + +go_download( + name = "go_linux_amd64", + goarch = "amd64", + goos = "linux", + sha256 = "692d17071736f74be04a72a06dab9cac1cd759377bd85316e52b2227604c004c", + urls = ["https://dl.google.com/go/go1.13.4.linux-amd64.tar.gz"], +) + +# register_toolchains makes one or more toolchain rules available for Bazel's +# automatic toolchain selection. Bazel will pick whichever toolchain is +# compatible with the execution and target platforms. +register_toolchains( + "@go_darwin_amd64//:toolchain", + "@go_linux_amd64//:toolchain", +) diff --git a/v5/BUILD.bazel b/v5/BUILD.bazel new file mode 100644 index 0000000..493538b --- /dev/null +++ b/v5/BUILD.bazel @@ -0,0 +1,8 @@ +# toolchain_type defines a name for a kind of toolchain. Our toolchains +# declare that they have this type. Our rules request a toolchain of this type. +# Bazel selects a toolchain of the correct type that satisfies platform +# constraints from among registered toolchains. +toolchain_type( + name = "toolchain_type", + visibility = ["//visibility:public"], +) diff --git a/v5/def.bzl b/v5/def.bzl new file mode 100644 index 0000000..72815fb --- /dev/null +++ b/v5/def.bzl @@ -0,0 +1,29 @@ +# Copyright Jay Conrod. All rights reserved. + +# This file is part of rules_go_simple. Use of this source code is governed by +# the 3-clause BSD license that can be found in the LICENSE.txt file. + +# def.bzl contains public definitions that may be used by Bazel projects for +# building Go programs. These definitions should be loaded from here and +# not any internal directory. + +load( + "//v5/internal:rules.bzl", + _go_binary = "go_binary", + _go_library = "go_library", + _go_test = "go_test", +) +load( + "//v5/internal:providers.bzl", + _GoLibrary = "GoLibrary", +) +load( + "//v5/internal:toolchain.bzl", + _go_toolchain = "go_toolchain", +) + +go_binary = _go_binary +go_library = _go_library +go_test = _go_test +go_toolchain = _go_toolchain +GoLibrary = _GoLibrary diff --git a/v5/deps.bzl b/v5/deps.bzl new file mode 100644 index 0000000..dbe3428 --- /dev/null +++ b/v5/deps.bzl @@ -0,0 +1,35 @@ +# Copyright Jay Conrod. All rights reserved. + +# This file is part of rules_go_simple. Use of this source code is governed by +# the 3-clause BSD license that can be found in the LICENSE.txt file. + +# deps.bzl exports public definitions from v5 of these rules. Later versions +# require newer, incompatible versions of Skylib, so they have their own +# deps.bzl files. Clients should load the deps.bzl for whichever version +# they need. + +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +load("//v5/internal:repo.bzl", _go_download = "go_download") + +go_download = _go_download + +def go_rules_dependencies(): + """Declares external repositories that rules_go_simple depends on. This + function should be loaded and called from WORKSPACE files.""" + + # bazel_skylib is a set of libraries that are useful for writing + # Bazel rules. We use it to handle quoting arguments in shell commands. + _maybe( + http_archive, + name = "bazel_skylib", + urls = [ + "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.0.2/bazel-skylib-1.0.2.tar.gz", + "https://github.com/bazelbuild/bazel-skylib/releases/download/1.0.2/bazel-skylib-1.0.2.tar.gz", + ], + sha256 = "97e70364e9249702246c0e9444bccdc4b847bed1eb03c5a3ece4f83dfe6abc44", + ) + +def _maybe(rule, name, **kwargs): + """Declares an external repository if it hasn't been declared already.""" + if name not in native.existing_rules(): + rule(name = name, **kwargs) diff --git a/v5/internal/BUILD.bazel b/v5/internal/BUILD.bazel new file mode 100644 index 0000000..5e2c0a9 --- /dev/null +++ b/v5/internal/BUILD.bazel @@ -0,0 +1 @@ +# Empty build file, just here to define a package. diff --git a/v5/internal/BUILD.dist.bazel.tpl b/v5/internal/BUILD.dist.bazel.tpl new file mode 100644 index 0000000..4f0f760 --- /dev/null +++ b/v5/internal/BUILD.dist.bazel.tpl @@ -0,0 +1,55 @@ +# This template is used by go_download to generate a build file for +# a downloaded Go distribution. + +load("@rules_go_simple//v5:def.bzl", "go_toolchain") +load("@rules_go_simple//v5/internal:rules.bzl", "go_tool_binary") + +# tools contains executable files that are part of the toolchain. +filegroup( + name = "tools", + srcs = ["bin/go{exe}"] + glob(["pkg/tool/{goos}_{goarch}/**"]), + visibility = ["//visibility:public"], +) + +# std_pkgs contains packages in the standard library. +filegroup( + name = "std_pkgs", + srcs = glob( + ["pkg/{goos}_{goarch}/**"], + exclude = ["pkg/{goos}_{goarch}/cmd/**"], + ), + visibility = ["//visibility:public"], +) + +# builder is an executable used by rules_go_simple to perform most actions. +# builder mostly acts as a wrapper around the compiler and linker. +go_tool_binary( + name = "builder", + srcs = ["@rules_go_simple//v5/internal/builder:builder_srcs"], + std_pkgs = [":std_pkgs"], + tools = [":tools"], +) + +# toolchain_impl gathers information about the Go toolchain. +# See the GoToolchain provider. +go_toolchain( + name = "toolchain_impl", + builder = ":builder", + std_pkgs = [":std_pkgs"], + tools = [":tools"], +) + +# toolchain is a Bazel toolchain that expresses execution and target +# constraints for toolchain_impl. This target should be registered by +# calling register_toolchains in a WORKSPACE file. +toolchain( + name = "toolchain", + exec_compatible_with = [ + {exec_constraints}, + ], + target_compatible_with = [ + {target_constraints}, + ], + toolchain = ":toolchain_impl", + toolchain_type = "@rules_go_simple//v5:toolchain_type", +) diff --git a/v5/internal/actions.bzl b/v5/internal/actions.bzl new file mode 100644 index 0000000..2600c6f --- /dev/null +++ b/v5/internal/actions.bzl @@ -0,0 +1,124 @@ +# Copyright Jay Conrod. All rights reserved. + +# This file is part of rules_go_simple. Use of this source code is governed by +# the 3-clause BSD license that can be found in the LICENSE.txt file. + +load("@bazel_skylib//lib:shell.bzl", "shell") + +def go_compile(ctx, srcs, out, importpath = "", deps = []): + """Compiles a single Go package from sources. + + Args: + ctx: analysis context. + srcs: list of source Files to be compiled. + out: output .a File. + importpath: the path other libraries may use to import this package. + deps: list of GoLibrary objects for direct dependencies. + """ + toolchain = ctx.toolchains["@rules_go_simple//v5:toolchain_type"] + + args = ctx.actions.args() + args.add("compile") + args.add("-stdimportcfg", toolchain.internal.stdimportcfg) + dep_infos = [d.info for d in deps] + args.add_all(dep_infos, before_each = "-arc", map_each = _format_arc) + if importpath: + args.add("-p", importpath) + args.add("-o", out) + args.add_all(srcs) + + inputs = (srcs + + [dep.info.archive for dep in deps] + + [toolchain.internal.stdimportcfg] + + toolchain.internal.tools + + toolchain.internal.std_pkgs) + ctx.actions.run( + outputs = [out], + inputs = inputs, + executable = toolchain.internal.builder, + arguments = [args], + env = toolchain.internal.env, + mnemonic = "GoCompile", + ) + +def go_link(ctx, out, main, deps = []): + """Links a Go executable. + + Args: + ctx: analysis context. + out: output executable file. + main: archive file for the main package. + deps: list of GoLibrary objects for direct dependencies. + """ + toolchain = ctx.toolchains["@rules_go_simple//v5:toolchain_type"] + + transitive_deps = depset( + direct = [d.info for d in deps], + transitive = [d.deps for d in deps], + ) + inputs = ([main, toolchain.internal.stdimportcfg] + + [d.archive for d in transitive_deps.to_list()] + + toolchain.internal.tools + + toolchain.internal.std_pkgs) + + args = ctx.actions.args() + args.add("link") + args.add("-stdimportcfg", toolchain.internal.stdimportcfg) + args.add_all(transitive_deps, before_each = "-arc", map_each = _format_arc) + args.add("-main", main) + args.add("-o", out) + + ctx.actions.run( + outputs = [out], + inputs = inputs, + executable = toolchain.internal.builder, + arguments = [args], + env = toolchain.internal.env, + mnemonic = "GoLink", + ) + +def go_build_test(ctx, srcs, deps, out, rundir = "", importpath = ""): + """Compiles and links a Go test executable. + + Args: + ctx: analysis context. + srcs: list of source Files to be compiled. + deps: list of GoLibrary objects for direct dependencies. + out: output executable file. + importpath: import path of the internal test archive. + rundir: directory the test should change to before executing. + """ + toolchain = ctx.toolchains["@rules_go_simple//v5:toolchain_type"] + direct_dep_infos = [d.info for d in deps] + transitive_dep_infos = depset(transitive = [d.deps for d in deps]).to_list() + inputs = (srcs + + [toolchain.internal.stdimportcfg] + + [d.archive for d in direct_dep_infos] + + [d.archive for d in transitive_dep_infos] + + toolchain.internal.tools + + toolchain.internal.std_pkgs) + + args = ctx.actions.args() + args.add("test") + args.add("-stdimportcfg", toolchain.internal.stdimportcfg) + args.add_all(direct_dep_infos, before_each = "-direct", map_each = _format_arc) + args.add_all(transitive_dep_infos, before_each = "-transitive", map_each = _format_arc) + if rundir != "": + args.add("-dir", rundir) + if importpath != "": + args.add("-p", importpath) + args.add("-o", out) + args.add_all(srcs) + + ctx.actions.run( + outputs = [out], + inputs = inputs, + executable = toolchain.internal.builder, + arguments = [args], + env = toolchain.internal.env, + mnemonic = "GoTest", + ) + +def _format_arc(lib): + """Formats a GoLibrary.info object as an -arc argument""" + return "{}={}".format(lib.importpath, lib.archive.path) diff --git a/v5/internal/builder/BUILD.bazel b/v5/internal/builder/BUILD.bazel new file mode 100644 index 0000000..122bc6a --- /dev/null +++ b/v5/internal/builder/BUILD.bazel @@ -0,0 +1,17 @@ +# builder_srcs is a list of source files used to create the builder binary. +# Each Go distribution will create its own builder with these source files, +# using its own compiler, linker, and standard library. +filegroup( + name = "builder_srcs", + srcs = [ + "builder.go", + "compile.go", + "env.go", + "flags.go", + "importcfg.go", + "link.go", + "sourceinfo.go", + "test.go", + ], + visibility = ["//visibility:public"], +) diff --git a/v5/internal/builder/builder.go b/v5/internal/builder/builder.go new file mode 100644 index 0000000..1af36ed --- /dev/null +++ b/v5/internal/builder/builder.go @@ -0,0 +1,44 @@ +// Copyright Jay Conrod. All rights reserved. + +// This file is part of rules_go_simple. Use of this source code is governed by +// the 3-clause BSD license that can be found in the LICENSE.txt file. + +// builder is a tool used to perform various tasks related to building Go code, +// such as compiling packages, linking executables, and generating +// test sources. +package main + +import ( + "log" + "os" +) + +func main() { + log.SetFlags(0) + log.SetPrefix("builder: ") + if len(os.Args) <= 2 { + log.Fatalf("usage: %s stdimportcfg|compile|link|test options...", os.Args[0]) + } + verb := os.Args[1] + args := os.Args[2:] + + var action func(args []string) error + switch verb { + case "stdimportcfg": + action = stdImportcfg + case "compile": + action = compile + case "link": + action = link + case "test": + action = test + default: + log.Fatalf("unknown action: %s", verb) + } + log.SetPrefix(verb + ": ") + + err := action(args) + if err != nil { + log.Fatal(err) + } +} diff --git a/v5/internal/builder/compile.go b/v5/internal/builder/compile.go new file mode 100644 index 0000000..600d8b1 --- /dev/null +++ b/v5/internal/builder/compile.go @@ -0,0 +1,105 @@ +// Copyright Jay Conrod. All rights reserved. + +// This file is part of rules_go_simple. Use of this source code is governed by +// the 3-clause BSD license that can be found in the LICENSE.txt file. + +package main + +import ( + "flag" + "fmt" + "go/build" + "os" + "os/exec" +) + +// compile produces a Go archive file (.a) from a list of .go sources. This +// function will filter sources using build constraints (OS and architecture +// file name suffixes and +build comments) and will build an importcfg file +// before invoking the Go compiler. +func compile(args []string) error { + // Process command line arguments. + var stdImportcfgPath, packagePath, outPath string + var archives []archive + fs := flag.NewFlagSet("compile", flag.ExitOnError) + fs.StringVar(&stdImportcfgPath, "stdimportcfg", "", "path to importcfg for the standard library") + fs.Var(archiveFlag{&archives}, "arc", "information about dependencies, formatted as packagepath=file (may be repeated)") + fs.StringVar(&packagePath, "p", "", "package path for the package being compiled") + fs.StringVar(&outPath, "o", "", "path to archive file the compiler should produce") + fs.Parse(args) + srcPaths := fs.Args() + + // Extract metadata from source files and filter out sources using + // build constraints. + srcs := make([]sourceInfo, 0, len(srcPaths)) + filteredSrcPaths := make([]string, 0, len(srcPaths)) + bctx := &build.Default + for _, srcPath := range srcPaths { + if src, err := loadSourceInfo(bctx, srcPath); err != nil { + return err + } else if src.match { + srcs = append(srcs, src) + filteredSrcPaths = append(filteredSrcPaths, srcPath) + } + } + + // Build an importcfg file that maps this package's imports to archive files + // from the standard library or direct dependencies. + stdArchiveMap, err := readImportcfg(stdImportcfgPath) + if err != nil { + return err + } + + directArchiveMap := make(map[string]string) + for _, arc := range archives { + directArchiveMap[arc.packagePath] = arc.filePath + } + + archiveMap := make(map[string]string) + for _, src := range srcs { + for _, imp := range src.imports { + switch { + case imp == "unsafe": + continue + + case imp == "C": + return fmt.Errorf("%s: cgo not supported", src.fileName) + + case stdArchiveMap[imp] != "": + archiveMap[imp] = stdArchiveMap[imp] + + case directArchiveMap[imp] != "": + archiveMap[imp] = directArchiveMap[imp] + + default: + return fmt.Errorf("%s: import %q is not provided by any direct dependency", src.fileName, imp) + } + } + } + importcfgPath, err := writeTempImportcfg(archiveMap) + if err != nil { + return err + } + defer os.Remove(importcfgPath) + + // Invoke the compiler. + return runCompiler(packagePath, importcfgPath, filteredSrcPaths, outPath) +} + +func runCompiler(packagePath, importcfgPath string, srcPaths []string, outPath string) error { + args := []string{"tool", "compile"} + if packagePath != "" { + args = append(args, "-p", packagePath) + } + args = append(args, "-importcfg", importcfgPath) + args = append(args, "-o", outPath, "--") + args = append(args, srcPaths...) + goTool, err := findGoTool() + if err != nil { + return err + } + cmd := exec.Command(goTool, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} diff --git a/v5/internal/builder/env.go b/v5/internal/builder/env.go new file mode 100644 index 0000000..b7c165a --- /dev/null +++ b/v5/internal/builder/env.go @@ -0,0 +1,31 @@ +// Copyright Jay Conrod. All rights reserved. + +// This file is part of rules_go_simple. Use of this source code is governed by +// the 3-clause BSD license that can be found in the LICENSE.txt file. + +package main + +import ( + "fmt" + "os" + "path/filepath" + "runtime" +) + +// findGoTool finds and returns an absolute path to the Go command, based +// on the GOROOT environment variable. +func findGoTool() (string, error) { + goroot, ok := os.LookupEnv("GOROOT") + if !ok { + return "", fmt.Errorf("GOROOT not set") + } + absGoroot, err := filepath.Abs(goroot) + if err != nil { + return "", err + } + ext := "" + if runtime.GOOS == "windows" { + ext = ".exe" + } + return filepath.Join(absGoroot, "bin", "go"+ext), nil +} diff --git a/v5/internal/builder/flags.go b/v5/internal/builder/flags.go new file mode 100644 index 0000000..74c5519 --- /dev/null +++ b/v5/internal/builder/flags.go @@ -0,0 +1,64 @@ +// Copyright Jay Conrod. All rights reserved. + +// This file is part of rules_go_simple. Use of this source code is governed by +// the 3-clause BSD license that can be found in the LICENSE.txt file. + +package main + +import ( + "fmt" + "strings" +) + +// splitArgs splits an argument list into two lists: builder arguments (for this +// program) and tool arguments (for an underlying tool like the compiler). The +// "--" argument is used as a separator. If this argument is not found, all +// arguments are builder arguments. +func splitArgs(args []string) (builderArgs, toolArgs []string) { + builderArgs = args + for i, arg := range args { + if arg == "--" { + builderArgs = args[:i] + toolArgs = args[i+1:] + } + } + return builderArgs, toolArgs +} + +// archive is a mapping from a package path (e.g., "fmt") to a file system +// path to the package's archive (e.g., "/opt/go/pkg/linux_amd64/fmt.a"). +type archive struct { + packagePath, filePath string +} + +// archiveFlag parses archives from command line arguments. Archive values +// have the form "packagePath=filePath". +type archiveFlag struct { + archives *[]archive +} + +func (f archiveFlag) String() string { + if f.archives == nil { + return "" + } + b := &strings.Builder{} + sep := "" + for _, arc := range *f.archives { + fmt.Fprintf(b, "%s%s=%s", sep, arc.packagePath, arc.filePath) + sep = " " + } + return b.String() +} + +func (f archiveFlag) Set(value string) error { + pos := strings.IndexByte(value, '=') + if pos < 0 { + return fmt.Errorf("malformed -arc flag: %q", value) + } + arc := archive{ + packagePath: value[:pos], + filePath: value[pos+1:], + } + *f.archives = append(*f.archives, arc) + return nil +} diff --git a/v5/internal/builder/importcfg.go b/v5/internal/builder/importcfg.go new file mode 100644 index 0000000..57c2d13 --- /dev/null +++ b/v5/internal/builder/importcfg.go @@ -0,0 +1,122 @@ +// Copyright Jay Conrod. All rights reserved. + +// This file is part of rules_go_simple. Use of this source code is governed by +// the 3-clause BSD license that can be found in the LICENSE.txt file. + +package main + +import ( + "bytes" + "flag" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "sort" + "strings" +) + +// stdImportcfg produces an importcfg file for all the packages in the +// standard library. +func stdImportcfg(args []string) error { + // Process command line arguments. + var outPath string + fs := flag.NewFlagSet("stdimportcfg", flag.ExitOnError) + fs.StringVar(&outPath, "o", "", "path to standard library importcfg") + fs.Parse(args) + + // Walk the directory of compiled archives. Each archive's location + // corresponds with its package path, so we don't need to run 'go list'. + archiveMap := make(map[string]string) + goroot, ok := os.LookupEnv("GOROOT") + if !ok { + return fmt.Errorf("GOROOT not set") + } + pkgDir := filepath.Join(goroot, "pkg", runtime.GOOS+"_"+runtime.GOARCH) + err := filepath.Walk(pkgDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && strings.HasSuffix(path, ".a") { + pkgPath := filepath.ToSlash(path[len(pkgDir)+1 : len(path)-len(".a")]) + archiveMap[pkgPath] = path + } + return nil + }) + if err != nil { + return err + } + + return writeImportcfg(archiveMap, outPath) +} + +// readImportcfg parses an importcfg file. It returns a map from package paths +// to archive file paths. +func readImportcfg(importcfgPath string) (map[string]string, error) { + archiveMap := make(map[string]string) + + data, err := ioutil.ReadFile(importcfgPath) + if err != nil { + return nil, err + } + + // based on parsing code in cmd/link/internal/ld/ld.go + for lineNum, line := range strings.Split(string(data), "\n") { + lineNum++ // 1-based + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + var verb, args string + if i := strings.Index(line, " "); i < 0 { + verb = line + } else { + verb, args = line[:i], strings.TrimSpace(line[i+1:]) + } + var before, after string + if i := strings.Index(args, "="); i >= 0 { + before, after = args[:i], args[i+1:] + } + if verb == "packagefile" { + archiveMap[before] = after + } + } + + return archiveMap, nil +} + +// writeTempImportcfg writes a temporary importcfg file. The caller is +// responsible for deleting it. +func writeTempImportcfg(archiveMap map[string]string) (string, error) { + tmpFile, err := ioutil.TempFile("", "importcfg-*") + if err != nil { + return "", err + } + tmpPath := tmpFile.Name() + if err := tmpFile.Close(); err != nil { + os.Remove(tmpPath) + return "", err + } + if err := writeImportcfg(archiveMap, tmpPath); err != nil { + os.Remove(tmpPath) + return "", err + } + return tmpPath, nil +} + +func writeImportcfg(archiveMap map[string]string, outPath string) error { + pkgPaths := make([]string, 0, len(archiveMap)) + for pkgPath := range archiveMap { + pkgPaths = append(pkgPaths, pkgPath) + } + sort.Strings(pkgPaths) + + buf := &bytes.Buffer{} + for _, pkgPath := range pkgPaths { + fmt.Fprintf(buf, "packagefile %s=%s\n", pkgPath, archiveMap[pkgPath]) + } + + return ioutil.WriteFile(outPath, buf.Bytes(), 0666) +} diff --git a/v5/internal/builder/link.go b/v5/internal/builder/link.go new file mode 100644 index 0000000..d1c477b --- /dev/null +++ b/v5/internal/builder/link.go @@ -0,0 +1,60 @@ +// Copyright Jay Conrod. All rights reserved. + +// This file is part of rules_go_simple. Use of this source code is governed by +// the 3-clause BSD license that can be found in the LICENSE.txt file. + +package main + +import ( + "flag" + "fmt" + "os" + "os/exec" +) + +// link produces an executable file from a main archive file and a list of +// dependencies (both direct and transitive). +func link(args []string) error { + // Process command line arguments. + var stdImportcfgPath, mainPath, outPath string + var archives []archive + fs := flag.NewFlagSet("link", flag.ExitOnError) + fs.StringVar(&stdImportcfgPath, "stdimportcfg", "", "path to importcfg for the standard library") + fs.Var(archiveFlag{&archives}, "arc", "information about dependencies (including transitive dependencies), formatted as packagepath=file (may be repeated)") + fs.StringVar(&mainPath, "main", "", "path to main package archive file") + fs.StringVar(&outPath, "o", "", "path to binary file the linker should produce") + fs.Parse(args) + if len(fs.Args()) != 0 { + return fmt.Errorf("expected 0 positional arguments; got %d", len(fs.Args())) + } + + // Build an importcfg file. + archiveMap, err := readImportcfg(stdImportcfgPath) + if err != nil { + return err + } + for _, arc := range archives { + archiveMap[arc.packagePath] = arc.filePath + } + importcfgPath, err := writeTempImportcfg(archiveMap) + if err != nil { + return err + } + defer os.Remove(importcfgPath) + + // Invoke the linker. + return runLinker(mainPath, importcfgPath, outPath) +} + +func runLinker(mainPath, importcfgPath string, outPath string) error { + args := []string{"tool", "link", "-importcfg", importcfgPath, "-o", outPath} + args = append(args, "--", mainPath) + goTool, err := findGoTool() + if err != nil { + return err + } + cmd := exec.Command(goTool, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} diff --git a/v5/internal/builder/sourceinfo.go b/v5/internal/builder/sourceinfo.go new file mode 100644 index 0000000..c85c162 --- /dev/null +++ b/v5/internal/builder/sourceinfo.go @@ -0,0 +1,92 @@ +// Copyright Jay Conrod. All rights reserved. + +// This file is part of rules_go_simple. Use of this source code is governed by +// the 3-clause BSD license that can be found in the LICENSE.txt file. + +package main + +import ( + "go/ast" + "go/build" + "go/parser" + "go/token" + "path/filepath" + "strconv" + "strings" +) + +type sourceInfo struct { + fileName string + match bool + packageName string + imports []string + tests []string + hasTestMain bool +} + +// loadSourceInfo extracts metadata from a source file. +func loadSourceInfo(bctx *build.Context, fileName string) (sourceInfo, error) { + if match, err := bctx.MatchFile(filepath.Dir(fileName), filepath.Base(fileName)); err != nil { + return sourceInfo{}, err + } else if !match { + return sourceInfo{fileName: fileName}, nil + } + + fset := token.NewFileSet() + flags := parser.ImportsOnly + if strings.HasSuffix(fileName, "_test.go") { + flags = 0 + } + tree, err := parser.ParseFile(fset, fileName, nil, flags) + if err != nil { + return sourceInfo{}, err + } + + si := sourceInfo{ + fileName: fileName, + match: true, + packageName: tree.Name.Name, + } + for _, decl := range tree.Decls { + switch decl := decl.(type) { + case *ast.GenDecl: + if decl.Tok != token.IMPORT { + break + } + + for _, spec := range decl.Specs { + importSpec := spec.(*ast.ImportSpec) + importPath, err := strconv.Unquote(importSpec.Path.Value) + if err != nil { + panic(err) + } + si.imports = append(si.imports, importPath) + } + + case *ast.FuncDecl: + if decl.Recv != nil || + !strings.HasPrefix(decl.Name.Name, "Test") { + break + } + if decl.Name.Name == "TestMain" { + si.hasTestMain = true + break + } + + if len(decl.Type.Params.List) != 1 || + decl.Type.Results != nil { + break + } + starExpr, ok := decl.Type.Params.List[0].Type.(*ast.StarExpr) + if !ok { + break + } + selExpr, ok := starExpr.X.(*ast.SelectorExpr) + if !ok || selExpr.Sel.Name != "T" { + break + } + si.tests = append(si.tests, decl.Name.Name) + } + } + return si, nil +} diff --git a/v5/internal/builder/test.go b/v5/internal/builder/test.go new file mode 100644 index 0000000..3c0087a --- /dev/null +++ b/v5/internal/builder/test.go @@ -0,0 +1,258 @@ +// Copyright Jay Conrod. All rights reserved. + +// This file is part of rules_go_simple. Use of this source code is governed by +// the 3-clause BSD license that can be found in the LICENSE.txt file. + +package main + +import ( + "errors" + "flag" + "fmt" + "go/build" + "io/ioutil" + "os" + "strings" + "text/template" +) + +// testMainInfo contains information needed to generate the main .go file +// for a test binary. It's consumed in a template used by generateTestMain. +type testMainInfo struct { + Imports []testArchiveInfo + TestMainPackageName string + RunDir string +} + +// testArchiveInfo contains information about a test archive. Tests may build +// two archives (in addition to the main archive): an internal archive which +// is compiled together with the library under test, and an external archive +// which is not. The external archive may reference exported symbols in +// the internal test archive. +type testArchiveInfo struct { + ImportPath, PackageName string + Tests []string + + srcs []sourceInfo + srcPaths []string + hasTestMain bool +} + +// test produces a test executable from a list of .go sources. test filters +// sources into internal and external archives, which are compiled separately. +// test then generates a main .go file that starts the tests and compiles +// that into the main archive. Finally, test links the test executable. +func test(args []string) error { + // Parse command line arguments. + var stdImportcfgPath, packagePath, outPath, runDir string + var directArchives, transitiveArchives []archive + fs := flag.NewFlagSet("test", flag.ExitOnError) + fs.StringVar(&stdImportcfgPath, "stdimportcfg", "", "path to importcfg for the standard library") + fs.StringVar(&packagePath, "p", "default", "string used to import the test library") + fs.Var(archiveFlag{&directArchives}, "direct", "information about direct dependencies") + fs.Var(archiveFlag{&transitiveArchives}, "transitive", "information about transitive dependencies") + fs.StringVar(&outPath, "o", "", "path to binary file to generate") + fs.StringVar(&runDir, "dir", ".", "directory the test binary should change to before running") + fs.Parse(args) + srcPaths := fs.Args() + + // Filter sources into two archives: an internal package that gets compiled + // together with the library under test, and an external package that + // gets compiled separately. + testInfo := testArchiveInfo{ + ImportPath: packagePath, + PackageName: "test", + } + xtestInfo := testArchiveInfo{ + ImportPath: packagePath + "_test", + PackageName: "xtest", + } + packageName := "" + bctx := &build.Default + for _, srcPath := range srcPaths { + src, err := loadSourceInfo(bctx, srcPath) + if err != nil { + return err + } + if !src.match { + continue + } + + info := &testInfo + srcPackageName := src.packageName + if strings.HasSuffix(src.packageName, "_test") { + info = &xtestInfo + srcPackageName = src.packageName[:len(src.packageName)-len("_test")] + } + if packageName == "" { + packageName = srcPackageName + } else if packageName != srcPackageName { + return fmt.Errorf("%s: package name %q does not match package name %q in file %s", src.fileName, src.packageName, info.PackageName, srcPaths[0]) + } + info.Tests = append(info.Tests, src.tests...) + info.srcs = append(info.srcs, src) + info.srcPaths = append(info.srcPaths, srcPath) + info.hasTestMain = info.hasTestMain || src.hasTestMain + } + + // Build a map from package paths to archive files using the standard + // importcfg and -direct command line arguments. + archiveMap, err := readImportcfg(stdImportcfgPath) + if err != nil { + return err + } + for _, arc := range directArchives { + archiveMap[arc.packagePath] = arc.filePath + } + + // Compile each archive. + mainInfo := testMainInfo{RunDir: runDir} + var testArchivePath string + if len(testInfo.srcs) > 0 { + mainInfo.Imports = append(mainInfo.Imports, testInfo) + if testInfo.hasTestMain { + mainInfo.TestMainPackageName = testInfo.PackageName + } + + testArchivePath, err = compileTestArchive(testInfo.ImportPath, testInfo.srcPaths, testInfo.srcs, archiveMap) + if err != nil { + return err + } + defer os.Remove(testArchivePath) + archiveMap[packagePath] = testArchivePath + } + + var xtestArchivePath string + if len(xtestInfo.srcs) > 0 { + mainInfo.Imports = append(mainInfo.Imports, xtestInfo) + if xtestInfo.hasTestMain { + if testInfo.hasTestMain { + return errors.New("TestMain defined in both internal and external test files") + } + mainInfo.TestMainPackageName = xtestInfo.PackageName + } + + xtestArchivePath, err = compileTestArchive(xtestInfo.ImportPath, xtestInfo.srcPaths, xtestInfo.srcs, archiveMap) + if err != nil { + return err + } + defer os.Remove(xtestArchivePath) + archiveMap[packagePath+"_test"] = xtestArchivePath + } + + // Generate a source file and compile the main package, which imports + // the test libraries and starts the test. + testmainSrcPath, err := generateTestMain(mainInfo) + if err != nil { + return err + } + defer os.Remove(testmainSrcPath) + + for _, arc := range transitiveArchives { + archiveMap[arc.packagePath] = arc.filePath + } + importcfgPath, err := writeTempImportcfg(archiveMap) + if err != nil { + return err + } + defer os.Remove(importcfgPath) + + testMainArchiveFile, err := ioutil.TempFile("", "*-testmain.a") + if err != nil { + return err + } + testMainArchivePath := testMainArchiveFile.Name() + defer os.Remove(testMainArchivePath) + if err := testMainArchiveFile.Close(); err != nil { + return err + } + if err := runCompiler("main", importcfgPath, []string{testmainSrcPath}, testMainArchivePath); err != nil { + return err + } + + // Link everything together. + return runLinker(testMainArchivePath, importcfgPath, outPath) +} + +func compileTestArchive(packagePath string, srcPaths []string, srcs []sourceInfo, archiveMap map[string]string) (string, error) { + importcfgPath, err := writeTempImportcfg(archiveMap) + if err != nil { + return "", err + } + + tmpArchiveFile, err := ioutil.TempFile("", "*-test.a") + if err != nil { + return "", err + } + tmpArchivePath := tmpArchiveFile.Name() + if err := tmpArchiveFile.Close(); err != nil { + os.Remove(tmpArchivePath) + return "", err + } + + if err := runCompiler(packagePath, importcfgPath, srcPaths, tmpArchivePath); err != nil { + os.Remove(tmpArchivePath) + return "", err + } + + return tmpArchivePath, nil +} + +var testmainTpl = template.Must(template.New("testmain").Parse(` +// Code generated by @rules_go_simple//v5/internal/builder:test.go. DO NOT EDIT. + +package main + +import ( + "log" + "os" + "testing" + "testing/internal/testdeps" + +{{range .Imports}} + {{.PackageName}} "{{.ImportPath}}" +{{end}} +) + +var allTests = []testing.InternalTest{ +{{range $p := .Imports}} +{{range $t := $p.Tests}} + {"{{$t}}", {{$p.PackageName}}.{{$t}}}, +{{end}} +{{end}} +} + +func main() { + if err := os.Chdir("{{.RunDir}}"); err != nil { + log.Fatalf("could not change to test directory: %v", err) + } + + m := testing.MainStart(testdeps.TestDeps{}, allTests, nil, nil) +{{if .TestMainPackageName}} + {{.TestMainPackageName}}.TestMain(m) +{{else}} + os.Exit(m.Run()) +{{end}} +} +`)) + +func generateTestMain(mainInfo testMainInfo) (testmainPath string, err error) { + testmainFile, err := ioutil.TempFile("", "*-testmain.go") + if err != nil { + return "", err + } + tmpPath := testmainFile.Name() // testmainPath only set on success + defer func() { + if cerr := testmainFile.Close(); cerr != nil && err == nil { + err = cerr + } + if err != nil { + os.Remove(tmpPath) + } + }() + + if err := testmainTpl.Execute(testmainFile, mainInfo); err != nil { + return "", err + } + return tmpPath, nil +} diff --git a/v5/internal/providers.bzl b/v5/internal/providers.bzl new file mode 100644 index 0000000..142b80e --- /dev/null +++ b/v5/internal/providers.bzl @@ -0,0 +1,49 @@ +# Copyright Jay Conrod. All rights reserved. + +# This file is part of rules_go_simple. Use of this source code is governed by +# the 3-clause BSD license that can be found in the LICENSE.txt file. + +GoLibrary = provider( + doc = "Contains information about a Go library", + fields = { + "info": """A struct containing information about this library. + Has the following fields: + importpath: Name by which the library may be imported. + archive: The .a file compiled from the library's sources. + """, + "deps": "A depset of info structs for this library's dependencies", + }, +) + +GoToolchain = provider( + doc = "Contains information about a Go toolchain", + fields = { + "compile": """Function that compiles a Go package from sources. + + Args: + ctx: analysis context. + srcs: list of source Files to be compiled. + out: output .a file. + importpath: the path other libraries may use to import this package. + deps: list of GoLibrary objects for direct dependencies. + """, + "link": """Function that links a Go executable. + + Args: + ctx: analysis context. + out: ouptut executable file. + main: archive File for the main package. + deps: list of GoLibrary objects for direct dependencies. + """, + "build_test": """Function that compiles and links a test executable. + + Args: + ctx: analysis context. + srcs: list of source Files to be compiled. + deps: list of GoLibrary objects for direct dependencies. + out: output executable file. + importpath: import path of the internal test archive. + rundir: directory the test should change to before executing. + """, + }, +) diff --git a/v5/internal/repo.bzl b/v5/internal/repo.bzl new file mode 100644 index 0000000..3520977 --- /dev/null +++ b/v5/internal/repo.bzl @@ -0,0 +1,72 @@ +# Copyright Jay Conrod. All rights reserved. + +# This file is part of rules_go_simple. Use of this source code is governed by +# the 3-clause BSD license that can be found in the LICENSE.txt file. + +def _go_download_impl(ctx): + # Download the Go distribution. + ctx.report_progress("downloading") + ctx.download_and_extract( + ctx.attr.urls, + sha256 = ctx.attr.sha256, + stripPrefix = "go", + ) + + # Add a build file to the repository root directory. + # We need to fill in some template parameters, based on the platform. + ctx.report_progress("generating build file") + if ctx.attr.goos == "darwin": + os_constraint = "@platforms//os:osx" + elif ctx.attr.goos == "linux": + os_constraint = "@platforms//os:linux" + elif ctx.attr.goos == "windows": + os_constraint = "@platforms//os:windows" + else: + fail("unsupported goos: " + ctx.attr.goos) + if ctx.attr.goarch == "amd64": + arch_constraint = "@platforms//cpu:x86_64" + else: + fail("unsupported arch: " + ctx.attr.goarch) + constraints = [os_constraint, arch_constraint] + constraint_str = ",\n ".join(['"%s"' % c for c in constraints]) + + substitutions = { + "{goos}": ctx.attr.goos, + "{goarch}": ctx.attr.goarch, + "{exe}": ".exe" if ctx.attr.goos == "windows" else "", + "{exec_constraints}": constraint_str, + "{target_constraints}": constraint_str, + } + ctx.template( + "BUILD.bazel", + ctx.attr._build_tpl, + substitutions = substitutions, + ) + +go_download = repository_rule( + implementation = _go_download_impl, + attrs = { + "urls": attr.string_list( + mandatory = True, + doc = "List of mirror URLs where a Go distribution archive can be downloaded", + ), + "sha256": attr.string( + mandatory = True, + doc = "Expected SHA-256 sum of the downloaded archive", + ), + "goos": attr.string( + mandatory = True, + values = ["darwin", "linux", "windows"], + doc = "Host operating system for the Go distribution", + ), + "goarch": attr.string( + mandatory = True, + values = ["amd64"], + doc = "Host architecture for the Go distribution", + ), + "_build_tpl": attr.label( + default = "@rules_go_simple//v5/internal:BUILD.dist.bazel.tpl", + ), + }, + doc = "Downloads a standard Go distribution and installs a build file", +) diff --git a/v5/internal/rules.bzl b/v5/internal/rules.bzl new file mode 100644 index 0000000..ec3b93c --- /dev/null +++ b/v5/internal/rules.bzl @@ -0,0 +1,237 @@ +# Copyright Jay Conrod. All rights reserved. + +# This file is part of rules_go_simple. Use of this source code is governed by +# the 3-clause BSD license that can be found in the LICENSE.txt file. + +load("@bazel_skylib//lib:shell.bzl", "shell") +load(":providers.bzl", "GoLibrary") + +def _go_binary_impl(ctx): + # Load the toolchain. + go_toolchain = ctx.toolchains["@rules_go_simple//v5:toolchain_type"] + + # Declare an output file for the main package and compile it from srcs. All + # our output files will start with a prefix to avoid conflicting with + # other rules. + main_archive = ctx.actions.declare_file("{name}_/main.a".format(name = ctx.label.name)) + go_toolchain.compile( + ctx, + srcs = ctx.files.srcs, + deps = [dep[GoLibrary] for dep in ctx.attr.deps], + out = main_archive, + ) + + # Declare an output file for the executable and link it. Note that output + # files may not have the same name as the rule, so we still need to use the + # prefix here. + executable_path = "{name}_/{name}".format(name = ctx.label.name) + executable = ctx.actions.declare_file(executable_path) + go_toolchain.link( + ctx, + main = main_archive, + deps = [dep[GoLibrary] for dep in ctx.attr.deps], + out = executable, + ) + + # Return the DefaultInfo provider. This tells Bazel what files should be + # built when someone asks to build a go_binary rule. It also says which + # file is executable (in this case, there's only one). + return [DefaultInfo( + files = depset([executable]), + runfiles = ctx.runfiles(collect_data = True), + executable = executable, + )] + +# Declare the go_binary rule. This statement is evaluated during the loading +# phase when this file is loaded. The function body above is evaluated only +# during the analysis phase. +go_binary = rule( + _go_binary_impl, + attrs = { + "srcs": attr.label_list( + allow_files = [".go"], + doc = "Source files to compile for the main package of this binary", + ), + "deps": attr.label_list( + providers = [GoLibrary], + doc = "Direct dependencies of the binary", + ), + "data": attr.label_list( + allow_files = True, + doc = "Data files available to this binary at run-time", + ), + }, + doc = "Builds an executable program from Go source code", + executable = True, + toolchains = ["@rules_go_simple//v5:toolchain_type"], +) + +def _go_tool_binary_impl(ctx): + # Locate the go command. We use it to invoke the compiler and linker. + go_cmd = None + for f in ctx.files.tools: + if f.path.endswith("/bin/go") or f.path.endswith("/bin/go.exe"): + go_cmd = f + break + if not go_cmd: + fail("could not locate Go command") + + # Declare the output executable file. + executable_path = "{name}_/{name}".format(name = ctx.label.name) + executable = ctx.actions.declare_file(executable_path) + + # Create a shell command that compiles and links the binary. + cmd_tpl = ("{go} tool compile -o {out}.a {srcs} && " + + "{go} tool link -o {out} {out}.a") + cmd = cmd_tpl.format( + go = shell.quote(go_cmd.path), + out = shell.quote(executable.path), + srcs = " ".join([shell.quote(src.path) for src in ctx.files.srcs]), + ) + inputs = ctx.files.srcs + ctx.files.tools + ctx.files.std_pkgs + ctx.actions.run_shell( + outputs = [executable], + inputs = inputs, + command = cmd, + mnemonic = "GoToolBuild", + ) + + return [DefaultInfo( + files = depset([executable]), + executable = executable, + )] + +go_tool_binary = rule( + implementation = _go_tool_binary_impl, + attrs = { + "srcs": attr.label_list( + allow_files = [".go"], + mandatory = True, + doc = "Source files to compile for the main package of this binary", + ), + "tools": attr.label_list( + allow_files = True, + mandatory = True, + doc = "Executable files that are part of a Go distribution", + ), + "std_pkgs": attr.label_list( + allow_files = True, + mandatory = True, + doc = "Pre-compiled standard library packages that are part of a Go distribution", + ), + }, + doc = """Builds an executable program for the Go toolchain. + +go_tool_binary is a simple version of go_binary. It is separate from go_binary +because go_binary depends on the Go toolchain, and the toolchain uses a binary +built with this rule to do most of its work. + +This rule does not support dependencies or build constraints. All source files +will be compiled, and they may only depend on the standard library. +""", + executable = True, +) + +def _go_library_impl(ctx): + # Load the toolchain. + toolchain = ctx.toolchains["@rules_go_simple//v5:toolchain_type"] + + # Declare an output file for the library package and compile it from srcs. + archive = ctx.actions.declare_file("{name}_/pkg.a".format(name = ctx.label.name)) + toolchain.compile( + ctx, + srcs = ctx.files.srcs, + importpath = ctx.attr.importpath, + deps = [dep[GoLibrary] for dep in ctx.attr.deps], + out = archive, + ) + + # Return the output file and metadata about the library. + return [ + DefaultInfo( + files = depset([archive]), + runfiles = ctx.runfiles(collect_data = True), + ), + GoLibrary( + info = struct( + importpath = ctx.attr.importpath, + archive = archive, + ), + deps = depset( + direct = [dep[GoLibrary].info for dep in ctx.attr.deps], + transitive = [dep[GoLibrary].deps for dep in ctx.attr.deps], + ), + ), + ] + +go_library = rule( + _go_library_impl, + attrs = { + "srcs": attr.label_list( + allow_files = [".go"], + doc = "Source files to compile", + ), + "deps": attr.label_list( + providers = [GoLibrary], + doc = "Direct dependencies of the library", + ), + "data": attr.label_list( + allow_files = True, + doc = "Data files available to binaries using this library", + ), + "importpath": attr.string( + mandatory = True, + doc = "Name by which the library may be imported", + ), + }, + doc = "Compiles a Go archive from Go sources and dependencies", + toolchains = ["@rules_go_simple//v5:toolchain_type"], +) + +def _go_test_impl(ctx): + toolchain = ctx.toolchains["@rules_go_simple//v5:toolchain_type"] + + executable_path = "{name}_/{name}".format(name = ctx.label.name) + executable = ctx.actions.declare_file(executable_path) + toolchain.build_test( + ctx, + srcs = ctx.files.srcs, + deps = [dep[GoLibrary] for dep in ctx.attr.deps], + out = executable, + importpath = ctx.attr.importpath, + rundir = ctx.label.package, + ) + + return [DefaultInfo( + files = depset([executable]), + runfiles = ctx.runfiles(collect_data = True), + executable = executable, + )] + +go_test = rule( + implementation = _go_test_impl, + attrs = { + "srcs": attr.label_list( + allow_files = [".go"], + doc = ("Source files to compile for this test. " + + "May be a mix of internal and external tests."), + ), + "deps": attr.label_list( + providers = [GoLibrary], + doc = "Direct dependencies of the test", + ), + "data": attr.label_list( + allow_files = True, + doc = "Data files available to this test", + ), + "importpath": attr.string( + default = "", + doc = "Name by which test archives may be imported (optional)", + ), + }, + doc = """Compiles and links a Go test executable. Functions with names +starting with "Test" in files with names ending in "_test.go" will be called +using the go "testing" framework.""", + test = True, + toolchains = ["@rules_go_simple//v5:toolchain_type"], +) diff --git a/v5/internal/toolchain.bzl b/v5/internal/toolchain.bzl new file mode 100644 index 0000000..294f938 --- /dev/null +++ b/v5/internal/toolchain.bzl @@ -0,0 +1,84 @@ +# Copyright Jay Conrod. All rights reserved. + +# This file is part of rules_go_simple. Use of this source code is governed by +# the 3-clause BSD license that can be found in the LICENSE.txt file. + +load( + "@bazel_skylib//lib:paths.bzl", + "paths", +) +load( + ":providers.bzl", + "GoToolchain", +) +load( + ":actions.bzl", + "go_build_test", + "go_compile", + "go_link", +) + +def _go_toolchain_impl(ctx): + # Find important files and paths. + go_cmd = None + for f in ctx.files.tools: + if f.path.endswith("/bin/go") or f.path.endswith("/bin/go.exe"): + go_cmd = f + break + if not go_cmd: + fail("could not locate go command") + env = {"GOROOT": paths.dirname(paths.dirname(go_cmd.path))} + + # Generate the package list from the standard library. + stdimportcfg = ctx.actions.declare_file(ctx.label.name + ".importcfg") + ctx.actions.run( + outputs = [stdimportcfg], + inputs = ctx.files.tools + ctx.files.std_pkgs, + arguments = ["stdimportcfg", "-o", stdimportcfg.path], + env = env, + executable = ctx.executable.builder, + mnemonic = "GoStdImportcfg", + ) + + # Return a TooclhainInfo provider. This is the object that rules get + # when they ask for the toolchain. + return [platform_common.ToolchainInfo( + # Functions that generate actions. Rules may call these. + # This is the public interface of the toolchain. + compile = go_compile, + link = go_link, + build_test = go_build_test, + + # Internal data. Contents may change without notice. + # Think of these like private fields in a class. Actions may use these + # (they are methods of the class) but rules may not (they are clients). + internal = struct( + go_cmd = go_cmd, + env = env, + stdimportcfg = stdimportcfg, + builder = ctx.executable.builder, + tools = ctx.files.tools, + std_pkgs = ctx.files.std_pkgs, + ), + )] + +go_toolchain = rule( + implementation = _go_toolchain_impl, + attrs = { + "builder": attr.label( + mandatory = True, + executable = True, + cfg = "host", + doc = "Executable that performs most actions", + ), + "tools": attr.label_list( + mandatory = True, + doc = "Compiler, linker, and other executables from the Go distribution", + ), + "std_pkgs": attr.label_list( + mandatory = True, + doc = "Standard library packages from the Go distribution", + ), + }, + doc = "Gathers functions and file lists needed for a Go toolchain", +) diff --git a/v5/tests/BUILD.bazel b/v5/tests/BUILD.bazel new file mode 100644 index 0000000..2e4c541 --- /dev/null +++ b/v5/tests/BUILD.bazel @@ -0,0 +1,88 @@ +load( + "//v5:def.bzl", + "go_binary", + "go_library", + "go_test", +) + +go_test( + name = "hello_test", + srcs = ["hello_test.go"], + args = ["-hello=$(location :hello)"], + data = [":hello"], +) + +go_binary( + name = "hello", + srcs = [ + "hello.go", + "message.go", + ], +) + +go_test( + name = "bin_with_libs_test", + srcs = ["bin_with_libs_test.go"], + args = ["$(location :bin_with_libs)"], + data = [":bin_with_libs"], +) + +go_binary( + name = "bin_with_libs", + srcs = ["bin_with_libs.go"], + deps = [":foo"], +) + +go_library( + name = "foo", + srcs = ["foo.go"], + importpath = "rules_go_simple/v5/tests/foo", + deps = [ + ":bar", + ":baz", + ], +) + +go_library( + name = "bar", + srcs = ["bar.go"], + importpath = "rules_go_simple/v5/tests/bar", + deps = [":baz"], +) + +go_library( + name = "baz", + srcs = ["baz.go"], + importpath = "rules_go_simple/v5/tests/baz", +) + +go_test( + name = "data_test", + srcs = ["data_test.go"], + args = ["$(location :list_data_bin)"], + data = [":list_data_bin"], +) + +go_binary( + name = "list_data_bin", + srcs = ["list_data_bin.go"], + deps = [":list_data_lib"], + data = ["foo.txt"], +) + +go_library( + name = "list_data_lib", + srcs = ["list_data_lib.go"], + data = ["bar.txt"], + importpath = "rules_go_simple/v5/tests/list_data_lib" +) + +go_test( + name = "internal_external_test", + importpath = "rules_go_simple/v5/tests/ix", + srcs = [ + "internal_external_lib.go", + "internal_test.go", + "external_test.go", + ], +) diff --git a/v5/tests/bar.go b/v5/tests/bar.go new file mode 100644 index 0000000..7921c8b --- /dev/null +++ b/v5/tests/bar.go @@ -0,0 +1,16 @@ +// Copyright Jay Conrod. All rights reserved. + +// This file is part of rules_go_simple. Use of this source code is governed by +// the 3-clause BSD license that can be found in the LICENSE.txt file. + +package bar + +import ( + "fmt" + "rules_go_simple/v5/tests/baz" +) + +func Bar() { + fmt.Println("bar") + baz.Baz() +} diff --git a/v5/tests/bar.txt b/v5/tests/bar.txt new file mode 100644 index 0000000..e69de29 diff --git a/v5/tests/baz.go b/v5/tests/baz.go new file mode 100644 index 0000000..787389a --- /dev/null +++ b/v5/tests/baz.go @@ -0,0 +1,12 @@ +// Copyright Jay Conrod. All rights reserved. + +// This file is part of rules_go_simple. Use of this source code is governed by +// the 3-clause BSD license that can be found in the LICENSE.txt file. + +package baz + +import "fmt" + +func Baz() { + fmt.Println("baz") +} diff --git a/v5/tests/bin_with_libs.go b/v5/tests/bin_with_libs.go new file mode 100644 index 0000000..6a2fe8b --- /dev/null +++ b/v5/tests/bin_with_libs.go @@ -0,0 +1,12 @@ +// Copyright Jay Conrod. All rights reserved. + +// This file is part of rules_go_simple. Use of this source code is governed by +// the 3-clause BSD license that can be found in the LICENSE.txt file. + +package main + +import "rules_go_simple/v5/tests/foo" + +func main() { + foo.Foo() +} diff --git a/v5/tests/bin_with_libs_test.go b/v5/tests/bin_with_libs_test.go new file mode 100644 index 0000000..8dbc301 --- /dev/null +++ b/v5/tests/bin_with_libs_test.go @@ -0,0 +1,27 @@ +// Copyright Jay Conrod. All rights reserved. + +// This file is part of rules_go_simple. Use of this source code is governed by +// the 3-clause BSD license that can be found in the LICENSE.txt file. + +package main + +import ( + "bytes" + "flag" + "os/exec" + "strings" + "testing" +) + +func TestBinWithLibs(t *testing.T) { + binPath := strings.TrimPrefix(flag.Args()[0], "v5/tests/") + got, err := exec.Command(binPath).Output() + if err != nil { + t.Fatal(err) + } + got = bytes.TrimSpace(got) + want := []byte("foo\nbar\nbaz\nbaz") + if !bytes.Equal(got, want) { + t.Errorf("got:\n%s\n\nwant:\n%s\n", got, want) + } +} diff --git a/v5/tests/bin_with_libs_test.sh b/v5/tests/bin_with_libs_test.sh new file mode 100755 index 0000000..f1cc84d --- /dev/null +++ b/v5/tests/bin_with_libs_test.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Copyright Jay Conrod. All rights reserved. + +# This file is part of rules_go_simple. Use of this source code is governed by +# the 3-clause BSD license that can be found in the LICENSE.txt file. + +set -euo pipefail + +program="$1" +got=$("$program") +want="foo +bar +baz +baz" + +if [ "$got" != "$want" ]; then + cat >&2 <&2 + echo "error: program output does not contain $w" >&2 + exit 1 + fi +done diff --git a/v5/tests/external_test.go b/v5/tests/external_test.go new file mode 100644 index 0000000..304eefd --- /dev/null +++ b/v5/tests/external_test.go @@ -0,0 +1,35 @@ +// Copyright Jay Conrod. All rights reserved. + +// This file is part of rules_go_simple. Use of this source code is governed by +// the 3-clause BSD license that can be found in the LICENSE.txt file. + +package ix_test + +import ( + "flag" + "log" + "os" + "rules_go_simple/v5/tests/ix" + "testing" +) + +var TestFooHelperCalled = false + +func TestFooHelper(t *testing.T) { + if got := ix.Helper(ix.Foo); got != "foofoo" { + t.Errorf("got %q; want \"foofoo\"", got) + } + TestFooHelperCalled = true +} + +func TestMain(m *testing.M) { + flag.Parse() + code := m.Run() + if !ix.TestFooCalled { + log.Fatal("TestFooCalled is false") + } + if !TestFooHelperCalled { + log.Fatal("TestFooHelperCalled is false") + } + os.Exit(code) +} diff --git a/v5/tests/foo.go b/v5/tests/foo.go new file mode 100644 index 0000000..52b5d0e --- /dev/null +++ b/v5/tests/foo.go @@ -0,0 +1,18 @@ +// Copyright Jay Conrod. All rights reserved. + +// This file is part of rules_go_simple. Use of this source code is governed by +// the 3-clause BSD license that can be found in the LICENSE.txt file. + +package foo + +import ( + "fmt" + "rules_go_simple/v5/tests/bar" + "rules_go_simple/v5/tests/baz" +) + +func Foo() { + fmt.Println("foo") + bar.Bar() + baz.Baz() +} diff --git a/v5/tests/foo.txt b/v5/tests/foo.txt new file mode 100644 index 0000000..e69de29 diff --git a/v5/tests/hello.go b/v5/tests/hello.go new file mode 100644 index 0000000..901f12b --- /dev/null +++ b/v5/tests/hello.go @@ -0,0 +1,12 @@ +// Copyright Jay Conrod. All rights reserved. + +// This file is part of rules_go_simple. Use of this source code is governed by +// the 3-clause BSD license that can be found in the LICENSE.txt file. + +package main + +import "fmt" + +func main() { + fmt.Println(message) +} diff --git a/v5/tests/hello_test.go b/v5/tests/hello_test.go new file mode 100644 index 0000000..9592b98 --- /dev/null +++ b/v5/tests/hello_test.go @@ -0,0 +1,28 @@ +// Copyright Jay Conrod. All rights reserved. + +// This file is part of rules_go_simple. Use of this source code is governed by +// the 3-clause BSD license that can be found in the LICENSE.txt file. + +package hello_test + +import ( + "bytes" + "flag" + "os/exec" + "strings" + "testing" +) + +var helloPath = flag.String("hello", "", "path to hello binary") + +func TestHello(t *testing.T) { + cmd := exec.Command(strings.TrimPrefix(*helloPath, "v5/tests/")) + out, err := cmd.Output() + if err != nil { + t.Fatal(err) + } + got := string(bytes.TrimSpace(out)) + if want := "Hello, world!"; got != want { + t.Errorf("got %q; want %q", got, want) + } +} diff --git a/v5/tests/hello_test.sh b/v5/tests/hello_test.sh new file mode 100755 index 0000000..4263811 --- /dev/null +++ b/v5/tests/hello_test.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Copyright Jay Conrod. All rights reserved. + +# This file is part of rules_go_simple. Use of this source code is governed by +# the 3-clause BSD license that can be found in the LICENSE.txt file. + +set -euo pipefail + +program="$1" +got=$("$program") +want="Hello, world!" + +if [ "$got" != "$want" ]; then + cat >&2 <