aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJay Conrod <jayconrod@gmail.com>2018-03-28 16:35:29 -0400
committerGitHub <noreply@github.com>2018-03-28 16:35:29 -0400
commit27b444a6025b8d0eddff27b2fa170fdc9c93b1af (patch)
treecbc17879bdb02682a0484669d70ba139feb848ad
parent7c8af872cab81d70ccece4059f5dcada88f5d4d2 (diff)
downloadbazelbuild-rules_go-27b444a6025b8d0eddff27b2fa170fdc9c93b1af.tar.gz
Rewrite go_path rule (#1409)
* Rewrite go_path rule * Implementation moved to //go/tools/builders:go_path * Added "archive" mode, which produces a zip file. * "copy" mode now produces a tree artifact. * go_path accepts a "data" attribute, for additional files. * Added documentation for the go_path rule and GoPath provider. Fixes #1370 Fixes #1371 Fixes #1372 Fixes #1329 Fixes #1373 * review feedback
-rw-r--r--go/core.rst60
-rw-r--r--go/private/tools/path.bzl209
-rw-r--r--go/private/tools/vet.bzl4
-rw-r--r--go/providers.rst29
-rw-r--r--go/tools/builders/BUILD.bazel6
-rw-r--r--go/tools/builders/go_path.go202
-rw-r--r--tests/README.rst2
-rw-r--r--tests/core/README.rst6
-rw-r--r--tests/core/go_path/BUILD.bazel33
-rw-r--r--tests/core/go_path/README.rst12
-rw-r--r--tests/core/go_path/cmd/bin/BUILD.bazel14
-rw-r--r--tests/core/go_path/cmd/bin/bin.go4
-rw-r--r--tests/core/go_path/extra.txt0
-rw-r--r--tests/core/go_path/go_path_test.go147
-rw-r--r--tests/core/go_path/pkg/lib/BUILD.bazel40
-rw-r--r--tests/core/go_path/pkg/lib/data.txt0
-rw-r--r--tests/core/go_path/pkg/lib/embed_test.go1
-rw-r--r--tests/core/go_path/pkg/lib/external_test.go1
-rw-r--r--tests/core/go_path/pkg/lib/internal_test.go1
-rw-r--r--tests/core/go_path/pkg/lib/lib.go3
-rw-r--r--tests/core/go_path/pkg/lib/vendored.go1
21 files changed, 687 insertions, 88 deletions
diff --git a/go/core.rst b/go/core.rst
index b6396b78..f31f9089 100644
--- a/go/core.rst
+++ b/go/core.rst
@@ -8,6 +8,7 @@ Core go rules
.. _GoLibrary: providers.rst#GoLibrary
.. _GoSource: providers.rst#GoSource
.. _GoArchive: providers.rst#GoArchive
+.. _GoPath: providers.rst#GoPath
.. _cgo: http://golang.org/cmd/cgo/
.. _"Make variable": https://docs.bazel.build/versions/master/be/make-variables.html
.. _Bourne shell tokenization: https://docs.bazel.build/versions/master/be/common-definitions.html#sh-tokenization
@@ -622,6 +623,65 @@ Attributes
| Subject to `"Make variable"`_ substitution and `Bourne shell tokenization`_. |
+----------------------------+-----------------------------+---------------------------------------+
+go_path
+~~~~~~~
+
+``go_path`` builds a directory structure that can be used with tools that
+understand the ``GOPATH`` directory layout. This directory structure can be
+built by zipping, copying, or linking files.
+
+``go_path`` can depend on one or more Go targets (i.e., `go_library`_,
+`go_binary`_, or `go_test`_). It will include packages from those targets, as
+well as their transitive dependencies. Packages will be in subdirectories named
+after their ``importpath`` or ``importmap`` attributes under a ``src/``
+directory.
+
+Attributes
+^^^^^^^^^^
+
++----------------------------+-----------------------------+---------------------------------------+
+| **Name** | **Type** | **Default value** |
++----------------------------+-----------------------------+---------------------------------------+
+| :param:`name` | :type:`string` | |mandatory| |
++----------------------------+-----------------------------+---------------------------------------+
+| A unique name for this rule. |
++----------------------------+-----------------------------+---------------------------------------+
+| :param:`deps` | :type:`label_list` | :value:`[]` |
++----------------------------+-----------------------------+---------------------------------------+
+| A list of targets that build Go packages. A directory will be generated from |
+| files in these targets and their transitive dependencies. All targets must |
+| provide GoArchive_ (`go_library`_, `go_binary`_, `go_test`_, and similar |
+| rules have this). |
+| |
+| Only targets with explicit ``importpath`` attributes will be included in the |
+| generated directory. Synthetic packages (like the main package produced by |
+| `go_test`_) and packages with inferred import paths will not be |
+| included. The values of ``importmap`` attributes may influence the placement |
+| of packages within the generated directory (for example, in vendor |
+| directories). |
+| |
+| The generated directory will contain original source files, including .go, |
+| .s, .h, and .c files compiled by cgo. It will not contain files generated by |
+| tools like cover and cgo, but it will contain generated files passed in |
+| ``srcs`` attributes like .pb.go files. The generated directory will also |
+| contain runfiles found in ``data`` attributes. |
++----------------------------+-----------------------------+---------------------------------------+
+| :param:`data` | :type:`label_list` | :value:`[]` |
++----------------------------+-----------------------------+---------------------------------------+
+| A list of targets producing data files that will be stored next to the |
+| ``src/`` directory. Useful for including things like licenses and readmes. |
++----------------------------+-----------------------------+---------------------------------------+
+| :param:`mode` | :type:`string` | :value:`"copy"` |
++----------------------------+-----------------------------+---------------------------------------+
+| Determines how the generated directory is provided. May be one of: |
+| |
+| * ``"archive"``: The generated directory is packaged as a single .zip file. |
+| * ``"copy"``: The generated directory is a single tree artifact. Source files |
+| are copied into the tree. |
+| * ``"link"``: Source files are symlinked into the tree. All of the symlink |
+| files are provided as separate output files. |
++----------------------------+-----------------------------+---------------------------------------+
+
go_rule
~~~~~~~
diff --git a/go/private/tools/path.bzl b/go/private/tools/path.bzl
index e5a4bdad..ffad6b37 100644
--- a/go/private/tools/path.bzl
+++ b/go/private/tools/path.bzl
@@ -15,122 +15,165 @@
load(
"@io_bazel_rules_go//go/private:context.bzl",
"go_context",
+ "EXPLICIT_PATH",
)
load(
"@io_bazel_rules_go//go/private:providers.bzl",
- "GoLibrary",
+ "GoArchive",
"GoPath",
"get_archive",
)
load(
"@io_bazel_rules_go//go/private:common.bzl",
"as_iterable",
+ "as_list",
)
load(
"@io_bazel_rules_go//go/private:rules/rule.bzl",
"go_rule",
)
-def _tag(go, path, outputs):
- """this generates a existance tag file for dependencies, and returns the path to the tag file"""
- tag = go.declare_file(go, path=path+".tag")
- path, _, _ = tag.short_path.rpartition("/")
- go.actions.write(tag, content="")
- outputs.append(tag)
- return path
-
def _go_path_impl(ctx):
- print("""
-EXPERIMENTAL: the go_path rule is still very experimental
-Please do not rely on it for production use, but feel free to use it and file issues
-""")
- go = go_context(ctx)
- #TODO: non specific mode?
- # First gather all the library rules
- golibs = depset()
- archives_runfiles = {}
+ # Gather all archives. Note that there may be multiple packages with the same
+ # importpath (e.g., multiple vendored libraries, internal tests).
+ direct_archives = []
+ transitive_archives = []
for dep in ctx.attr.deps:
archive = get_archive(dep)
- golibs += archive.transitive
- importpath = archive.source.library.importpath
- if importpath:
- archives_runfiles[importpath] = archive.source.runfiles
+ direct_archives.append(archive.data)
+ transitive_archives.append(archive.transitive)
+ archives = depset(direct = direct_archives, transitive = transitive_archives)
+
+ # Collect sources and data files from archives. Merge archives into packages.
+ pkg_map = {} # map from package path to structs
+ for archive in as_iterable(archives):
+ importpath, pkgpath = _get_importpath_pkgpath(archive)
+ if importpath == "":
+ continue # synthetic archive or inferred location
+ out_prefix = "src/" + pkgpath
+ pkg = struct(
+ importpath = importpath,
+ dir = out_prefix,
+ srcs = as_list(archive.orig_srcs),
+ data = as_list(archive.data_files),
+ )
+ if pkgpath in pkg_map:
+ _merge_pkg(pkg_map[pkgpath], pkg)
+ else:
+ pkg_map[pkgpath] = pkg
+
+ # Build a manifest file that includes all files to copy/link/zip.
+ inputs = []
+ manifest_entries = []
+ for pkg in pkg_map.values():
+ for f in pkg.srcs + pkg.data:
+ manifest_entries.append(struct(
+ src = f.path,
+ dst = pkg.dir + "/" + f.basename,
+ ))
+ inputs.append(f)
+ for f in ctx.files.data:
+ manifest_entries.append(struct(
+ src = f.path,
+ dst = f.basename,
+ ))
+ inputs.append(f)
+ manifest_file = ctx.actions.declare_file(ctx.label.name + "~manifest")
+ manifest_entries_json = [e.to_json() for e in manifest_entries]
+ manifest_content = "[\n " + ",\n ".join(manifest_entries_json) + "\n]"
+ ctx.actions.write(manifest_file, manifest_content)
+ inputs.append(manifest_file)
+
+ # Execute the builder
+ if ctx.attr.mode == "archive":
+ out = ctx.actions.declare_file(ctx.label.name + ".zip")
+ out_path = out.path
+ out_short_path = out.short_path
+ outputs = [out]
+ elif ctx.attr.mode == "copy":
+ out = ctx.actions.declare_directory(ctx.label.name)
+ out_path = out.path
+ out_short_path = out.short_path
+ outputs = [out]
+ else: # link
+ # Declare individual outputs in link mode. Symlinks can't point outside
+ # tree artifacts.
+ outputs = [ctx.actions.declare_file(ctx.label.name + "/" + e.dst)
+ for e in manifest_entries]
+ tag = ctx.actions.declare_file(ctx.label.name + "/.tag")
+ ctx.actions.write(tag, "")
+ out_path = tag.dirname
+ out_short_path = tag.short_path.rpartition("/")[0]
+ args = [
+ "-manifest=" + manifest_file.path,
+ "-out=" + out_path,
+ "-mode=" + ctx.attr.mode,
+ ]
+ ctx.actions.run(
+ outputs = outputs,
+ inputs = inputs,
+ mnemonic = "GoPath",
+ executable = ctx.executable._go_path,
+ arguments = args,
+ )
- # Now scan them for sources
- seen_libs = {}
- seen_paths = {}
- outputs = []
- packages = []
- for golib in as_iterable(golibs):
- if not golib.importpath:
- print("Missing importpath on {}".format(golib.label))
- continue
- if golib.importpath in seen_libs:
- # We found two different library rules that map to the same import path
- # This is legal in bazel, but we can't build a valid go path for it.
- # TODO: we might be able to ignore this if the content is identical
- print("""Duplicate package
-Found {} in
- {}
- {}
-""".format(golib.importpath, golib.label, seen_libs[golib.importpath].label))
- # for now we don't fail if we see duplicate packages
- # the most common case is the same source from two different workspaces
- continue
- seen_libs[golib.importpath] = golib
- package_files = []
- prefix = "src/" + golib.importpath + "/"
- golib_files = golib.srcs
- if golib.importpath in archives_runfiles:
- golib_files = list(golib.srcs) + as_iterable(archives_runfiles[golib.importpath].files)
- for src in golib_files:
- outpath = prefix + src.basename
- if outpath in seen_paths:
- # If we see the same path twice, it's a fatal error
- fail("Duplicate path {}".format(outpath))
- seen_paths[outpath] = True
- out = go.declare_file(go, path=outpath)
- package_files += [out]
- outputs += [out]
- if ctx.attr.mode == "copy":
- ctx.actions.expand_template(template=src, output=out, substitutions={})
- elif ctx.attr.mode == "link":
- ctx.actions.run_shell(
- command='ln -s $(readlink "$1") "$2"',
- arguments=[src.path, out.path],
- mnemonic = "GoLn",
- inputs=[src],
- outputs=[out],
- )
- else:
- fail("Invalid go path mode '{}'".format(ctx.attr.mode))
- packages += [struct(
- golib = golib,
- dir = _tag(go, prefix, outputs),
- files = package_files,
- )]
- gopath = _tag(go, "", outputs)
return [
DefaultInfo(
files = depset(outputs),
+ runfiles = ctx.runfiles(files = outputs),
),
GoPath(
- gopath = gopath,
- packages = packages,
- srcs = outputs,
- )
+ gopath = out_short_path,
+ packages = pkg_map.values(),
+ ),
]
-go_path = go_rule(
+go_path = rule(
_go_path_impl,
attrs = {
- "deps": attr.label_list(providers = [GoLibrary]),
+ "deps": attr.label_list(providers = [GoArchive]),
+ "data": attr.label_list(
+ allow_files = True,
+ cfg = "data",
+ ),
"mode": attr.string(
default = "copy",
values = [
- "link",
+ "archive",
"copy",
+ "link",
],
),
+ "_go_path": attr.label(
+ default = "@io_bazel_rules_go//go/tools/builders:go_path",
+ executable = True,
+ cfg = "host",
+ ),
},
)
+
+def _get_importpath_pkgpath(archive):
+ if archive.pathtype != EXPLICIT_PATH:
+ return "", ""
+ importpath = archive.importpath
+ importmap = archive.importmap
+ if importpath.endswith("_test"): importpath = importpath[:-len("_test")]
+ if importmap.endswith("_test"): importmap = importmap[:-len("_test")]
+ parts = importmap.split("/")
+ if "vendor" not in parts:
+ # Unusual case not handled by go build. Just return importpath.
+ return importpath, importpath
+ elif len(parts) > 2 and archive.label.workspace_root == "external/" + parts[0]:
+ # Common case for importmap set by Gazelle in external repos.
+ return importpath, importmap[len(parts[0]):]
+ else:
+ # Vendor directory somewhere in the main repo. Leave it alone.
+ return importpath, importmap
+
+def _merge_pkg(x, y):
+ x_srcs = {f.path: None for f in x.srcs}
+ x_data = {f.path: None for f in x.data}
+ x.srcs.extend([f for f in y.srcs if f.path not in x_srcs])
+ x.data.extend([f for f in y.data if f.path not in x_srcs])
+
+
diff --git a/go/private/tools/vet.bzl b/go/private/tools/vet.bzl
index 9c83a193..08999e4d 100644
--- a/go/private/tools/vet.bzl
+++ b/go/private/tools/vet.bzl
@@ -39,7 +39,7 @@ Please do not rely on it for production use, but feel free to use it and file is
for data in ctx.attr.data:
entry = data[GoPath]
gopath += [entry.gopath]
- packages += [package.dir for package in entry.packages]
+ packages += [entry.gopath + "/" + package.dir for package in entry.packages]
ctx.actions.write(output=script_file, is_executable=True, content="""
export GOPATH="{gopath}"
{go} tool vet {packages}
@@ -75,4 +75,4 @@ def go_vet_test(name, data, **kwargs):
srcs=[script_name],
data=data,
**kwargs
- ) \ No newline at end of file
+ )
diff --git a/go/providers.rst b/go/providers.rst
index 1939539a..3114a314 100644
--- a/go/providers.rst
+++ b/go/providers.rst
@@ -6,6 +6,7 @@ Go providers
.. _go_library: core.rst#go_library
.. _go_binary: core.rst#go_binary
.. _go_test: core.rst#go_test
+.. _go_path: core.rst#go_path
.. _cc_library: https://docs.bazel.build/versions/master/be/c-cpp.html#cc_library
.. _flatbuffers: http://google.github.io/flatbuffers/
.. _static linking: modes.rst#building-static-binaries
@@ -272,3 +273,31 @@ This is used when compiling and linking dependant libraries or binaries.
+--------------------------------+-----------------------------------------------------------------+
| The files needed to run anything that includes this library. |
+--------------------------------+-----------------------------------------------------------------+
+
+GoPath
+~~~~~~
+
+GoPath is produced by the `go_path`_ rule. It gives a list of packages used to
+build the ``go_path`` directory and provides a list of original files for
+each package.
+
++--------------------------------+-----------------------------------------------------------------+
+| **Name** | **Type** |
++--------------------------------+-----------------------------------------------------------------+
+| :param:`gopath` | :type:`string` |
++--------------------------------+-----------------------------------------------------------------+
+| The short path to the output file or directory. Useful for constructing |
+| ``runfiles`` paths. |
++--------------------------------+-----------------------------------------------------------------+
+| :param:`packages` | :type:`list of struct` |
++--------------------------------+-----------------------------------------------------------------+
+| A list of structs representing packages used to build the ``go_path`` |
+| directory. Each struct has the following fields: |
+| |
+| * ``importpath``: the import path of the package. |
+| * ``dir``: the subdirectory of the package within the ``go_path``, including |
+| the ``src/`` prefix. May different from ``importpath`` due to vendoring. |
+| * ``srcs``: list of source ``File``s. |
+| * ``data``: list of data ``File``s. |
++--------------------------------+-----------------------------------------------------------------+
+
diff --git a/go/tools/builders/BUILD.bazel b/go/tools/builders/BUILD.bazel
index 634dda2b..553019c5 100644
--- a/go/tools/builders/BUILD.bazel
+++ b/go/tools/builders/BUILD.bazel
@@ -69,6 +69,12 @@ go_tool_binary(
)
go_tool_binary(
+ name = "go_path",
+ srcs = ["go_path.go"],
+ visibility = ["//visibility:public"],
+)
+
+go_tool_binary(
name = "info",
srcs = [
"env.go",
diff --git a/go/tools/builders/go_path.go b/go/tools/builders/go_path.go
new file mode 100644
index 00000000..53261db8
--- /dev/null
+++ b/go/tools/builders/go_path.go
@@ -0,0 +1,202 @@
+// 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 main
+
+import (
+ "archive/zip"
+ "encoding/json"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "os"
+ "path/filepath"
+)
+
+type mode int
+
+const (
+ invalidMode mode = iota
+ archiveMode
+ copyMode
+ linkMode
+)
+
+func modeFromString(s string) (mode, error) {
+ switch s {
+ case "archive":
+ return archiveMode, nil
+ case "copy":
+ return copyMode, nil
+ case "link":
+ return linkMode, nil
+ default:
+ return invalidMode, fmt.Errorf("invalid mode: %s", s)
+ }
+}
+
+type manifestEntry struct {
+ Src, Dst string
+}
+
+func main() {
+ log.SetFlags(0)
+ if err := run(os.Args[1:]); err != nil {
+ log.Fatal(err)
+ }
+}
+
+func run(args []string) error {
+ var manifest, out string
+ flags := flag.NewFlagSet("go_path", flag.ContinueOnError)
+ flags.StringVar(&manifest, "manifest", "", "name of json file listing files to include")
+ flags.StringVar(&out, "out", "", "output file or directory")
+ modeFlag := flags.String("mode", "", "copy, link, or archive")
+ if err := flags.Parse(args); err != nil {
+ return err
+ }
+ if manifest == "" {
+ return errors.New("-manifest not set")
+ }
+ if out == "" {
+ return errors.New("-out not set")
+ }
+ if *modeFlag == "" {
+ return errors.New("-mode not set")
+ }
+ mode, err := modeFromString(*modeFlag)
+ if err != nil {
+ return err
+ }
+
+ entries, err := readManifest(manifest)
+ if err != nil {
+ return err
+ }
+
+ switch mode {
+ case archiveMode:
+ err = archivePath(out, entries)
+ case copyMode:
+ err = copyPath(out, entries)
+ case linkMode:
+ err = linkPath(out, entries)
+ }
+ return err
+}
+
+func readManifest(path string) ([]manifestEntry, error) {
+ data, err := ioutil.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("error reading manifest: %v", err)
+ }
+ var entries []manifestEntry
+ if err := json.Unmarshal(data, &entries); err != nil {
+ return nil, fmt.Errorf("error unmarshalling manifest %s: %v", path, err)
+ }
+ return entries, nil
+}
+
+func archivePath(out string, manifest []manifestEntry) (err error) {
+ outFile, err := os.Create(out)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if e := outFile.Close(); err == nil && e != nil {
+ err = fmt.Errorf("error closing archive %s: %v", out, e)
+ }
+ }()
+ outZip := zip.NewWriter(outFile)
+
+ for _, entry := range manifest {
+ srcFile, err := os.Open(entry.Src)
+ if err != nil {
+ return err
+ }
+ w, err := outZip.Create(entry.Dst)
+ if err != nil {
+ srcFile.Close()
+ return err
+ }
+ if _, err := io.Copy(w, srcFile); err != nil {
+ srcFile.Close()
+ return err
+ }
+ if err := srcFile.Close(); err != nil {
+ return err
+ }
+ }
+
+ if err := outZip.Close(); err != nil {
+ return fmt.Errorf("error constructing archive %s: %v", out, err)
+ }
+ return nil
+}
+
+func copyPath(out string, manifest []manifestEntry) error {
+ if err := os.MkdirAll(out, 0777); err != nil {
+ return err
+ }
+ for _, entry := range manifest {
+ dst := filepath.Join(out, filepath.FromSlash(entry.Dst))
+ if err := os.MkdirAll(filepath.Dir(dst), 0777); err != nil {
+ return err
+ }
+ srcFile, err := os.Open(entry.Src)
+ if err != nil {
+ return err
+ }
+ dstFile, err := os.Create(dst)
+ if err != nil {
+ srcFile.Close()
+ return err
+ }
+ if _, err := io.Copy(dstFile, srcFile); err != nil {
+ dstFile.Close()
+ srcFile.Close()
+ return err
+ }
+ srcFile.Close()
+ if err := dstFile.Close(); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func linkPath(out string, manifest []manifestEntry) error {
+ // out directory may already exist and may contain old symlinks. Delete.
+ if err := os.RemoveAll(out); err != nil {
+ return err
+ }
+ if err := os.MkdirAll(out, 0777); err != nil {
+ return err
+ }
+ for _, entry := range manifest {
+ dst := filepath.Join(out, filepath.FromSlash(entry.Dst))
+ dstDir := filepath.Dir(dst)
+ src, _ := filepath.Rel(dstDir, entry.Src)
+ if err := os.MkdirAll(dstDir, 0777); err != nil {
+ return err
+ }
+ if err := os.Symlink(src, dst); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/tests/README.rst b/tests/README.rst
index b39d9432..707d9526 100644
--- a/tests/README.rst
+++ b/tests/README.rst
@@ -6,10 +6,10 @@ Main test areas
.. Child list start
-* `Go rules examples <examples/README.rst>`_
* `Core Go rules tests <core/README.rst>`_
* `Integration tests <integration/README.rst>`_
* `Legacy tests <legacy/README.rst>`_
+* `Go rules examples <examples/README.rst>`_
.. Child list end
diff --git a/tests/core/README.rst b/tests/core/README.rst
index 003a00a6..6e25c9fb 100644
--- a/tests/core/README.rst
+++ b/tests/core/README.rst
@@ -8,11 +8,13 @@ Contents
.. Child list start
+* `go_proto_library importmap <go_proto_library_importmap/README.rst>`_
* `Cross compilation <cross/README.rst>`_
* `Basic go_proto_library functionality <go_proto_library/README.rst>`_
-* `Import maps <importmap/README.rst>`_
+* `c-archive / c-shared linkmodes <c_linkmodes/README.rst>`_
* `Basic go_test functionality <go_test/README.rst>`_
-* `go_proto_library importmap <go_proto_library_importmap/README.rst>`_
+* `Import maps <importmap/README.rst>`_
+* `Basic go_path functionality <go_path/README.rst>`_
.. Child list end
diff --git a/tests/core/go_path/BUILD.bazel b/tests/core/go_path/BUILD.bazel
new file mode 100644
index 00000000..ea2662d3
--- /dev/null
+++ b/tests/core/go_path/BUILD.bazel
@@ -0,0 +1,33 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_path", "go_test")
+
+test_suite(name = "go_path")
+
+[go_path(
+ name = mode + "_path",
+ deps = [
+ "//tests/core/go_path/cmd/bin",
+ "//tests/core/go_path/pkg/lib:go_default_library",
+ "//tests/core/go_path/pkg/lib:embed_test",
+ "//tests/core/go_path/pkg/lib:go_default_test",
+ "//tests/core/go_path/pkg/lib:vendored",
+ ],
+ data = ["extra.txt"],
+ mode = mode,
+ testonly = True,
+) for mode in ("archive", "copy", "link")]
+
+go_test(
+ name = "go_path_test",
+ srcs = ["go_path_test.go"],
+ args = [
+ "-archive_path=$(location :archive_path)",
+ "-copy_path=$(location :copy_path)",
+ "-link_path=tests/core/go_path/link_path", # can't use location; not a single file
+ ],
+ data = [
+ ":archive_path",
+ ":copy_path",
+ ":link_path",
+ ],
+ rundir = ".",
+)
diff --git a/tests/core/go_path/README.rst b/tests/core/go_path/README.rst
new file mode 100644
index 00000000..a9c676a9
--- /dev/null
+++ b/tests/core/go_path/README.rst
@@ -0,0 +1,12 @@
+Basic go_path functionality
+===========================
+
+.. _go_path: /go/core.rst#_go_path
+
+Tests to ensure the basic features of `go_path`_ are working as expected.
+
+go_path_test
+------------
+
+Consumes `go_path`_ rules built for the same set of packages in archive, copy,
+and link modes and verifies that expected files are present in each mode.
diff --git a/tests/core/go_path/cmd/bin/BUILD.bazel b/tests/core/go_path/cmd/bin/BUILD.bazel
new file mode 100644
index 00000000..02bf1725
--- /dev/null
+++ b/tests/core/go_path/cmd/bin/BUILD.bazel
@@ -0,0 +1,14 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = ["bin.go"],
+ importpath = "example.com/repo/cmd/bin",
+ visibility = ["//visibility:public"],
+)
+
+go_binary(
+ name = "bin",
+ embed = [":go_default_library"],
+ visibility = ["//visibility:public"],
+)
diff --git a/tests/core/go_path/cmd/bin/bin.go b/tests/core/go_path/cmd/bin/bin.go
new file mode 100644
index 00000000..da29a2ca
--- /dev/null
+++ b/tests/core/go_path/cmd/bin/bin.go
@@ -0,0 +1,4 @@
+package main
+
+func main() {
+}
diff --git a/tests/core/go_path/extra.txt b/tests/core/go_path/extra.txt
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tests/core/go_path/extra.txt
diff --git a/tests/core/go_path/go_path_test.go b/tests/core/go_path/go_path_test.go
new file mode 100644
index 00000000..943c1ff2
--- /dev/null
+++ b/tests/core/go_path/go_path_test.go
@@ -0,0 +1,147 @@
+/* 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 go_path
+
+import (
+ "archive/zip"
+ "flag"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+var copyPath, linkPath, archivePath string
+
+var files = []string{
+ "extra.txt",
+ "src/",
+ "-src/example.com/repo/cmd/bin/bin",
+ "-src/testmain/testmain.go",
+ "src/example.com/repo/cmd/bin/bin.go",
+ "src/example.com/repo/pkg/lib/lib.go",
+ "src/example.com/repo/pkg/lib/embed_test.go",
+ // TODO: how about go_test without embed?
+ // "src/example.com/repo/pkg/lib/internal_test.go",
+ // "src/example.com/repo/pkg/lib/external_test.go",
+ "-src/example.com/repo/pkg/lib_test/embed_test.go",
+ "src/example.com/repo/pkg/lib/data.txt",
+ "src/example.com/repo/vendor/example.com/repo2/vendored.go",
+}
+
+func TestMain(m *testing.M) {
+ flag.StringVar(&copyPath, "copy_path", "", "path to copied go_path")
+ flag.StringVar(&linkPath, "link_path", "", "path to symlinked go_path")
+ flag.StringVar(&archivePath, "archive_path", "", "path to archive go_path")
+ flag.Parse()
+ os.Exit(m.Run())
+}
+
+func TestCopyPath(t *testing.T) {
+ if copyPath == "" {
+ t.Fatal("-copy_path not set")
+ }
+ checkPath(t, copyPath, files, os.FileMode(0))
+}
+
+func TestLinkPath(t *testing.T) {
+ if linkPath == "" {
+ t.Fatal("-link_path not set")
+ }
+ checkPath(t, linkPath, files, os.ModeSymlink)
+}
+
+func TestArchivePath(t *testing.T) {
+ if archivePath == "" {
+ t.Fatal("-archive_path not set")
+ }
+ dir, err := ioutil.TempDir(os.Getenv("TEST_TEMPDIR"), "TestArchivePath")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(dir)
+
+ z, err := zip.OpenReader(archivePath)
+ if err != nil {
+ t.Fatalf("error opening zip: %v", err)
+ }
+ defer z.Close()
+ for _, f := range z.File {
+ r, err := f.Open()
+ if err != nil {
+ t.Fatalf("error reading file %s: %v", f.Name, err)
+ }
+ dstPath := filepath.Join(dir, filepath.FromSlash(f.Name))
+ if err := os.MkdirAll(filepath.Dir(dstPath), 0777); err != nil {
+ t.Fatalf("error creating directory %s: %v", filepath.Dir(dstPath), err)
+ }
+ w, err := os.Create(dstPath)
+ if err != nil {
+ t.Fatalf("error creating file %s: %v", dstPath, err)
+ }
+ if _, err := io.Copy(w, r); err != nil {
+ w.Close()
+ t.Fatalf("error writing file %s: %v", dstPath, err)
+ }
+ if err := w.Close(); err != nil {
+ t.Fatalf("error closing file %s: %v", dstPath, err)
+ }
+ }
+
+ checkPath(t, dir, files, os.FileMode(0))
+}
+
+// checkPath checks that dir contains a list of files. files is a list of
+// slash-separated paths relative to dir. Files that start with "-" should be
+// absent. Files that end with "/" should be directories. Other files should
+// be of fileType.
+func checkPath(t *testing.T, dir string, files []string, fileType os.FileMode) {
+ for _, f := range files {
+ wantType := fileType
+ wantAbsent := false
+ if strings.HasPrefix(f, "-") {
+ f = f[1:]
+ wantAbsent = true
+ }
+ if strings.HasSuffix(f, "/") {
+ wantType = os.ModeDir
+ }
+ path := filepath.Join(dir, filepath.FromSlash(f))
+ st, err := os.Lstat(path)
+ if wantAbsent {
+ if err == nil {
+ t.Errorf("found %s: should not be present", path)
+ } else if !os.IsNotExist(err) {
+ t.Error(err)
+ }
+ } else {
+ if err != nil {
+ if os.IsNotExist(err) {
+ t.Errorf("%s is missing", path)
+ } else {
+ t.Error(err)
+ }
+ continue
+ }
+ gotType := st.Mode() & os.ModeType
+ if gotType != wantType {
+ t.Errorf("%s: got type %s; want type %s .. %s", path, gotType, wantType, st.Mode())
+ }
+ }
+ }
+}
diff --git a/tests/core/go_path/pkg/lib/BUILD.bazel b/tests/core/go_path/pkg/lib/BUILD.bazel
new file mode 100644
index 00000000..820ed336
--- /dev/null
+++ b/tests/core/go_path/pkg/lib/BUILD.bazel
@@ -0,0 +1,40 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+ name = "go_default_library",
+ srcs = ["lib.go"],
+ cgo = True,
+ data = ["data.txt"],
+ importpath = "example.com/repo/pkg/lib",
+ visibility = ["//visibility:public"],
+)
+
+go_test(
+ name = "go_default_test",
+ srcs = [
+ "external_test.go",
+ "internal_test.go",
+ ],
+ visibility = ["//visibility:public"],
+)
+
+go_test(
+ name = "embed_test",
+ embed = [":embed_lib"],
+ visibility = ["//visibility:public"],
+)
+
+go_library(
+ name = "embed_lib",
+ srcs = ["embed_test.go"],
+ importpath = "example.com/repo/pkg/lib",
+ visibility = ["//visibility:public"],
+)
+
+go_library(
+ name = "vendored",
+ srcs = ["vendored.go"],
+ importpath = "example.com/repo2",
+ importmap = "example.com/repo/vendor/example.com/repo2",
+ visibility = ["//visibility:public"],
+)
diff --git a/tests/core/go_path/pkg/lib/data.txt b/tests/core/go_path/pkg/lib/data.txt
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tests/core/go_path/pkg/lib/data.txt
diff --git a/tests/core/go_path/pkg/lib/embed_test.go b/tests/core/go_path/pkg/lib/embed_test.go
new file mode 100644
index 00000000..55c21f80
--- /dev/null
+++ b/tests/core/go_path/pkg/lib/embed_test.go
@@ -0,0 +1 @@
+package lib
diff --git a/tests/core/go_path/pkg/lib/external_test.go b/tests/core/go_path/pkg/lib/external_test.go
new file mode 100644
index 00000000..df64f213
--- /dev/null
+++ b/tests/core/go_path/pkg/lib/external_test.go
@@ -0,0 +1 @@
+package lib_test
diff --git a/tests/core/go_path/pkg/lib/internal_test.go b/tests/core/go_path/pkg/lib/internal_test.go
new file mode 100644
index 00000000..55c21f80
--- /dev/null
+++ b/tests/core/go_path/pkg/lib/internal_test.go
@@ -0,0 +1 @@
+package lib
diff --git a/tests/core/go_path/pkg/lib/lib.go b/tests/core/go_path/pkg/lib/lib.go
new file mode 100644
index 00000000..592be3ae
--- /dev/null
+++ b/tests/core/go_path/pkg/lib/lib.go
@@ -0,0 +1,3 @@
+package lib
+
+import "C"
diff --git a/tests/core/go_path/pkg/lib/vendored.go b/tests/core/go_path/pkg/lib/vendored.go
new file mode 100644
index 00000000..2355022f
--- /dev/null
+++ b/tests/core/go_path/pkg/lib/vendored.go
@@ -0,0 +1 @@
+package vendored