1# Copyright 2024 The Bazel Authors. All rights reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14"""Run a py_binary with altered config settings in an sh_test. 15 16This facilitates verify running binaries with different configuration settings 17without the overhead of a bazel-in-bazel integration test. 18""" 19 20load("//python:py_binary.bzl", "py_binary") 21load("//python:py_test.bzl", "py_test") 22load("//python/private:toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE") # buildifier: disable=bzl-visibility 23load("//tests/support:support.bzl", "VISIBLE_FOR_TESTING") 24 25def _perform_transition_impl(input_settings, attr): 26 settings = dict(input_settings) 27 settings[VISIBLE_FOR_TESTING] = True 28 settings["//command_line_option:build_python_zip"] = attr.build_python_zip 29 if attr.bootstrap_impl: 30 settings["//python/config_settings:bootstrap_impl"] = attr.bootstrap_impl 31 if attr.extra_toolchains: 32 settings["//command_line_option:extra_toolchains"] = attr.extra_toolchains 33 if attr.python_version: 34 settings["//python/config_settings:python_version"] = attr.python_version 35 return settings 36 37_perform_transition = transition( 38 implementation = _perform_transition_impl, 39 inputs = [ 40 "//python/config_settings:bootstrap_impl", 41 "//command_line_option:extra_toolchains", 42 "//python/config_settings:python_version", 43 ], 44 outputs = [ 45 "//command_line_option:build_python_zip", 46 "//command_line_option:extra_toolchains", 47 "//python/config_settings:bootstrap_impl", 48 "//python/config_settings:python_version", 49 VISIBLE_FOR_TESTING, 50 ], 51) 52 53def _py_reconfig_impl(ctx): 54 default_info = ctx.attr.target[DefaultInfo] 55 exe_ext = default_info.files_to_run.executable.extension 56 if exe_ext: 57 exe_ext = "." + exe_ext 58 exe_name = ctx.label.name + exe_ext 59 60 executable = ctx.actions.declare_file(exe_name) 61 ctx.actions.symlink(output = executable, target_file = default_info.files_to_run.executable) 62 63 default_outputs = [executable] 64 65 # todo: could probably check target.owner vs src.owner to check if it should 66 # be symlinked or included as-is 67 # For simplicity of implementation, we're assuming the target being run is 68 # py_binary-like. In order for Windows to work, we need to make sure the 69 # file that the .exe launcher runs (the .zip or underlying non-exe 70 # executable) is a sibling of the .exe file with the same base name. 71 for src in default_info.files.to_list(): 72 if src.extension in ("", "zip"): 73 ext = ("." if src.extension else "") + src.extension 74 output = ctx.actions.declare_file(ctx.label.name + ext) 75 ctx.actions.symlink(output = output, target_file = src) 76 default_outputs.append(output) 77 78 return [ 79 DefaultInfo( 80 executable = executable, 81 files = depset(default_outputs), 82 # On windows, the other default outputs must also be included 83 # in runfiles so the exe launcher can find the backing file. 84 runfiles = ctx.runfiles(default_outputs).merge( 85 default_info.default_runfiles, 86 ), 87 ), 88 testing.TestEnvironment( 89 environment = ctx.attr.env, 90 ), 91 ] 92 93def _make_reconfig_rule(**kwargs): 94 attrs = { 95 "bootstrap_impl": attr.string(), 96 "build_python_zip": attr.string(default = "auto"), 97 "env": attr.string_dict(), 98 "extra_toolchains": attr.string_list( 99 doc = """ 100Value for the --extra_toolchains flag. 101 102NOTE: You'll likely have to also specify //tests/support/cc_toolchains:all (or some CC toolchain) 103to make the RBE presubmits happy, which disable auto-detection of a CC 104toolchain. 105""", 106 ), 107 "python_version": attr.string(), 108 "target": attr.label(executable = True, cfg = "target"), 109 "_allowlist_function_transition": attr.label( 110 default = "@bazel_tools//tools/allowlists/function_transition_allowlist", 111 ), 112 } 113 return rule( 114 implementation = _py_reconfig_impl, 115 attrs = attrs, 116 cfg = _perform_transition, 117 **kwargs 118 ) 119 120_py_reconfig_binary = _make_reconfig_rule(executable = True) 121 122_py_reconfig_test = _make_reconfig_rule(test = True) 123 124def py_reconfig_test(*, name, **kwargs): 125 """Create a py_test with customized build settings for testing. 126 127 Args: 128 name: str, name of teset target. 129 **kwargs: kwargs to pass along to _py_reconfig_test and py_test. 130 """ 131 reconfig_kwargs = {} 132 reconfig_kwargs["bootstrap_impl"] = kwargs.pop("bootstrap_impl", None) 133 reconfig_kwargs["extra_toolchains"] = kwargs.pop("extra_toolchains", None) 134 reconfig_kwargs["python_version"] = kwargs.pop("python_version", None) 135 reconfig_kwargs["env"] = kwargs.get("env") 136 reconfig_kwargs["target_compatible_with"] = kwargs.get("target_compatible_with") 137 138 inner_name = "_{}_inner" + name 139 _py_reconfig_test( 140 name = name, 141 target = inner_name, 142 **reconfig_kwargs 143 ) 144 py_test( 145 name = inner_name, 146 tags = ["manual"], 147 **kwargs 148 ) 149 150def sh_py_run_test(*, name, sh_src, py_src, **kwargs): 151 bin_name = "_{}_bin".format(name) 152 native.sh_test( 153 name = name, 154 srcs = [sh_src], 155 data = [bin_name], 156 deps = [ 157 "@bazel_tools//tools/bash/runfiles", 158 ], 159 env = { 160 "BIN_RLOCATION": "$(rlocationpath {})".format(bin_name), 161 }, 162 ) 163 164 _py_reconfig_binary( 165 name = bin_name, 166 tags = ["manual"], 167 target = "_{}_plain_bin".format(name), 168 **kwargs 169 ) 170 171 py_binary( 172 name = "_{}_plain_bin".format(name), 173 srcs = [py_src], 174 main = py_src, 175 tags = ["manual"], 176 ) 177 178def _current_build_settings_impl(ctx): 179 info = ctx.actions.declare_file(ctx.label.name + ".json") 180 toolchain = ctx.toolchains[TARGET_TOOLCHAIN_TYPE] 181 runtime = toolchain.py3_runtime 182 files = [info] 183 ctx.actions.write( 184 output = info, 185 content = json.encode({ 186 "interpreter": { 187 "short_path": runtime.interpreter.short_path if runtime.interpreter else None, 188 }, 189 "interpreter_path": runtime.interpreter_path, 190 "toolchain_label": str(getattr(toolchain, "toolchain_label", None)), 191 }), 192 ) 193 return [DefaultInfo( 194 files = depset(files), 195 )] 196 197current_build_settings = rule( 198 doc = """ 199Writes information about the current build config to JSON for testing. 200 201This is so tests can verify information about the build config used for them. 202""", 203 implementation = _current_build_settings_impl, 204 toolchains = [ 205 TARGET_TOOLCHAIN_TYPE, 206 ], 207) 208