aboutsummaryrefslogtreecommitdiff
path: root/go/private/tools/path.bzl
blob: f23677a2ea9d7bc953b944c34f371bf97874f6ab (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# 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.

load(
    "//go/private:providers.bzl",
    "GoArchive",
    "GoPath",
    "effective_importpath_pkgpath",
    "get_archive",
)
load(
    "//go/private:common.bzl",
    "as_iterable",
    "as_list",
)
load(
    "@bazel_skylib//lib:paths.bzl",
    "paths",
)

def _go_path_impl(ctx):
    # Gather all archives. Note that there may be multiple packages with the same
    # importpath (e.g., multiple vendored libraries, internal tests). The same
    # package may also appear in different modes.
    mode_to_deps = {}
    for dep in ctx.attr.deps:
        archive = get_archive(dep)
        if archive.mode not in mode_to_deps:
            mode_to_deps[archive.mode] = []
        mode_to_deps[archive.mode].append(archive)
    mode_to_archive = {}
    for mode, archives in mode_to_deps.items():
        direct = [a.data for a in archives]
        transitive = []
        if ctx.attr.include_transitive:
            transitive = [a.transitive for a in archives]
        mode_to_archive[mode] = depset(direct = direct, transitive = transitive)

    # Collect sources and data files from archives. Merge archives into packages.
    pkg_map = {}  # map from package path to structs
    for mode, archives in mode_to_archive.items():
        for archive in as_iterable(archives):
            importpath, pkgpath = effective_importpath_pkgpath(archive)
            if importpath == "":
                continue  # synthetic archive or inferred location
            pkg = struct(
                importpath = importpath,
                dir = "src/" + pkgpath,
                srcs = as_list(archive.orig_srcs),
                data = as_list(archive.data_files),
                embedsrcs = as_list(archive._embedsrcs),
                pkgs = {mode: archive.file},
            )
            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 = []
    manifest_entry_map = {}
    for pkg in pkg_map.values():
        # src_dir is the path to the directory holding the source.
        # Paths to embedded sources will be relative to this path.
        src_dir = None

        for f in pkg.srcs:
            src_dir = f.dirname
            dst = pkg.dir + "/" + f.basename
            _add_manifest_entry(manifest_entries, manifest_entry_map, inputs, f, dst)
        for f in pkg.embedsrcs:
            if src_dir == None:
                fail("cannot relativize {}: src_dir is unset".format(f.path))
            embedpath = paths.relativize(f.path, f.root.path)
            dst = pkg.dir + "/" + paths.relativize(embedpath.lstrip(ctx.bin_dir.path + "/"), src_dir.lstrip(ctx.bin_dir.path + "/"))
            _add_manifest_entry(manifest_entries, manifest_entry_map, inputs, f, dst)
    if ctx.attr.include_pkg:
        for pkg in pkg_map.values():
            for mode, f in pkg.pkgs.items():
                # TODO(jayconrod): include other mode attributes, e.g., race.
                installsuffix = mode.goos + "_" + mode.goarch
                dst = "pkg/" + installsuffix + "/" + pkg.dir[len("src/"):] + ".a"
                _add_manifest_entry(manifest_entries, manifest_entry_map, inputs, f, dst)
    if ctx.attr.include_data:
        for pkg in pkg_map.values():
            for f in pkg.data:
                parts = f.path.split("/")
                if "testdata" in parts:
                    i = parts.index("testdata")
                    dst = pkg.dir + "/" + "/".join(parts[i:])
                else:
                    dst = pkg.dir + "/" + f.basename
                _add_manifest_entry(manifest_entries, manifest_entry_map, inputs, f, dst)
    for f in ctx.files.data:
        _add_manifest_entry(
            manifest_entries,
            manifest_entry_map,
            inputs,
            f,
            f.basename,
        )
    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]
        out_file = 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]
        out_file = 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]
        out_file = tag
    args = ctx.actions.args()
    args.add("-manifest", manifest_file)
    args.add("-out", out_path)
    args.add("-mode", ctx.attr.mode)
    ctx.actions.run(
        outputs = outputs,
        inputs = inputs,
        mnemonic = "GoPath",
        executable = ctx.executable._go_path,
        arguments = [args],
    )

    return [
        DefaultInfo(
            files = depset(outputs),
            runfiles = ctx.runfiles(files = outputs),
        ),
        GoPath(
            gopath = out_short_path,
            gopath_file = out_file,
            packages = pkg_map.values(),
        ),
    ]

go_path = rule(
    _go_path_impl,
    attrs = {
        "deps": attr.label_list(
            providers = [GoArchive],
            doc = """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.
            """,
        ),
        "data": attr.label_list(
            allow_files = True,
            doc = """
            A list of targets producing data files that will be stored next to the
            `src/` directory. Useful for including things like licenses and readmes.
            """,
        ),
        "mode": attr.string(
            default = "copy",
            values = [
                "archive",
                "copy",
                "link",
            ],
            doc = """
            Determines how the generated directory is provided. May be one of:
            <ul>
                <li><code>"archive"</code>: The generated directory is packaged as a single .zip file.</li>
                <li><code>"copy"</code>: The generated directory is a single tree artifact. Source files
                are copied into the tree.</li>
                <li><code>"link"</code>: <b>Unmaintained due to correctness issues</b>. Source files
                are symlinked into the tree. All of the symlink files are provided as separate output
                files.</li>
            </ul>

            ***Note:*** In <code>"copy"</code> mode, when a <code>GoPath</code> is consumed as a set of input
            files or run files, Bazel may provide symbolic links instead of regular files.
            Any program that consumes these files should dereference links, e.g., if you
            run <code>tar</code>, use the <code>--dereference</code> flag.
            """,
        ),
        "include_data": attr.bool(
            default = True,
            doc = """
            When true, data files referenced by libraries, binaries, and tests will be
            included in the output directory. Files listed in the `data` attribute
            for this rule will be included regardless of this attribute.
            """,
        ),
        "include_pkg": attr.bool(
            default = False,
            doc = """
            When true, a `pkg` subdirectory containing the compiled libraries will be created in the
            generated `GOPATH` containing compiled libraries.
            """,
        ),
        "include_transitive": attr.bool(
            default = True,
            doc = """
            When true, the transitive dependency graph will be included in the generated `GOPATH`. This is
            the default behaviour. When false, only the direct dependencies will be included in the
            generated `GOPATH`.
            """,
        ),
        "_go_path": attr.label(
            default = "//go/tools/builders:go_path",
            executable = True,
            cfg = "exec",
        ),
    },
    doc = """`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.
    """,
)

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_embedsrcs = {f.path: None for f in x.embedsrcs}
    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_data])
    x.embedsrcs.extend([f for f in y.embedsrcs if f.path not in x_embedsrcs])
    x.pkgs.update(y.pkgs)

def _add_manifest_entry(entries, entry_map, inputs, src, dst):
    if dst in entry_map:
        if entry_map[dst] != src.path:
            fail("{}: references multiple files ({} and {})".format(dst, entry_map[dst], src.path))
        return
    entries.append(struct(src = src.path, dst = dst))
    entry_map[dst] = src.path
    inputs.append(src)