diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel index 8479f67b49..5db77750bd 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -69,6 +69,14 @@ bzl_library( ], ) +bzl_library( + name = "builders_bzl", + srcs = ["builders.bzl"], + deps = [ + "@bazel_skylib//lib:types", + ], +) + bzl_library( name = "bzlmod_enabled_bzl", srcs = ["bzlmod_enabled.bzl"], @@ -270,8 +278,10 @@ bzl_library( name = "py_info_bzl", srcs = ["py_info.bzl"], deps = [ + ":builders_bzl", ":reexports_bzl", ":util_bzl", + "@rules_python_internal//:rules_python_config_bzl", ], ) diff --git a/python/private/builders.bzl b/python/private/builders.bzl new file mode 100644 index 0000000000..50aa3ed91a --- /dev/null +++ b/python/private/builders.bzl @@ -0,0 +1,190 @@ +# Copyright 2024 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. +"""Builders to make building complex objects easier.""" + +load("@bazel_skylib//lib:types.bzl", "types") + +def _DepsetBuilder(): + """Create a builder for a depset.""" + + # buildifier: disable=uninitialized + self = struct( + _order = [None], + add = lambda *a, **k: _DepsetBuilder_add(self, *a, **k), + build = lambda *a, **k: _DepsetBuilder_build(self, *a, **k), + direct = [], + get_order = lambda *a, **k: _DepsetBuilder_get_order(self, *a, **k), + set_order = lambda *a, **k: _DepsetBuilder_set_order(self, *a, **k), + transitive = [], + ) + return self + +def _DepsetBuilder_add(self, *values): + """Add value to the depset. + + Args: + self: {type}`DepsetBuilder` implicitly added. + *values: {type}`depset | list | object` Values to add to the depset. + The values can be a depset, the non-depset value to add, or + a list of such values to add. + + Returns: + {type}`DepsetBuilder` + """ + for value in values: + if types.is_list(value): + for sub_value in value: + if types.is_depset(sub_value): + self.transitive.append(sub_value) + else: + self.direct.append(sub_value) + elif types.is_depset(value): + self.transitive.append(value) + else: + self.direct.append(value) + return self + +def _DepsetBuilder_set_order(self, order): + """Sets the order to use. + + Args: + self: {type}`DepsetBuilder` implicitly added. + order: {type}`str` One of the {obj}`depset` `order` values. + + Returns: + {type}`DepsetBuilder` + """ + self._order[0] = order + return self + +def _DepsetBuilder_get_order(self): + """Gets the depset order that will be used. + + Args: + self: {type}`DepsetBuilder` implicitly added. + + Returns: + {type}`str | None` If not previously set, `None` is returned. + """ + return self._order[0] + +def _DepsetBuilder_build(self): + """Creates a {obj}`depset` from the accumulated values. + + Args: + self: {type}`DepsetBuilder` implicitly added. + + Returns: + {type}`depset` + """ + if not self.direct and len(self.transitive) == 1 and self._order[0] == None: + return self.transitive[0] + else: + kwargs = {} + if self._order[0] != None: + kwargs["order"] = self._order[0] + return depset(direct = self.direct, transitive = self.transitive, **kwargs) + +def _RunfilesBuilder(): + """Creates a `RunfilesBuilder`. + + Returns: + {type}`RunfilesBuilder` + """ + + # buildifier: disable=uninitialized + self = struct( + add = lambda *a, **k: _RunfilesBuilder_add(self, *a, **k), + add_targets = lambda *a, **k: _RunfilesBuilder_add_targets(self, *a, **k), + build = lambda *a, **k: _RunfilesBuilder_build(self, *a, **k), + files = _DepsetBuilder(), + root_symlinks = {}, + runfiles = [], + symlinks = {}, + ) + return self + +def _RunfilesBuilder_add(self, *values): + """Adds a value to the runfiles. + + Args: + self: {type}`RunfilesBuilder` implicitly added. + *values: {type}`File | runfiles | list[File] | depset[File] | list[runfiles]` + The values to add. + + Returns: + {type}`RunfilesBuilder` + """ + for value in values: + if types.is_list(value): + for sub_value in value: + _RunfilesBuilder_add_internal(self, sub_value) + else: + _RunfilesBuilder_add_internal(self, value) + return self + +def _RunfilesBuilder_add_targets(self, targets): + """Adds runfiles from targets + + Args: + self: {type}`RunfilesBuilder` implicitly added. + targets: {type}`list[Target]` targets whose default runfiles + to add. + + Returns: + {type}`RunfilesBuilder` + """ + for t in targets: + self.runfiles.append(t[DefaultInfo].default_runfiles) + return self + +def _RunfilesBuilder_add_internal(self, value): + if _is_file(value): + self.files.add(value) + elif types.is_depset(value): + self.files.add(value) + elif _is_runfiles(value): + self.runfiles.append(value) + else: + fail("Unhandled value: type {}: {}".format(type(value), value)) + +def _RunfilesBuilder_build(self, ctx, **kwargs): + """Creates a {obj}`runfiles` from the accumulated values. + + Args: + self: {type}`RunfilesBuilder` implicitly added. + ctx: {type}`ctx` The rule context to use to create the runfiles object. + **kwargs: additional args to pass along to {obj}`ctx.runfiles`. + + Returns: + {type}`runfiles` + """ + return ctx.runfiles( + transitive_files = self.files.build(), + symlinks = self.symlinks, + root_symlinks = self.root_symlinks, + **kwargs + ).merge_all(self.runfiles) + +# Skylib's types module doesn't have is_file, so roll our own +def _is_file(value): + return type(value) == "File" + +def _is_runfiles(value): + return type(value) == "runfiles" + +builders = struct( + DepsetBuilder = _DepsetBuilder, + RunfilesBuilder = _RunfilesBuilder, +) diff --git a/python/private/common/common.bzl b/python/private/common/common.bzl index bbda712d2d..e4cc254654 100644 --- a/python/private/common/common.bzl +++ b/python/private/common/common.bzl @@ -13,7 +13,7 @@ # limitations under the License. """Various things common to Bazel and Google rule implementations.""" -load("//python/private:py_info.bzl", "PyInfo") +load("//python/private:py_info.bzl", "PyInfo", "PyInfoBuilder") load("//python/private:reexports.bzl", "BuiltinPyInfo") load(":cc_helper.bzl", "cc_helper") load(":py_internal.bzl", "py_internal") @@ -282,7 +282,7 @@ def collect_imports(ctx, semantics): if BuiltinPyInfo in dep ]) -def collect_runfiles(ctx, files): +def collect_runfiles(ctx, files = depset()): """Collects the necessary files from the rule's context. This presumes the ctx is for a py_binary, py_test, or py_library rule. @@ -364,84 +364,50 @@ def create_py_info(ctx, *, direct_sources, direct_pyc_files, imports): transitive sources collected from dependencies (the latter is only necessary for deprecated extra actions support). """ - uses_shared_libraries = False - has_py2_only_sources = ctx.attr.srcs_version in ("PY2", "PY2ONLY") - has_py3_only_sources = ctx.attr.srcs_version in ("PY3", "PY3ONLY") - transitive_sources_depsets = [] # list of depsets - transitive_sources_files = [] # list of Files - transitive_pyc_depsets = [direct_pyc_files] # list of depsets + + py_info = PyInfoBuilder() + py_info.direct_pyc_files.add(direct_pyc_files) + py_info.transitive_pyc_files.add(direct_pyc_files) + py_info.imports.add(imports) + py_info.merge_has_py2_only_sources(ctx.attr.srcs_version in ("PY2", "PY2ONLY")) + py_info.merge_has_py3_only_sources(ctx.attr.srcs_version in ("PY3", "PY3ONLY")) + for target in ctx.attr.deps: # PyInfo may not be present e.g. cc_library rules. if PyInfo in target or BuiltinPyInfo in target: - info = _get_py_info(target) - transitive_sources_depsets.append(info.transitive_sources) - uses_shared_libraries = uses_shared_libraries or info.uses_shared_libraries - has_py2_only_sources = has_py2_only_sources or info.has_py2_only_sources - has_py3_only_sources = has_py3_only_sources or info.has_py3_only_sources - - # BuiltinPyInfo doesn't have this field. - if hasattr(info, "transitive_pyc_files"): - transitive_pyc_depsets.append(info.transitive_pyc_files) + py_info.merge(_get_py_info(target)) else: # TODO(b/228692666): Remove this once non-PyInfo targets are no # longer supported in `deps`. files = target.files.to_list() for f in files: if f.extension == "py": - transitive_sources_files.append(f) - uses_shared_libraries = ( - uses_shared_libraries or - cc_helper.is_valid_shared_library_artifact(f) - ) - deps_transitive_sources = depset( - direct = transitive_sources_files, - transitive = transitive_sources_depsets, - ) + py_info.transitive_sources.add(f) + py_info.merge_uses_shared_libraries(cc_helper.is_valid_shared_library_artifact(f)) + + deps_transitive_sources = py_info.transitive_sources.build() + py_info.transitive_sources.add(direct_sources) # We only look at data to calculate uses_shared_libraries, if it's already # true, then we don't need to waste time looping over it. - if not uses_shared_libraries: + if not py_info.get_uses_shared_libraries(): # Similar to the above, except we only calculate uses_shared_libraries for target in ctx.attr.data: # TODO(b/234730058): Remove checking for PyInfo in data once depot # cleaned up. if PyInfo in target or BuiltinPyInfo in target: info = _get_py_info(target) - uses_shared_libraries = info.uses_shared_libraries + py_info.merge_uses_shared_libraries(info.uses_shared_libraries) else: files = target.files.to_list() for f in files: - uses_shared_libraries = cc_helper.is_valid_shared_library_artifact(f) - if uses_shared_libraries: + py_info.merge_uses_shared_libraries(cc_helper.is_valid_shared_library_artifact(f)) + if py_info.get_uses_shared_libraries(): break - if uses_shared_libraries: + if py_info.get_uses_shared_libraries(): break - py_info_kwargs = dict( - transitive_sources = depset( - transitive = [deps_transitive_sources, direct_sources], - ), - imports = imports, - # NOTE: This isn't strictly correct, but with Python 2 gone, - # the srcs_version logic is largely defunct, so shouldn't matter in - # practice. - has_py2_only_sources = has_py2_only_sources, - has_py3_only_sources = has_py3_only_sources, - uses_shared_libraries = uses_shared_libraries, - direct_pyc_files = direct_pyc_files, - transitive_pyc_files = depset(transitive = transitive_pyc_depsets), - ) - - # TODO(b/203567235): Set `uses_shared_libraries` field, though the Bazel - # docs indicate it's unused in Bazel and may be removed. - py_info = PyInfo(**py_info_kwargs) - - # Remove args that BuiltinPyInfo doesn't support - py_info_kwargs.pop("direct_pyc_files") - py_info_kwargs.pop("transitive_pyc_files") - builtin_py_info = BuiltinPyInfo(**py_info_kwargs) - - return py_info, deps_transitive_sources, builtin_py_info + return py_info.build(), deps_transitive_sources, py_info.build_builtin_py_info() def _get_py_info(target): return target[PyInfo] if PyInfo in target else target[BuiltinPyInfo] diff --git a/python/private/common/py_executable.bzl b/python/private/common/py_executable.bzl index 37ca313e0b..1d14344d4e 100644 --- a/python/private/common/py_executable.bzl +++ b/python/private/common/py_executable.bzl @@ -17,6 +17,7 @@ load("@bazel_skylib//lib:dicts.bzl", "dicts") load("@bazel_skylib//lib:structs.bzl", "structs") load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") load("@rules_cc//cc:defs.bzl", "cc_common") +load("//python/private:builders.bzl", "builders") load("//python/private:flags.bzl", "PrecompileAddToRunfilesFlag") load("//python/private:py_executable_info.bzl", "PyExecutableInfo") load("//python/private:py_info.bzl", "PyInfo") @@ -170,9 +171,10 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment = direct_pyc_files = depset(precompile_result.pyc_files) executable = _declare_executable_file(ctx) - default_outputs = [executable] - default_outputs.extend(precompile_result.keep_srcs) - default_outputs.extend(precompile_result.pyc_files) + default_outputs = builders.DepsetBuilder() + default_outputs.add(executable) + default_outputs.add(precompile_result.keep_srcs) + default_outputs.add(precompile_result.pyc_files) imports = collect_imports(ctx, semantics) @@ -219,6 +221,7 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment = native_deps_details = native_deps_details, runfiles_details = runfiles_details, ) + default_outputs.add(exec_result.extra_files_to_build) extra_exec_runfiles = exec_result.extra_runfiles.merge( ctx.runfiles(transitive_files = exec_result.extra_files_to_build), @@ -240,7 +243,7 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment = imports = imports, direct_sources = direct_sources, direct_pyc_files = direct_pyc_files, - default_outputs = depset(default_outputs, transitive = [exec_result.extra_files_to_build]), + default_outputs = default_outputs.build(), runtime_details = runtime_details, cc_info = cc_details.cc_info_for_propagating, inherited_environment = inherited_environment, @@ -429,26 +432,25 @@ def _get_base_runfiles_for_binary( * build_data_file: A file with build stamp information if stamping is enabled, otherwise None. """ - common_runfiles_depsets = [main_py_files] + common_runfiles = builders.RunfilesBuilder() + common_runfiles.add(main_py_files) if ctx.attr._precompile_add_to_runfiles_flag[BuildSettingInfo].value == PrecompileAddToRunfilesFlag.ALWAYS: - common_runfiles_depsets.append(direct_pyc_files) + common_runfiles.add(direct_pyc_files) elif PycCollectionAttr.is_pyc_collection_enabled(ctx): - common_runfiles_depsets.append(direct_pyc_files) + common_runfiles.add(direct_pyc_files) for dep in (ctx.attr.deps + extra_deps): if PyInfo not in dep: continue - common_runfiles_depsets.append(dep[PyInfo].transitive_pyc_files) + common_runfiles.add(dep[PyInfo].transitive_pyc_files) - common_runfiles = collect_runfiles(ctx, depset( - transitive = common_runfiles_depsets, - )) + common_runfiles.add(collect_runfiles(ctx)) + common_runfiles.add(collect_runfiles(ctx)) 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) + common_runfiles.add_runfiles(targets = extra_deps) + common_runfiles.add(extra_common_runfiles) + + common_runfiles = common_runfiles.build(ctx) if semantics.should_create_init_files(ctx): common_runfiles = _py_builtins.merge_runfiles_with_generated_inits_empty_files_supplier( diff --git a/python/private/common/py_library.bzl b/python/private/common/py_library.bzl index fd534908d0..4423986e1b 100644 --- a/python/private/common/py_library.bzl +++ b/python/private/common/py_library.bzl @@ -15,6 +15,7 @@ load("@bazel_skylib//lib:dicts.bzl", "dicts") load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") +load("//python/private:builders.bzl", "builders") load("//python/private:flags.bzl", "PrecompileAddToRunfilesFlag") load( "//python/private:toolchain_types.bzl", @@ -67,16 +68,19 @@ def py_library_impl(ctx, *, semantics): precompile_result = semantics.maybe_precompile(ctx, direct_sources) direct_pyc_files = depset(precompile_result.pyc_files) - default_outputs = depset(precompile_result.keep_srcs, transitive = [direct_pyc_files]) + default_outputs = builders.DepsetBuilder() + default_outputs.add(precompile_result.keep_srcs) + default_outputs.add(direct_pyc_files) + default_outputs = default_outputs.build() + + runfiles = builders.RunfilesBuilder() + runfiles.add(precompile_result.keep_srcs) - extra_runfiles_depsets = [depset(precompile_result.keep_srcs)] if ctx.attr._precompile_add_to_runfiles_flag[BuildSettingInfo].value == PrecompileAddToRunfilesFlag.ALWAYS: - extra_runfiles_depsets.append(direct_pyc_files) + runfiles.add(direct_pyc_files) - runfiles = collect_runfiles( - ctx = ctx, - files = depset(transitive = extra_runfiles_depsets), - ) + runfiles.add(collect_runfiles(ctx)) + runfiles = runfiles.build(ctx) cc_info = semantics.get_cc_info_for_library(ctx) py_info, deps_transitive_sources, builtins_py_info = create_py_info( diff --git a/python/private/py_info.bzl b/python/private/py_info.bzl index 7945775a25..a3e40f2924 100644 --- a/python/private/py_info.bzl +++ b/python/private/py_info.bzl @@ -13,6 +13,9 @@ # limitations under the License. """Implementation of PyInfo provider and PyInfo-specific utilities.""" +load("@rules_python_internal//:rules_python_config.bzl", "config") +load("//python/private:reexports.bzl", "BuiltinPyInfo") +load(":builders.bzl", "builders") load(":util.bzl", "define_bazel_6_provider") def _check_arg_type(name, required_type, value): @@ -113,3 +116,123 @@ This field is currently unused in Bazel and may go away in the future. """, }, ) + +# The "effective" PyInfo is what the canonical //python:py_info.bzl%PyInfo symbol refers to +_EffectivePyInfo = PyInfo if config.enable_pystar else BuiltinPyInfo + +def PyInfoBuilder(): + # buildifier: disable=uninitialized + self = struct( + _has_py2_only_sources = [False], + _has_py3_only_sources = [False], + _uses_shared_libraries = [False], + build = lambda *a, **k: _PyInfoBuilder_build(self, *a, **k), + build_builtin_py_info = lambda *a, **k: _PyInfoBuilder_build_builtin_py_info(self, *a, **k), + direct_pyc_files = builders.DepsetBuilder(), + get_has_py2_only_sources = lambda *a, **k: _PyInfoBuilder_get_has_py2_only_sources(self, *a, **k), + get_has_py3_only_sources = lambda *a, **k: _PyInfoBuilder_get_has_py3_only_sources(self, *a, **k), + get_uses_shared_libraries = lambda *a, **k: _PyInfoBuilder_get_uses_shared_libraries(self, *a, **k), + imports = builders.DepsetBuilder(), + merge = lambda *a, **k: _PyInfoBuilder_merge(self, *a, **k), + merge_all = lambda *a, **k: _PyInfoBuilder_merge_all(self, *a, **k), + merge_has_py2_only_sources = lambda *a, **k: _PyInfoBuilder_merge_has_py2_only_sources(self, *a, **k), + merge_has_py3_only_sources = lambda *a, **k: _PyInfoBuilder_merge_has_py3_only_sources(self, *a, **k), + merge_target = lambda *a, **k: _PyInfoBuilder_merge_target(self, *a, **k), + merge_targets = lambda *a, **k: _PyInfoBuilder_merge_targets(self, *a, **k), + merge_uses_shared_libraries = lambda *a, **k: _PyInfoBuilder_merge_uses_shared_libraries(self, *a, **k), + set_has_py2_only_sources = lambda *a, **k: _PyInfoBuilder_set_has_py2_only_sources(self, *a, **k), + set_has_py3_only_sources = lambda *a, **k: _PyInfoBuilder_set_has_py3_only_sources(self, *a, **k), + set_uses_shared_libraries = lambda *a, **k: _PyInfoBuilder_set_uses_shared_libraries(self, *a, **k), + transitive_pyc_files = builders.DepsetBuilder(), + transitive_sources = builders.DepsetBuilder(), + ) + return self + +def _PyInfoBuilder_get_has_py3_only_sources(self): + return self._has_py3_only_sources[0] + +def _PyInfoBuilder_get_has_py2_only_sources(self): + return self._has_py2_only_sources[0] + +def _PyInfoBuilder_set_has_py2_only_sources(self, value): + self._has_py2_only_sources[0] = value + return self + +def _PyInfoBuilder_set_has_py3_only_sources(self, value): + self._has_py3_only_sources[0] = value + return self + +def _PyInfoBuilder_merge_has_py2_only_sources(self, value): + self._has_py2_only_sources[0] = self._has_py2_only_sources[0] or value + return self + +def _PyInfoBuilder_merge_has_py3_only_sources(self, value): + self._has_py3_only_sources[0] = self._has_py3_only_sources[0] or value + return self + +def _PyInfoBuilder_merge_uses_shared_libraries(self, value): + self._uses_shared_libraries[0] = self._uses_shared_libraries[0] or value + return self + +def _PyInfoBuilder_get_uses_shared_libraries(self): + return self._uses_shared_libraries[0] + +def _PyInfoBuilder_set_uses_shared_libraries(self, value): + self._uses_shared_libraries[0] = value + return self + +def _PyInfoBuilder_merge(self, *infos): + return self.merge_all(infos) + +def _PyInfoBuilder_merge_all(self, py_infos): + for info in py_infos: + self.imports.add(info.imports) + self.merge_has_py2_only_sources(info.has_py2_only_sources) + self.merge_has_py3_only_sources(info.has_py3_only_sources) + self.merge_uses_shared_libraries(info.uses_shared_libraries) + self.transitive_sources.add(info.transitive_sources) + + # BuiltinPyInfo doesn't have these fields + if hasattr(info, "transitive_pyc_files"): + self.transitive_pyc_files.add(info.transitive_pyc_files) + + return self + +def _PyInfoBuilder_merge_target(self, target): + if PyInfo in target: + self.merge(target[PyInfo]) + elif BuiltinPyInfo in target: + self.merge(target[BuiltinPyInfo]) + return self + +def _PyInfoBuilder_merge_targets(self, targets): + for t in targets: + self.merge_target(t) + return self + +def _PyInfoBuilder_build(self): + if config.enable_pystar: + kwargs = dict( + direct_pyc_files = self.direct_pyc_files.build(), + transitive_pyc_files = self.transitive_pyc_files.build(), + ) + else: + kwargs = {} + + return _EffectivePyInfo( + has_py2_only_sources = self._has_py2_only_sources[0], + has_py3_only_sources = self._has_py3_only_sources[0], + imports = self.imports.build(), + transitive_sources = self.transitive_sources.build(), + uses_shared_libraries = self._uses_shared_libraries[0], + **kwargs + ) + +def _PyInfoBuilder_build_builtin_py_info(self): + return BuiltinPyInfo( + has_py2_only_sources = self._has_py2_only_sources[0], + has_py3_only_sources = self._has_py3_only_sources[0], + imports = self.imports.build(), + transitive_sources = self.transitive_sources.build(), + uses_shared_libraries = self._uses_shared_libraries[0], + ) diff --git a/tests/base_rules/py_info/BUILD.bazel b/tests/base_rules/py_info/BUILD.bazel new file mode 100644 index 0000000000..69f0bdae3f --- /dev/null +++ b/tests/base_rules/py_info/BUILD.bazel @@ -0,0 +1,23 @@ +# Copyright 2024 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(":py_info_tests.bzl", "py_info_test_suite") + +filegroup( + name = "some_runfiles", + data = ["runfile1.txt"], + tags = ["manual"], +) + +py_info_test_suite(name = "py_info_tests") diff --git a/tests/base_rules/py_info/py_info_tests.bzl b/tests/base_rules/py_info/py_info_tests.bzl new file mode 100644 index 0000000000..b64263f6ba --- /dev/null +++ b/tests/base_rules/py_info/py_info_tests.bzl @@ -0,0 +1,198 @@ +# Copyright 2024 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. +"""Tests for py_info.""" + +load("@rules_python_internal//:rules_python_config.bzl", "config") +load("@rules_testing//lib:analysis_test.bzl", "analysis_test") +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("@rules_testing//lib:util.bzl", rt_util = "util") +load("//python:py_info.bzl", "PyInfo") +load("//python/private:py_info.bzl", "PyInfoBuilder") # buildifier: disable=bzl-visibility +load("//python/private:reexports.bzl", "BuiltinPyInfo") # buildifier: disable=bzl-visibility +load("//tests/support:py_info_subject.bzl", "py_info_subject") + +def _provide_py_info_impl(ctx): + kwargs = { + "direct_pyc_files": depset(ctx.files.direct_pyc_files), + "imports": depset(ctx.attr.imports), + "transitive_pyc_files": depset(ctx.files.transitive_pyc_files), + "transitive_sources": depset(ctx.files.transitive_sources), + } + if ctx.attr.has_py2_only_sources != -1: + kwargs["has_py2_only_sources"] = bool(ctx.attr.has_py2_only_sources) + if ctx.attr.has_py3_only_sources != -1: + kwargs["has_py2_only_sources"] = bool(ctx.attr.has_py2_only_sources) + + providers = [] + if config.enable_pystar: + providers.append(PyInfo(**kwargs)) + + # Handle Bazel 6 or if Bazel autoloading is enabled + if not config.enable_pystar or PyInfo != BuiltinPyInfo: + providers.append(BuiltinPyInfo(**{ + k: kwargs[k] + for k in ( + "transitive_sources", + "has_py2_only_sources", + "has_py3_only_sources", + "uses_shared_libraries", + "imports", + ) + if k in kwargs + })) + return providers + +provide_py_info = rule( + implementation = _provide_py_info_impl, + attrs = { + "direct_pyc_files": attr.label_list(allow_files = True), + "has_py2_only_sources": attr.int(default = -1), + "has_py3_only_sources": attr.int(default = -1), + "imports": attr.string_list(), + "transitive_pyc_files": attr.label_list(allow_files = True), + "transitive_sources": attr.label_list(allow_files = True), + }, +) + +_tests = [] + +def _test_py_info_create(name): + rt_util.helper_target( + native.filegroup, + name = name + "_files", + srcs = ["trans.py", "direct.pyc", "trans.pyc"], + ) + analysis_test( + name = name, + target = name + "_files", + impl = _test_py_info_create_impl, + ) + +def _test_py_info_create_impl(env, target): + trans_py, direct_pyc, trans_pyc = target[DefaultInfo].files.to_list() + actual = PyInfo( + has_py2_only_sources = True, + has_py3_only_sources = True, + imports = depset(["import-path"]), + transitive_sources = depset([trans_py]), + uses_shared_libraries = True, + **(dict( + direct_pyc_files = depset([direct_pyc]), + transitive_pyc_files = depset([trans_pyc]), + ) if config.enable_pystar else {}) + ) + + subject = py_info_subject(actual, meta = env.expect.meta) + subject.uses_shared_libraries().equals(True) + subject.has_py2_only_sources().equals(True) + subject.has_py3_only_sources().equals(True) + subject.transitive_sources().contains_exactly(["tests/base_rules/py_info/trans.py"]) + subject.imports().contains_exactly(["import-path"]) + if config.enable_pystar: + subject.direct_pyc_files().contains_exactly(["tests/base_rules/py_info/direct.pyc"]) + subject.transitive_pyc_files().contains_exactly(["tests/base_rules/py_info/trans.pyc"]) + +_tests.append(_test_py_info_create) + +def _test_py_info_builder(name): + rt_util.helper_target( + native.filegroup, + name = name + "_misc", + srcs = ["trans.py", "direct.pyc", "trans.pyc"], + ) + rt_util.helper_target( + provide_py_info, + name = name + "_py1", + transitive_sources = ["py1-trans.py"], + direct_pyc_files = ["py1-direct-pyc.pyc"], + imports = ["py1import"], + transitive_pyc_files = ["py1-trans.pyc"], + ) + rt_util.helper_target( + provide_py_info, + name = name + "_py2", + transitive_sources = ["py2-trans.py"], + direct_pyc_files = ["py2-direct.pyc"], + imports = ["py2import"], + transitive_pyc_files = ["py2-trans.pyc"], + ) + analysis_test( + name = name, + impl = _test_py_info_builder_impl, + targets = { + "misc": name + "_misc", + "py1": name + "_py1", + "py2": name + "_py2", + }, + ) + +def _test_py_info_builder_impl(env, targets): + trans, direct_pyc, trans_pyc = targets.misc[DefaultInfo].files.to_list() + builder = PyInfoBuilder() + builder.direct_pyc_files.add(direct_pyc) + builder.merge_has_py2_only_sources(True) + builder.merge_has_py3_only_sources(True) + builder.imports.add("import-path") + builder.transitive_pyc_files.add(trans_pyc) + builder.transitive_sources.add(trans) + builder.merge_uses_shared_libraries(True) + + builder.merge_target(targets.py1) + builder.merge_targets([targets.py2]) + + def check(actual): + subject = py_info_subject(actual, meta = env.expect.meta) + + subject.uses_shared_libraries().equals(True) + subject.has_py2_only_sources().equals(True) + subject.has_py3_only_sources().equals(True) + + subject.transitive_sources().contains_exactly([ + "tests/base_rules/py_info/trans.py", + "tests/base_rules/py_info/py1-trans.py", + "tests/base_rules/py_info/py2-trans.py", + ]) + subject.imports().contains_exactly([ + "import-path", + "py1import", + "py2import", + ]) + if hasattr(actual, "direct_pyc_files"): + subject.direct_pyc_files().contains_exactly([ + "tests/base_rules/py_info/direct.pyc", + ]) + subject.transitive_pyc_files().contains_exactly([ + "tests/base_rules/py_info/trans.pyc", + "tests/base_rules/py_info/py1-trans.pyc", + "tests/base_rules/py_info/py2-trans.pyc", + ]) + + check(builder.build()) + check(builder.build_builtin_py_info()) + + builder.set_has_py2_only_sources(False) + builder.set_has_py3_only_sources(False) + builder.set_uses_shared_libraries(False) + + env.expect.that_bool(builder.get_has_py2_only_sources()).equals(False) + env.expect.that_bool(builder.get_has_py3_only_sources()).equals(False) + env.expect.that_bool(builder.get_uses_shared_libraries()).equals(False) + +_tests.append(_test_py_info_builder) + +def py_info_test_suite(name): + test_suite( + name = name, + tests = _tests, + ) diff --git a/tests/builders/BUILD.bazel b/tests/builders/BUILD.bazel new file mode 100644 index 0000000000..3ad0c3e80c --- /dev/null +++ b/tests/builders/BUILD.bazel @@ -0,0 +1,17 @@ +# Copyright 2024 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(":builders_tests.bzl", "builders_test_suite") + +builders_test_suite(name = "builders_test_suite") diff --git a/tests/builders/builders_tests.bzl b/tests/builders/builders_tests.bzl new file mode 100644 index 0000000000..f1d596eaff --- /dev/null +++ b/tests/builders/builders_tests.bzl @@ -0,0 +1,116 @@ +# Copyright 2024 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. +"""Tests for py_info.""" + +load("@rules_testing//lib:analysis_test.bzl", "analysis_test") +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("@rules_testing//lib:truth.bzl", "subjects") +load("@rules_testing//lib:util.bzl", rt_util = "util") +load("//python/private:builders.bzl", "builders") # buildifier: disable=bzl-visibility + +_tests = [] + +def _test_depset_builder(name): + rt_util.helper_target( + native.filegroup, + name = name + "_files", + ) + analysis_test( + name = name, + target = name + "_files", + impl = _test_depset_builder_impl, + ) + +def _test_depset_builder_impl(env, target): + _ = target # @unused + builder = builders.DepsetBuilder() + builder.set_order("preorder") + builder.add("one") + builder.add(["two"]) + builder.add(depset(["three"])) + builder.add([depset(["four"])]) + + env.expect.that_str(builder.get_order()).equals("preorder") + + actual = builder.build() + + env.expect.that_collection(actual).contains_exactly([ + "one", + "two", + "three", + "four", + ]).in_order() + +_tests.append(_test_depset_builder) + +def _test_runfiles_builder(name): + rt_util.helper_target( + native.filegroup, + name = name + "_files", + srcs = ["f1.txt", "f2.txt", "f3.txt", "f4.txt", "f5.txt"], + ) + rt_util.helper_target( + native.filegroup, + name = name + "_runfiles", + data = ["runfile.txt"], + ) + analysis_test( + name = name, + impl = _test_runfiles_builder_impl, + targets = { + "files": name + "_files", + "runfiles": name + "_runfiles", + }, + ) + +def _test_runfiles_builder_impl(env, targets): + ctx = env.ctx + + f1, f2, f3, f4, f5 = targets.files[DefaultInfo].files.to_list() + builder = builders.RunfilesBuilder() + builder.add(f1) + builder.add([f2]) + builder.add(depset([f3])) + + rf1 = ctx.runfiles([f4]) + rf2 = ctx.runfiles([f5]) + builder.add(rf1) + builder.add([rf2]) + + builder.add_targets([targets.runfiles]) + + builder.root_symlinks["root_link"] = f1 + builder.symlinks["regular_link"] = f1 + + actual = builder.build(ctx) + + subject = subjects.runfiles(actual, meta = env.expect.meta) + subject.contains_exactly([ + "root_link", + "{workspace}/regular_link", + "{workspace}/tests/builders/f1.txt", + "{workspace}/tests/builders/f2.txt", + "{workspace}/tests/builders/f3.txt", + "{workspace}/tests/builders/f4.txt", + "{workspace}/tests/builders/f5.txt", + "{workspace}/tests/builders/runfile.txt", + ]) + +_tests.append(_test_runfiles_builder) + +def builders_test_suite(name): + test_suite( + name = name, + tests = _tests, + )