aboutsummaryrefslogtreecommitdiff
path: root/go/private/rules/transition.bzl
blob: 4e87e30efe9d515a6a9687a67a3af09ed570d1d3 (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
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# Copyright 2020 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(
    "@bazel_skylib//lib:paths.bzl",
    "paths",
)
load(
    "//go/private:mode.bzl",
    "LINKMODES",
    "LINKMODE_NORMAL",
)
load(
    "//go/private:platforms.bzl",
    "CGO_GOOS_GOARCH",
    "GOOS_GOARCH",
)
load(
    "//go/private:providers.bzl",
    "GoArchive",
    "GoLibrary",
    "GoSource",
)

# A list of rules_go settings that are possibly set by go_transition.
# Keep their package name in sync with the implementation of
# _original_setting_key.
TRANSITIONED_GO_SETTING_KEYS = [
    "//go/config:static",
    "//go/config:msan",
    "//go/config:race",
    "//go/config:pure",
    "//go/config:linkmode",
    "//go/config:tags",
]

def _deduped_and_sorted(strs):
    return sorted({s: None for s in strs}.keys())

def _original_setting_key(key):
    if not "//go/config:" in key:
        fail("_original_setting_key currently assumes that all Go settings live under //go/config, got: " + key)
    name = key.split(":")[1]
    return "//go/private/rules:original_" + name

_SETTING_KEY_TO_ORIGINAL_SETTING_KEY = {
    setting: _original_setting_key(setting)
    for setting in TRANSITIONED_GO_SETTING_KEYS
}

def _go_transition_impl(settings, attr):
    # NOTE: Keep the list of rules_go settings set by this transition in sync
    # with POTENTIALLY_TRANSITIONED_SETTINGS.
    #
    # NOTE(bazelbuild/bazel#11409): Calling fail here for invalid combinations
    # of flags reports an error but does not stop the build.
    # In any case, get_mode should mainly be responsible for reporting
    # invalid modes, since it also takes --features flags into account.

    original_settings = settings
    settings = dict(settings)

    _set_ternary(settings, attr, "static")
    race = _set_ternary(settings, attr, "race")
    msan = _set_ternary(settings, attr, "msan")
    pure = _set_ternary(settings, attr, "pure")
    if race == "on":
        if pure == "on":
            fail('race = "on" cannot be set when pure = "on" is set. race requires cgo.')
        pure = "off"
        settings["//go/config:pure"] = False
    if msan == "on":
        if pure == "on":
            fail('msan = "on" cannot be set when msan = "on" is set. msan requires cgo.')
        pure = "off"
        settings["//go/config:pure"] = False
    if pure == "on":
        race = "off"
        settings["//go/config:race"] = False
        msan = "off"
        settings["//go/config:msan"] = False
    cgo = pure == "off"

    goos = getattr(attr, "goos", "auto")
    goarch = getattr(attr, "goarch", "auto")
    _check_ternary("pure", pure)
    if goos != "auto" or goarch != "auto":
        if goos == "auto":
            fail("goos must be set if goarch is set")
        if goarch == "auto":
            fail("goarch must be set if goos is set")
        if (goos, goarch) not in GOOS_GOARCH:
            fail("invalid goos, goarch pair: {}, {}".format(goos, goarch))
        if cgo and (goos, goarch) not in CGO_GOOS_GOARCH:
            fail('pure is "off" but cgo is not supported on {} {}'.format(goos, goarch))
        platform = "@io_bazel_rules_go//go/toolchain:{}_{}{}".format(goos, goarch, "_cgo" if cgo else "")
        settings["//command_line_option:platforms"] = platform

    tags = getattr(attr, "gotags", [])
    if tags:
        settings["//go/config:tags"] = _deduped_and_sorted(tags)

    linkmode = getattr(attr, "linkmode", "auto")
    if linkmode != "auto":
        if linkmode not in LINKMODES:
            fail("linkmode: invalid mode {}; want one of {}".format(linkmode, ", ".join(LINKMODES)))
        settings["//go/config:linkmode"] = linkmode

    for key, original_key in _SETTING_KEY_TO_ORIGINAL_SETTING_KEY.items():
        old_value = original_settings[key]
        value = settings[key]

        # If the outgoing configuration would differ from the incoming one in a
        # value, record the old value in the special original_* key so that the
        # real setting can be reset to this value before the new configuration
        # would cross a non-deps dependency edge.
        if value != old_value:
            # Encoding as JSON makes it possible to embed settings of arbitrary
            # types (currently bool, string and string_list) into a single type
            # of setting (string) with the information preserved whether the
            # original setting wasn't set explicitly (empty string) or was set
            # explicitly to its default  (always a non-empty string with JSON
            # encoding, e.g. "\"\"" or "[]").
            settings[original_key] = json.encode(old_value)
        else:
            settings[original_key] = ""

    return settings

def _request_nogo_transition(settings, _attr):
    """Indicates that we want the project configured nogo instead of a noop.

    This does not guarantee that the project configured nogo will be used (if
    bootstrap is true we are currently building nogo so that is a cyclic
    dependency).

    The config setting nogo_active requires bootstrap to be false and
    request_nogo to be true to provide the project configured nogo.
    """
    settings = dict(settings)
    settings["//go/private:request_nogo"] = True
    return settings

request_nogo_transition = transition(
    implementation = _request_nogo_transition,
    inputs = [],
    outputs = ["//go/private:request_nogo"],
)

go_transition = transition(
    implementation = _go_transition_impl,
    inputs = [
        "//command_line_option:platforms",
    ] + TRANSITIONED_GO_SETTING_KEYS,
    outputs = [
        "//command_line_option:platforms",
    ] + TRANSITIONED_GO_SETTING_KEYS + _SETTING_KEY_TO_ORIGINAL_SETTING_KEY.values(),
)

_common_reset_transition_dict = dict({
    "//go/private:request_nogo": False,
    "//go/config:static": False,
    "//go/config:msan": False,
    "//go/config:race": False,
    "//go/config:pure": False,
    "//go/config:debug": False,
    "//go/config:linkmode": LINKMODE_NORMAL,
    "//go/config:tags": [],
}, **{setting: "" for setting in _SETTING_KEY_TO_ORIGINAL_SETTING_KEY.values()})

_reset_transition_dict = dict(_common_reset_transition_dict, **{
    "//go/private:bootstrap_nogo": True,
})

_reset_transition_keys = sorted(_reset_transition_dict.keys())

_stdlib_keep_keys = sorted([
    "//go/config:msan",
    "//go/config:race",
    "//go/config:pure",
    "//go/config:linkmode",
    "//go/config:tags",
])

def _go_tool_transition_impl(settings, _attr):
    """Sets most Go settings to default values (use for external Go tools).

    go_tool_transition sets all of the //go/config settings to their default
    values and disables nogo. This is used for Go tool binaries like nogo
    itself. Tool binaries shouldn't depend on the link mode or tags of the
    target configuration and neither the tools nor the code they potentially
    generate should be subject to nogo's static analysis. This transition
    doesn't change the platform (goos, goarch), but tool binaries should also
    have `cfg = "exec"` so tool binaries should be built for the execution
    platform.
    """
    return dict(settings, **_reset_transition_dict)

go_tool_transition = transition(
    implementation = _go_tool_transition_impl,
    inputs = _reset_transition_keys,
    outputs = _reset_transition_keys,
)

def _non_go_tool_transition_impl(settings, _attr):
    """Sets all Go settings to default values (use for external non-Go tools).

    non_go_tool_transition sets all of the //go/config settings as well as the
    nogo settings to their default values. This is used for all tools that are
    not themselves targets created from rules_go rules and thus do not read
    these settings. Resetting all of them to defaults prevents unnecessary
    configuration changes for these targets that could cause rebuilds.

    Examples: This transition is applied to attributes referencing proto_library
    targets or protoc directly.
    """
    settings = dict(settings, **_reset_transition_dict)
    settings["//go/private:bootstrap_nogo"] = False
    return settings

non_go_tool_transition = transition(
    implementation = _non_go_tool_transition_impl,
    inputs = _reset_transition_keys,
    outputs = _reset_transition_keys,
)

def _go_stdlib_transition_impl(settings, _attr):
    """Sets all Go settings to their default values, except for those affecting the Go SDK.

    This transition is similar to _non_go_tool_transition except that it keeps the
    parts of the configuration that determine how to build the standard library.
    It's used to consolidate the configurations used to build the standard library to limit
    the number built.
    """
    settings = dict(settings)
    for label, value in _reset_transition_dict.items():
        if label not in _stdlib_keep_keys:
            settings[label] = value
    settings["//go/config:tags"] = [t for t in settings["//go/config:tags"] if t in _TAG_AFFECTS_STDLIB]
    settings["//go/private:bootstrap_nogo"] = False
    return settings

go_stdlib_transition = transition(
    implementation = _go_stdlib_transition_impl,
    inputs = _reset_transition_keys,
    outputs = _reset_transition_keys,
)

def _go_reset_target_impl(ctx):
    t = ctx.attr.dep[0]  # [0] seems to be necessary with the transition
    providers = [t[p] for p in [GoLibrary, GoSource, GoArchive] if p in t]

    # We can't pass DefaultInfo through as-is, since Bazel forbids executable
    # if it's a file declared in a different target. To emulate that, symlink
    # to the original executable, if there is one.
    default_info = t[DefaultInfo]

    new_executable = None
    original_executable = default_info.files_to_run.executable
    default_runfiles = default_info.default_runfiles
    if original_executable:
        # In order for the symlink to have the same basename as the original
        # executable (important in the case of proto plugins), put it in a
        # subdirectory named after the label to prevent collisions.
        new_executable = ctx.actions.declare_file(paths.join(ctx.label.name, original_executable.basename))
        ctx.actions.symlink(
            output = new_executable,
            target_file = original_executable,
            is_executable = True,
        )
        default_runfiles = default_runfiles.merge(ctx.runfiles([new_executable]))

    providers.append(
        DefaultInfo(
            files = default_info.files,
            data_runfiles = default_info.data_runfiles,
            default_runfiles = default_runfiles,
            executable = new_executable,
        ),
    )
    return providers

go_reset_target = rule(
    implementation = _go_reset_target_impl,
    attrs = {
        "dep": attr.label(
            mandatory = True,
            cfg = go_tool_transition,
        ),
        "_allowlist_function_transition": attr.label(
            default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
        ),
    },
    doc = """Forwards providers from a target and applies go_tool_transition.

go_reset_target depends on a single target, built using go_tool_transition. It
forwards Go providers and DefaultInfo.

This is used to work around a problem with building tools: Go tools should be
built with 'cfg = "exec"' so they work on the execution platform, but we also
need to apply go_tool_transition so that e.g. a tool isn't built as a shared
library with race instrumentation. This acts as an intermediate rule that allows
to apply both both transitions.
""",
)

non_go_reset_target = rule(
    implementation = _go_reset_target_impl,
    attrs = {
        "dep": attr.label(
            mandatory = True,
            cfg = non_go_tool_transition,
        ),
        "_allowlist_function_transition": attr.label(
            default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
        ),
    },
    doc = """Forwards providers from a target and applies non_go_tool_transition.

non_go_reset_target depends on a single target, built using
non_go_tool_transition. It forwards Go providers and DefaultInfo.

This is used to work around a problem with building tools: Non-Go tools should
be built with 'cfg = "exec"' so they work on the execution platform, but they
also shouldn't be affected by Go-specific config changes applied by
go_transition.
""",
)

def _non_go_transition_impl(settings, _attr):
    """Sets all Go settings to the values they had before the last go_transition.

    non_go_transition sets all of the //go/config settings to the value they had
    before the last go_transition. This should be used on all attributes of
    go_library/go_binary/go_test that are built in the target configuration and
    do not constitute advertise any Go providers.

    Examples: This transition is applied to the 'data' attribute of go_binary so
    that other Go binaries used at runtime aren't affected by a non-standard
    link mode set on the go_binary target, but still use the same top-level
    settings such as e.g. race instrumentation.
    """
    new_settings = {}
    for key, original_key in _SETTING_KEY_TO_ORIGINAL_SETTING_KEY.items():
        original_value = settings[original_key]
        if original_value:
            # Reset to the original value of the setting before go_transition.
            new_settings[key] = json.decode(original_value)
        else:
            new_settings[key] = settings[key]

        # Reset the value of the helper setting to its default for two reasons:
        # 1. Performance: This ensures that the Go settings of non-Go
        #    dependencies have the same values as before the go_transition,
        #    which can prevent unnecessary rebuilds caused by configuration
        #    changes.
        # 2. Correctness in edge cases: If there is a path in the build graph
        #    from a go_binary's non-Go dependency to a go_library that does not
        #    pass through another go_binary (e.g., through a custom rule
        #    replacement for go_binary), this transition could be applied again
        #    and cause incorrect Go setting values.
        new_settings[original_key] = ""

    return new_settings

non_go_transition = transition(
    implementation = _non_go_transition_impl,
    inputs = TRANSITIONED_GO_SETTING_KEYS + _SETTING_KEY_TO_ORIGINAL_SETTING_KEY.values(),
    outputs = TRANSITIONED_GO_SETTING_KEYS + _SETTING_KEY_TO_ORIGINAL_SETTING_KEY.values(),
)

def _check_ternary(name, value):
    if value not in ("on", "off", "auto"):
        fail('{}: must be "on", "off", or "auto"'.format(name))

def _set_ternary(settings, attr, name):
    value = getattr(attr, name, "auto")
    _check_ternary(name, value)
    if value != "auto":
        label = "//go/config:{}".format(name)
        settings[label] = value == "on"
    return value

_SDK_VERSION_BUILD_SETTING = "//go/toolchain:sdk_version"
TRANSITIONED_GO_CROSS_SETTING_KEYS = [
    _SDK_VERSION_BUILD_SETTING,
    "//command_line_option:platforms",
]

def _go_cross_transition_impl(settings, attr):
    settings = dict(settings)
    if attr.sdk_version != None:
        settings[_SDK_VERSION_BUILD_SETTING] = attr.sdk_version

    if attr.platform != None:
        settings["//command_line_option:platforms"] = str(attr.platform)

    return settings

go_cross_transition = transition(
    implementation = _go_cross_transition_impl,
    inputs = TRANSITIONED_GO_CROSS_SETTING_KEYS,
    outputs = TRANSITIONED_GO_CROSS_SETTING_KEYS,
)

# A list of Go build tags that potentially affect the build of the standard
# library.
#
# This should be updated to contain the union of all tags relevant for all
# versions of Go that are still relevant.
#
# Currently supported versions: 1.18, 1.19, 1.20
#
# To regenerate, run and paste the output of
#     bazel run //go/tools/internal/stdlib_tags:stdlib_tags -- path/to/go_sdk_1/src ...
_TAG_AFFECTS_STDLIB = {
    "alpha": None,
    "appengine": None,
    "asan": None,
    "boringcrypto": None,
    "cmd_go_bootstrap": None,
    "compiler_bootstrap": None,
    "debuglog": None,
    "faketime": None,
    "gc": None,
    "gccgo": None,
    "gen": None,
    "generate": None,
    "gofuzz": None,
    "ignore": None,
    "libfuzzer": None,
    "m68k": None,
    "math_big_pure_go": None,
    "msan": None,
    "netcgo": None,
    "netgo": None,
    "nethttpomithttp2": None,
    "nios2": None,
    "noopt": None,
    "osusergo": None,
    "purego": None,
    "race": None,
    "sh": None,
    "shbe": None,
    "tablegen": None,
    "testgo": None,
    "timetzdata": None,
}