-
Notifications
You must be signed in to change notification settings - Fork 4.1k
/
py_executable.bzl
852 lines (772 loc) · 32.6 KB
/
py_executable.bzl
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
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
# Copyright 2022 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.
"""Common functionality between test/binary executables."""
load(":common/cc/cc_helper.bzl", "cc_helper")
load(
":common/python/common.bzl",
"TOOLCHAIN_TYPE",
"collect_imports",
"collect_runfiles",
"create_instrumented_files_info",
"create_output_group_info",
"create_py_info",
"csv",
"filter_to_py_srcs",
"union_attrs",
)
load(
":common/python/attributes.bzl",
"AGNOSTIC_EXECUTABLE_ATTRS",
"COMMON_ATTRS",
"PY_SRCS_ATTRS",
"SRCS_VERSION_ALL_VALUES",
"create_srcs_attr",
"create_srcs_version_attr",
)
load(
":common/python/providers.bzl",
"PyRuntimeInfo",
)
load(
":common/python/semantics.bzl",
"BUILD_DATA_SYMLINK_PATH",
"IS_BAZEL",
"PY_RUNTIME_ATTR_NAME",
)
_cc_common = _builtins.toplevel.cc_common
_py_builtins = _builtins.internal.py_builtins
# Non-Google-specific attributes for executables
# These attributes are for rules that accept Python sources.
EXECUTABLE_ATTRS = union_attrs(
COMMON_ATTRS,
AGNOSTIC_EXECUTABLE_ATTRS,
PY_SRCS_ATTRS,
{
# TODO(b/203567235): In the Java impl, any file is allowed. While marked
# label, it is more treated as a string, and doesn't have to refer to
# anything that exists because it gets treated as suffix-search string
# over `srcs`.
"main": attr.label(
allow_single_file = True,
doc = """\
Optional; the name of the source file that is the main entry point of the
application. This file must also be listed in `srcs`. If left unspecified,
`name`, with `.py` appended, is used instead. If `name` does not match any
filename in `srcs`, `main` must be specified.
""",
),
# TODO(b/203567235): In Google, this attribute is deprecated, and can
# only effectively be PY3. Externally, with Bazel, this attribute has
# a separate story.
"python_version": attr.string(
# TODO(b/203567235): In the Java impl, the default comes from
# --python_version. Not clear what the Starlark equivalent is.
default = "PY3",
# NOTE: Some tests care about the order of these values.
values = ["PY2", "PY3"],
),
},
create_srcs_version_attr(values = SRCS_VERSION_ALL_VALUES),
create_srcs_attr(mandatory = True),
allow_none = True,
)
def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment = []):
"""Base rule implementation for a Python executable.
Google and Bazel call this common base and apply customizations using the
semantics object.
Args:
ctx: The rule ctx
semantics: BinarySemantics struct; see create_binary_semantics_struct()
is_test: bool, True if the rule is a test rule (has `test=True`),
False if not (has `executable=True`)
inherited_environment: List of str; additional environment variable
names that should be inherited from the runtime environment when the
executable is run.
Returns:
DefaultInfo provider for the executable
"""
_validate_executable(ctx)
main_py = determine_main(ctx)
direct_sources = filter_to_py_srcs(ctx.files.srcs)
output_sources = semantics.maybe_precompile(ctx, direct_sources)
imports = collect_imports(ctx, semantics)
executable, files_to_build = _compute_outputs(ctx, output_sources)
runtime_details = _get_runtime_details(ctx, semantics)
if ctx.configuration.coverage_enabled:
extra_deps = semantics.get_coverage_deps(ctx, runtime_details)
else:
extra_deps = []
# The debugger dependency should be prevented by select() config elsewhere,
# but just to be safe, also guard against adding it to the output here.
if not _is_tool_config(ctx):
extra_deps.extend(semantics.get_debugger_deps(ctx, runtime_details))
cc_details = semantics.get_cc_details_for_binary(ctx, extra_deps = extra_deps)
native_deps_details = _get_native_deps_details(
ctx,
semantics = semantics,
cc_details = cc_details,
is_test = is_test,
)
runfiles_details = _get_base_runfiles_for_binary(
ctx,
executable = executable,
extra_deps = extra_deps,
files_to_build = files_to_build,
extra_common_runfiles = [
runtime_details.runfiles,
cc_details.extra_runfiles,
native_deps_details.runfiles,
semantics.get_extra_common_runfiles_for_binary(ctx),
],
semantics = semantics,
)
exec_result = semantics.create_executable(
ctx,
executable = executable,
main_py = main_py,
imports = imports,
is_test = is_test,
runtime_details = runtime_details,
cc_details = cc_details,
native_deps_details = native_deps_details,
runfiles_details = runfiles_details,
)
files_to_build = depset(transitive = [
exec_result.extra_files_to_build,
files_to_build,
])
extra_exec_runfiles = ctx.runfiles(transitive_files = files_to_build)
runfiles_details = struct(
default_runfiles = runfiles_details.default_runfiles.merge(extra_exec_runfiles),
data_runfiles = runfiles_details.data_runfiles.merge(extra_exec_runfiles),
)
legacy_providers, modern_providers = _create_providers(
ctx = ctx,
executable = executable,
runfiles_details = runfiles_details,
main_py = main_py,
imports = imports,
direct_sources = direct_sources,
files_to_build = files_to_build,
runtime_details = runtime_details,
cc_info = cc_details.cc_info_for_propagating,
inherited_environment = inherited_environment,
semantics = semantics,
output_groups = exec_result.output_groups,
)
return struct(
legacy_providers = legacy_providers,
providers = modern_providers,
)
def _validate_executable(ctx):
if ctx.attr.python_version != "PY3":
fail("It is not allowed to use Python 2")
def _compute_outputs(ctx, output_sources):
# TODO: This should use the configuration instead of the Bazel OS.
if _py_builtins.get_current_os_name() == "windows":
executable = ctx.actions.declare_file(ctx.label.name + ".exe")
else:
executable = ctx.actions.declare_file(ctx.label.name)
# TODO(b/208657718): Remove output_sources from the default outputs
# once the depot is cleaned up.
return executable, depset([executable] + output_sources)
def _get_runtime_details(ctx, semantics):
"""Gets various information about the Python runtime to use.
While most information comes from the toolchain, various legacy and
compatibility behaviors require computing some other information.
Args:
ctx: Rule ctx
semantics: A `BinarySemantics` struct; see `create_binary_semantics_struct`
Returns:
A struct; see inline-field comments of the return value for details.
"""
# NOTE: Both Bazel and Google have similar legacy "path to a python
# interpreter" flags with similar functions, but with subtle differences.
#
# Bazel has --python_path. This flag has a computed default of "python" when
# its actual default is null (see
# BazelPythonConfiguration.java#getPythonPath). This flag is only used if
# toolchains are not enabled and `--python_top` isn't set.
#
# Google has --python_binary. This flag defaults to null; no special
# computed behavior. If set, it is used instead of any runtime or toolchain.
# This is a legacy behavior, but not fully cleaned up yet.
#
# TODO(b/230428071): Remove this once Google's --python_binary flag is removed.
# TOOD(bazelbuild/bazel#7901): Remove this once --python_path flag is removed.
if IS_BAZEL:
flag_interpreter_path = ctx.fragments.bazel_py.python_path
toolchain_runtime, effective_runtime = _maybe_get_runtime_from_ctx(ctx)
if not effective_runtime:
# Clear these just in case
toolchain_runtime = None
effective_runtime = None
else: # Google code path
flag_interpreter_path = None
toolchain_runtime, effective_runtime = _maybe_get_runtime_from_ctx(ctx)
if not effective_runtime:
fail("Unable to find Python runtime")
if effective_runtime:
direct = [] # List of files
transitive = [] # List of depsets
if effective_runtime.interpreter:
direct.append(effective_runtime.interpreter)
transitive.append(effective_runtime.files)
if ctx.configuration.coverage_enabled:
if effective_runtime.coverage_tool:
direct.append(effective_runtime.coverage_tool)
if effective_runtime.coverage_files:
transitive.append(effective_runtime.coverage_files)
runtime_files = depset(direct = direct, transitive = transitive)
else:
runtime_files = depset()
executable_interpreter_path = semantics.get_interpreter_path(
ctx,
runtime = effective_runtime,
flag_interpreter_path = flag_interpreter_path,
)
return struct(
# Optional PyRuntimeInfo: The runtime found from toolchain resolution.
# This may be None because, within Google, toolchain resolution isn't
# yet enabled.
toolchain_runtime = toolchain_runtime,
# Optional PyRuntimeInfo: The runtime that should be used. When
# toolchain resolution is enabled, this is the same as
# `toolchain_resolution`. Otherwise, this probably came from the
# `_python_top` attribute that the Google implementation still uses.
# This is separate from `toolchain_runtime` because toolchain_runtime
# is propagated as a provider, while non-toolchain runtimes are not.
effective_runtime = effective_runtime,
# str; Path to the Python interpreter to use for running the executable
# itself (not the bootstrap script). Either an absolute path (which
# means it is platform-specific), or a runfiles-relative path (which
# means the interpreter should be within `runtime_files`)
executable_interpreter_path = executable_interpreter_path,
# runfiles: Additional runfiles specific to the runtime that should
# be included. For in-build runtimes, this shold include the interpreter
# and any supporting files.
runfiles = ctx.runfiles(transitive_files = runtime_files),
)
def _maybe_get_runtime_from_ctx(ctx):
"""Finds the PyRuntimeInfo from the toolchain or attribute, if available.
Returns:
2-tuple of toolchain_runtime, effective_runtime
"""
if ctx.fragments.py.use_toolchains:
toolchain = ctx.toolchains[TOOLCHAIN_TYPE]
# Hack around the fact that the autodetecting Python toolchain, which is
# automatically registered, does not yet support Windows. In this case,
# we want to return null so that _get_interpreter_path falls back on
# --python_path. See tools/python/toolchain.bzl.
# TODO(#7844): Remove this hack when the autodetecting toolchain has a
# Windows implementation.
if (
# BazelPyBinaryConfiguredTargetTest.toolchainInfoFieldHasBadVersion purposefully
# omits the py2_runtime attribute to test for other error messages.
hasattr(toolchain, "py2_runtime") and toolchain.py2_runtime and
toolchain.py2_runtime.interpreter_path == "/_magic_pyruntime_sentinel_do_not_use"
):
return None, None
if not hasattr(toolchain, "py3_runtime"):
fail("Python toolchain field 'py3_runtime' is missing")
if not toolchain.py3_runtime:
fail("Python toolchain missing py3_runtime")
py3_runtime = toolchain.py3_runtime
if py3_runtime.python_version != "PY3":
fail("Python toolchain py3_runtime must be python_version=PY3, got {}".format(
py3_runtime.python_version,
))
toolchain_runtime = toolchain.py3_runtime
effective_runtime = toolchain_runtime
else:
toolchain_runtime = None
attr_target = getattr(ctx.attr, PY_RUNTIME_ATTR_NAME)
# In Bazel, --python_top is null by default.
if attr_target and PyRuntimeInfo in attr_target:
effective_runtime = attr_target[PyRuntimeInfo]
else:
return None, None
return toolchain_runtime, effective_runtime
def _get_base_runfiles_for_binary(
ctx,
*,
executable,
extra_deps,
files_to_build,
extra_common_runfiles,
semantics):
"""Returns the set of runfiles necessary prior to executable creation.
NOTE: The term "common runfiles" refers to the runfiles that both the
default and data runfiles have in common.
Args:
ctx: The rule ctx.
executable: The main executable output.
extra_deps: List of Targets; additional targets whose runfiles
will be added to the common runfiles.
files_to_build: depset of File of the default outputs to add into runfiles.
extra_common_runfiles: List of runfiles; additional runfiles that
will be added to the common runfiles.
semantics: A `BinarySemantics` struct; see `create_binary_semantics_struct`.
Returns:
struct with attributes:
* default_runfiles: The default runfiles
* data_runfiles: The data runfiles
"""
common_runfiles = collect_runfiles(ctx, depset(
direct = [executable],
transitive = [files_to_build],
))
if extra_deps:
common_runfiles = common_runfiles.merge_all([
t[DefaultInfo].default_runfiles
for t in extra_deps
])
common_runfiles = common_runfiles.merge_all(extra_common_runfiles)
if semantics.should_create_init_files(ctx):
common_runfiles = _py_builtins.merge_runfiles_with_generated_inits_empty_files_supplier(
ctx = ctx,
runfiles = common_runfiles,
)
# Don't include build_data.txt in data runfiles. This allows binaries to
# contain other binaries while still using the same fixed location symlink
# for the build_data.txt file. Really, the fixed location symlink should be
# removed and another way found to locate the underlying build data file.
data_runfiles = common_runfiles
if is_stamping_enabled(ctx, semantics) and semantics.should_include_build_data(ctx):
default_runfiles = common_runfiles.merge(_create_runfiles_with_build_data(
ctx,
semantics.get_central_uncachable_version_file(ctx),
semantics.get_extra_write_build_data_env(ctx),
))
else:
default_runfiles = common_runfiles
return struct(
default_runfiles = default_runfiles,
data_runfiles = data_runfiles,
)
def _create_runfiles_with_build_data(
ctx,
central_uncachable_version_file,
extra_write_build_data_env):
return ctx.runfiles(
symlinks = {
BUILD_DATA_SYMLINK_PATH: _write_build_data(
ctx,
central_uncachable_version_file,
extra_write_build_data_env,
),
},
)
def _write_build_data(ctx, central_uncachable_version_file, extra_write_build_data_env):
# TODO: Remove this logic when a central file is always available
if not central_uncachable_version_file:
version_file = ctx.actions.declare_file(ctx.label.name + "-uncachable_version_file.txt")
_py_builtins.copy_without_caching(
ctx = ctx,
read_from = ctx.version_file,
write_to = version_file,
)
else:
version_file = central_uncachable_version_file
direct_inputs = [ctx.info_file, version_file]
# A "constant metadata" file is basically a special file that doesn't
# support change detection logic and reports that it is unchanged. i.e., it
# behaves like ctx.version_file and is ignored when computing "what inputs
# changed" (see https://bazel.build/docs/user-manual#workspace-status).
#
# We do this so that consumers of the final build data file don't have
# to transitively rebuild everything -- the `uncachable_version_file` file
# isn't cachable, which causes the build data action to always re-run.
#
# While this technically means a binary could have stale build info,
# it ends up not mattering in practice because the volatile information
# doesn't meaningfully effect other outputs.
#
# This is also done for performance and Make It work reasons:
# * Passing the transitive dependencies into the action requires passing
# the runfiles, but actions don't directly accept runfiles. While
# flattening the depsets can be deferred, accessing the
# `runfiles.empty_filenames` attribute will will invoke the empty
# file supplier a second time, which is too much of a memory and CPU
# performance hit.
# * Some targets specify a directory in `data`, which is unsound, but
# mostly works. Google's RBE, unfortunately, rejects it.
# * A binary's transitive closure may be so large that it exceeds
# Google RBE limits for action inputs.
build_data = _py_builtins.declare_constant_metadata_file(
ctx = ctx,
name = ctx.label.name + ".build_data.txt",
root = ctx.bin_dir,
)
ctx.actions.run(
executable = ctx.executable._build_data_gen,
env = {
"TARGET": str(ctx.label),
"OUTPUT": build_data.path,
"VERSION_FILE": version_file.path,
# NOTE: ctx.info_file is undocumented; see
# https://github.com/bazelbuild/bazel/issues/9363
"INFO_FILE": ctx.info_file.path,
"PLATFORM": cc_helper.find_cpp_toolchain(ctx).toolchain_id,
} | extra_write_build_data_env,
inputs = depset(
direct = direct_inputs,
),
outputs = [build_data],
mnemonic = "PyWriteBuildData",
progress_message = "Generating %{label} build_data.txt",
)
return build_data
def _get_native_deps_details(ctx, *, semantics, cc_details, is_test):
if not semantics.should_build_native_deps_dso(ctx):
return struct(dso = None, runfiles = ctx.runfiles())
cc_info = cc_details.cc_info_for_self_link
if not cc_info.linking_context.linker_inputs:
return struct(dso = None, runfiles = ctx.runfiles())
dso = ctx.actions.declare_file(semantics.get_native_deps_dso_name(ctx))
share_native_deps = ctx.fragments.cpp.share_native_deps()
cc_feature_config = cc_configure_features(
ctx,
cc_toolchain = cc_details.cc_toolchain,
# See b/171276569#comment18: this feature string is just to allow
# Google's RBE to know the link action is for the Python case so it can
# take special actions (though as of Jun 2022, no special action is
# taken).
extra_features = ["native_deps_link"],
)
if share_native_deps:
linked_lib = _create_shared_native_deps_dso(
ctx,
cc_info = cc_info,
is_test = is_test,
requested_features = cc_feature_config.requested_features,
feature_configuration = cc_feature_config.feature_configuration,
)
ctx.actions.symlink(
output = dso,
target_file = linked_lib,
progress_message = "Symlinking shared native deps for %(label)",
)
else:
linked_lib = dso
_cc_common.link(
name = ctx.label.name,
actions = ctx.actions,
linking_contexts = [cc_info.linking_context],
output_type = "dynamic_library",
never_link = True,
native_deps = True,
feature_configuration = cc_feature_config.feature_configuration,
cc_toolchain = cc_details.cc_toolchain,
test_only_target = is_test,
stamp = 1 if is_stamping_enabled(ctx, semantics) else 0,
grep_includes = ctx.file._grep_includes,
main_output = linked_lib,
use_shareable_artifact_factory = True,
# NOTE: Only flags not captured by cc_info.linking_context need to
# be manually passed
user_link_flags = semantics.get_native_deps_user_link_flags(ctx),
)
return struct(
dso = dso,
runfiles = ctx.runfiles(files = [dso]),
)
def _create_shared_native_deps_dso(
ctx,
*,
cc_info,
is_test,
feature_configuration,
requested_features):
linkstamps = cc_info.linking_context.linkstamps()
partially_disabled_thin_lto = (
_cc_common.is_enabled(
feature_name = "thin_lto_linkstatic_tests_use_shared_nonlto_backends",
feature_configuration = feature_configuration,
) and not _cc_common.is_enabled(
feature_name = "thin_lto_all_linkstatic_use_shared_nonlto_backends",
feature_configuration = feature_configuration,
)
)
dso_hash = _get_shared_native_deps_hash(
linker_inputs = cc_helper.get_static_mode_params_for_dynamic_library_libraries(
depset([
lib
for linker_input in cc_info.linking_context.linker_inputs.to_list()
for lib in linker_input.libraries
]),
),
link_opts = [
flag
for input in cc_info.linking_context.linker_inputs.to_list()
for flag in input.user_link_flags
],
linkstamps = [linkstamp.file() for linkstamp in linkstamps.to_list()],
build_info_artifacts = _cc_common.get_build_info(ctx) if linkstamps else [],
features = requested_features,
is_test_target_partially_disabled_thin_lto = is_test and partially_disabled_thin_lto,
)
return ctx.actions.declare_shareable_artifact("_nativedeps/%x.so" % dso_hash)
# This is a minimal version of NativeDepsHelper.getSharedNativeDepsPath, see
# com.google.devtools.build.lib.rules.nativedeps.NativeDepsHelper#getSharedNativeDepsPath
# The basic idea is to take all the inputs that affect linking and encode (via
# hashing) them into the filename.
# TODO(b/234232820): The settings that affect linking must be kept in sync with the actual
# C++ link action. For more information, see the large descriptive comment on
# NativeDepsHelper#getSharedNativeDepsPath.
def _get_shared_native_deps_hash(
*,
linker_inputs,
link_opts,
linkstamps,
build_info_artifacts,
features,
is_test_target_partially_disabled_thin_lto):
# NOTE: We use short_path because the build configuration root in which
# files are always created already captures the configuration-specific
# parts, so no need to include them manually.
parts = []
for artifact in linker_inputs:
parts.append(artifact.short_path)
parts.append(str(len(link_opts)))
parts.extend(link_opts)
for artifact in linkstamps:
parts.append(artifact.short_path)
for artifact in build_info_artifacts:
parts.append(artifact.short_path)
parts.extend(sorted(features))
# Sharing of native dependencies may cause an {@link
# ActionConflictException} when ThinLTO is disabled for test and test-only
# targets that are statically linked, but enabled for other statically
# linked targets. This happens in case the artifacts for the shared native
# dependency are output by {@link Action}s owned by the non-test and test
# targets both. To fix this, we allow creation of multiple artifacts for the
# shared native library - one shared among the test and test-only targets
# where ThinLTO is disabled, and the other shared among other targets where
# ThinLTO is enabled. See b/138118275
parts.append("1" if is_test_target_partially_disabled_thin_lto else "0")
return hash("".join(parts))
def determine_main(ctx):
"""Determine the main entry point .py source file.
Args:
ctx: The rule ctx.
Returns:
Artifact; the main file. If one can't be found, an error is raised.
"""
if ctx.attr.main:
proposed_main = ctx.attr.main.label.name
if not proposed_main.endswith(".py"):
fail("main must end in '.py'")
else:
if ctx.label.name.endswith(".py"):
fail("name must not end in '.py'")
proposed_main = ctx.label.name + ".py"
main_files = [src for src in ctx.files.srcs if _path_endswith(src.short_path, proposed_main)]
if not main_files:
if ctx.attr.main:
fail("could not find '{}' as specified by 'main' attribute".format(proposed_main))
else:
fail(("corresponding default '{}' does not appear in srcs. Add " +
"it or override default file name with a 'main' attribute").format(
proposed_main,
))
elif len(main_files) > 1:
if ctx.attr.main:
fail(("file name '{}' specified by 'main' attributes matches multiple files. " +
"Matches: {}").format(
proposed_main,
csv([f.short_path for f in main_files]),
))
else:
fail(("default main file '{}' matches multiple files in srcs. Perhaps specify " +
"an explicit file with 'main' attribute? Matches were: {}").format(
proposed_main,
csv([f.short_path for f in main_files]),
))
return main_files[0]
def _path_endswith(path, endswith):
# Use slash to anchor each path to prevent e.g.
# "ab/c.py".endswith("b/c.py") from incorrectly matching.
return ("/" + path).endswith("/" + endswith)
def is_stamping_enabled(ctx, semantics):
"""Tells if stamping is enabled or not.
Args:
ctx: The rule ctx
semantics: a semantics struct (see create_semantics_struct).
Returns:
bool; True if stamping is enabled, False if not.
"""
if _is_tool_config(ctx):
return False
stamp = ctx.attr.stamp
if stamp == 1:
return True
elif stamp == 0:
return False
elif stamp == -1:
return semantics.get_stamp_flag(ctx)
else:
fail("Unsupported `stamp` value: {}".format(stamp))
def _is_tool_config(ctx):
# NOTE: The is_tool_configuration() function is only usable by builtins.
# See https://github.com/bazelbuild/bazel/issues/14444 for the FR for
# a more public API. Outside of builtins, ctx.bin_dir.path can be
# checked for `/host/` or `-exec-`.
return ctx.configuration.is_tool_configuration()
def _create_providers(
*,
ctx,
executable,
main_py,
direct_sources,
files_to_build,
runfiles_details,
imports,
cc_info,
inherited_environment,
runtime_details,
output_groups,
semantics):
"""Creates the providers an executable should return.
Args:
ctx: The rule ctx.
executable: File; the target's executable file.
main_py: File; the main .py entry point.
direct_sources: list of Files; the direct, raw `.py` sources for the target.
This should only be Python source files. It should not include pyc
files.
files_to_build: depset of Files; the files for DefaultInfo.files
runfiles_details: runfiles that will become the default and data runfiles.
imports: depset of strings; the import paths to propagate
cc_info: optional CcInfo; Linking information to propagate as
PyCcLinkParamsProvider. Note that only the linking information
is propagated, not the whole CcInfo.
inherited_environment: list of strings; Environment variable names
that should be inherited from the environment the executuble
is run within.
runtime_details: struct of runtime information; see _get_runtime_details()
output_groups: dict[str, depset[File]]; used to create OutputGroupInfo
semantics: BinarySemantics struct; see create_binary_semantics()
Returns:
A two-tuple of:
1. A dict of legacy providers.
2. A list of modern providers.
"""
providers = [
DefaultInfo(
executable = executable,
files = files_to_build,
default_runfiles = _py_builtins.make_runfiles_respect_legacy_external_runfiles(
ctx,
runfiles_details.default_runfiles,
),
data_runfiles = _py_builtins.make_runfiles_respect_legacy_external_runfiles(
ctx,
runfiles_details.data_runfiles,
),
),
create_instrumented_files_info(ctx),
_create_run_environment_info(ctx, inherited_environment),
]
# TODO(b/265840007): Make this non-conditional once Google enables
# --incompatible_use_python_toolchains.
if runtime_details.toolchain_runtime:
providers.append(runtime_details.toolchain_runtime)
# TODO(b/163083591): Remove the PyCcLinkParamsProvider once binaries-in-deps
# are cleaned up.
if cc_info:
providers.append(
_py_builtins.new_py_cc_link_params_provider(cc_info = cc_info),
)
py_info, deps_transitive_sources = create_py_info(
ctx,
direct_sources = depset(direct_sources),
imports = imports,
)
# TODO(b/253059598): Remove support for extra actions; https://github.com/bazelbuild/bazel/issues/16455
listeners_enabled = _py_builtins.are_action_listeners_enabled(ctx)
if listeners_enabled:
_py_builtins.add_py_extra_pseudo_action(
ctx = ctx,
dependency_transitive_python_sources = deps_transitive_sources,
)
providers.append(py_info)
providers.append(create_output_group_info(py_info.transitive_sources, output_groups))
extra_legacy_providers, extra_providers = semantics.get_extra_providers(
ctx,
main_py = main_py,
runtime_details = runtime_details,
)
providers.extend(extra_providers)
return extra_legacy_providers, providers
def _create_run_environment_info(ctx, inherited_environment):
expanded_env = {}
for key, value in ctx.attr.env.items():
expanded_env[key] = _py_builtins.expand_location_and_make_variables(
ctx = ctx,
attribute_name = "env[{}]".format(key),
expression = value,
targets = ctx.attr.data,
)
return RunEnvironmentInfo(
environment = expanded_env,
inherited_environment = inherited_environment,
)
def create_base_executable_rule(*, attrs, fragments = [], **kwargs):
"""Create a function for defining for Python binary/test targets.
Args:
attrs: Rule attributes
fragments: List of str; extra config fragments that are required.
**kwargs: Additional args to pass onto `rule()`
Returns:
A rule function
"""
if "py" not in fragments:
# The list might be frozen, so use concatentation
fragments = fragments + ["py"]
return rule(
# TODO: add ability to remove attrs, i.e. for imports attr
attrs = EXECUTABLE_ATTRS | attrs,
toolchains = [TOOLCHAIN_TYPE] + cc_helper.use_cpp_toolchain(),
fragments = fragments,
**kwargs
)
def cc_configure_features(ctx, *, cc_toolchain, extra_features):
"""Configure C++ features for Python purposes.
Args:
ctx: Rule ctx
cc_toolchain: The CcToolchain the target is using.
extra_features: list of strings; additional features to request be
enabled.
Returns:
struct of the feature configuration and all requested features.
"""
requested_features = ["static_linking_mode"]
requested_features.extend(extra_features)
requested_features.extend(ctx.features)
if "legacy_whole_archive" not in ctx.disabled_features:
requested_features.append("legacy_whole_archive")
feature_configuration = _cc_common.configure_features(
ctx = ctx,
cc_toolchain = cc_toolchain,
requested_features = requested_features,
unsupported_features = ctx.disabled_features,
)
return struct(
feature_configuration = feature_configuration,
requested_features = requested_features,
)
only_exposed_for_google_internal_reason = struct(
create_runfiles_with_build_data = _create_runfiles_with_build_data,
)