aboutsummaryrefslogtreecommitdiff
path: root/go/private/actions/link.bzl
blob: e919e3db920adadb1778dc9a56f6844d2f5d756e (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
# 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:common.bzl",
    "as_set",
    "count_group_matches",
    "has_shared_lib_extension",
)
load(
    "//go/private:mode.bzl",
    "LINKMODE_NORMAL",
    "LINKMODE_PLUGIN",
    "extld_from_cc_toolchain",
    "extldflags_from_cc_toolchain",
)
load(
    "//go/private:rpath.bzl",
    "rpath",
)
load(
    "@bazel_skylib//lib:collections.bzl",
    "collections",
)

def _format_archive(d):
    return "{}={}={}".format(d.label, d.importmap, d.file.path)

def _transitive_archives_without_test_archives(archive, test_archives):
    # Build the set of transitive dependencies. Currently, we tolerate multiple
    # archives with the same importmap (though this will be an error in the
    # future), but there is a special case which is difficult to avoid:
    # If a go_test has internal and external archives, and the external test
    # transitively depends on the library under test, we need to exclude the
    # library under test and use the internal test archive instead.
    deps = depset(transitive = [d.transitive for d in archive.direct])
    result = {}

    # Unfortunately, Starlark doesn't support set()
    test_imports = {}
    for t in test_archives:
        test_imports[t.importmap] = True
    for d in deps.to_list():
        if d.importmap in test_imports:
            continue
        if d.importmap in result:
            print("Multiple copies of {} passed to the linker. Ignoring {} in favor of {}".format(d.importmap, d.file.path, result[d.importmap].file.path))
            continue
        result[d.importmap] = d
    return result.values()

def emit_link(
        go,
        archive = None,
        test_archives = [],
        executable = None,
        gc_linkopts = [],
        version_file = None,
        info_file = None):
    """See go/toolchains.rst#link for full documentation."""

    if archive == None:
        fail("archive is a required parameter")
    if executable == None:
        fail("executable is a required parameter")

    # Exclude -lstdc++ from link options. We don't want to link against it
    # unless we actually have some C++ code. _cgo_codegen will include it
    # in archives via CGO_LDFLAGS if it's needed.
    extldflags = [f for f in extldflags_from_cc_toolchain(go) if f not in ("-lstdc++", "-lc++")]

    if go.coverage_enabled:
        extldflags.append("--coverage")
    gc_linkopts = list(gc_linkopts)
    gc_linkopts.extend(go.mode.gc_linkopts)
    gc_linkopts, extldflags = _extract_extldflags(gc_linkopts, extldflags)
    builder_args = go.builder_args(go, "link")
    tool_args = go.tool_args(go)

    # Add in any mode specific behaviours
    if go.mode.race:
        tool_args.add("-race")
    if go.mode.msan:
        tool_args.add("-msan")

    if go.mode.pure:
        tool_args.add("-linkmode", "internal")
    else:
        extld = extld_from_cc_toolchain(go)
        tool_args.add_all(extld)
        if extld and (go.mode.static or
                      go.mode.race or
                      go.mode.link != LINKMODE_NORMAL or
                      go.mode.goos == "windows" and go.mode.msan):
            # Force external linking for the following conditions:
            # * Mode is static but not pure: -static must be passed to the C
            #   linker if the binary contains cgo code. See #2168, #2216.
            # * Non-normal build mode: may not be strictly necessary, especially
            #   for modes like "pie".
            # * Race or msan build for Windows: Go linker has pairwise
            #   incompatibilities with mingw, and we get link errors in race mode.
            #   Using the C linker avoids that. Race and msan always require a
            #   a C toolchain. See #2614.
            # * Linux race builds: we get linker errors during build with Go's
            #   internal linker. For example, when using zig cc v0.10
            #   (clang-15.0.3):
            #
            #       runtime/cgo(.text): relocation target memset not defined
            tool_args.add("-linkmode", "external")

    if go.mode.static:
        extldflags.append("-static")
    if go.mode.link != LINKMODE_NORMAL:
        builder_args.add("-buildmode", go.mode.link)
    if go.mode.link == LINKMODE_PLUGIN:
        tool_args.add("-pluginpath", archive.data.importpath)

    arcs = _transitive_archives_without_test_archives(archive, test_archives)
    arcs.extend(test_archives)
    if (go.coverage_enabled and go.coverdata and
        not any([arc.importmap == go.coverdata.data.importmap for arc in arcs])):
        arcs.append(go.coverdata.data)
    builder_args.add_all(arcs, before_each = "-arc", map_each = _format_archive)
    builder_args.add("-package_list", go.package_list)

    # Build a list of rpaths for dynamic libraries we need to find.
    # rpaths are relative paths from the binary to directories where libraries
    # are stored. Binaries that require these will only work when installed in
    # the bazel execroot. Most binaries are only dynamically linked against
    # system libraries though.
    cgo_rpaths = sorted(collections.uniq([
        f
        for d in archive.cgo_deps.to_list()
        if has_shared_lib_extension(d.basename)
        for f in rpath.flags(go, d, executable = executable)
    ]))
    extldflags.extend(cgo_rpaths)

    # Process x_defs, and record whether stamping is used.
    stamp_x_defs_volatile = False
    stamp_x_defs_stable = False
    for k, v in archive.x_defs.items():
        builder_args.add("-X", "%s=%s" % (k, v))
        if go.stamp:
            stable_vars_count = (count_group_matches(v, "{STABLE_", "}") +
                                 v.count("{BUILD_EMBED_LABEL}") +
                                 v.count("{BUILD_USER}") +
                                 v.count("{BUILD_HOST}"))
            if stable_vars_count > 0:
                stamp_x_defs_stable = True
            if count_group_matches(v, "{", "}") != stable_vars_count:
                stamp_x_defs_volatile = True

    # Stamping support
    stamp_inputs = []
    if stamp_x_defs_stable:
        stamp_inputs.append(info_file)
    if stamp_x_defs_volatile:
        stamp_inputs.append(version_file)
    if stamp_inputs:
        builder_args.add_all(stamp_inputs, before_each = "-stamp")

    builder_args.add("-o", executable)
    builder_args.add("-main", archive.data.file)
    builder_args.add("-p", archive.data.importmap)
    tool_args.add_all(gc_linkopts)
    tool_args.add_all(go.toolchain.flags.link)

    # Do not remove, somehow this is needed when building for darwin/arm only.
    tool_args.add("-buildid=redacted")
    if go.mode.strip:
        tool_args.add("-s", "-w")
    tool_args.add_joined("-extldflags", extldflags, join_with = " ")

    conflict_err = _check_conflicts(arcs)
    if conflict_err:
        # Report package conflict errors in execution instead of analysis.
        # We could call fail() with this message, but Bazel prints a stack
        # that doesn't give useful information.
        builder_args.add("-conflict_err", conflict_err)

    inputs_direct = stamp_inputs + [go.sdk.package_list]
    if go.coverage_enabled and go.coverdata:
        inputs_direct.append(go.coverdata.data.file)
    inputs_transitive = [
        archive.libs,
        archive.cgo_deps,
        as_set(go.crosstool),
        as_set(go.sdk.tools),
        as_set(go.stdlib.libs),
    ]
    inputs = depset(direct = inputs_direct, transitive = inputs_transitive)

    go.actions.run(
        inputs = inputs,
        outputs = [executable],
        mnemonic = "GoLink",
        executable = go.toolchain._builder,
        arguments = [builder_args, "--", tool_args],
        env = go.env,
    )

def _extract_extldflags(gc_linkopts, extldflags):
    """Extracts -extldflags from gc_linkopts and combines them into a single list.

    Args:
      gc_linkopts: a list of flags passed in through the gc_linkopts attributes.
        ctx.expand_make_variables should have already been applied. -extldflags
        may appear multiple times in this list.
      extldflags: a list of flags to be passed to the external linker.

    Return:
      A tuple containing the filtered gc_linkopts with external flags removed,
      and a combined list of external flags. Each string in the returned
      extldflags list may contain multiple flags, separated by whitespace.
    """
    filtered_gc_linkopts = []
    is_extldflags = False
    for opt in gc_linkopts:
        if is_extldflags:
            is_extldflags = False
            extldflags.append(opt)
        elif opt == "-extldflags":
            is_extldflags = True
        else:
            filtered_gc_linkopts.append(opt)
    return filtered_gc_linkopts, extldflags

def _check_conflicts(arcs):
    importmap_to_label = {}
    for arc in arcs:
        if arc.importmap in importmap_to_label:
            return """package conflict error: {}: multiple copies of package passed to linker:
	{}
	{}
Set "importmap" to different paths or use 'bazel cquery' to ensure only one
package with this path is linked.""".format(
                arc.importmap,
                importmap_to_label[arc.importmap],
                arc.label,
            )
        importmap_to_label[arc.importmap] = arc.label
    for arc in arcs:
        for dep_importmap, dep_label in zip(arc._dep_importmaps, arc._dep_labels):
            if dep_importmap not in importmap_to_label:
                return "package conflict error: {}: package needed by {} was not passed to linker".format(
                    dep_importmap,
                    arc.label,
                )
            if importmap_to_label[dep_importmap] != dep_label:
                err = """package conflict error: {}: package imports {}
	  was compiled with: {}
	but was linked with: {}""".format(
                    arc.importmap,
                    dep_importmap,
                    dep_label,
                    importmap_to_label[dep_importmap],
                )
                if importmap_to_label[dep_importmap].name.endswith("_test"):
                    err += """
This sometimes happens when an external test (package ending with _test)
imports a package that imports the library being tested. This is not supported."""
                err += "\nSee https://github.com/bazelbuild/rules_go/issues/1877."
                return err
    return None