1# Copyright 2022 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"""Implementation for Bazel Python executable.""" 15 16load("@bazel_skylib//lib:dicts.bzl", "dicts") 17load("@bazel_skylib//lib:paths.bzl", "paths") 18load(":attributes.bzl", "IMPORTS_ATTRS") 19load( 20 ":common.bzl", 21 "create_binary_semantics_struct", 22 "create_cc_details_struct", 23 "create_executable_result_struct", 24 "target_platform_has_any_constraint", 25 "union_attrs", 26) 27load(":common_bazel.bzl", "collect_cc_info", "get_imports", "maybe_precompile") 28load(":flags.bzl", "BootstrapImplFlag") 29load( 30 ":py_executable.bzl", 31 "create_base_executable_rule", 32 "py_executable_base_impl", 33) 34load(":py_internal.bzl", "py_internal") 35load(":py_runtime_info.bzl", "DEFAULT_STUB_SHEBANG") 36load(":toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE") 37 38_py_builtins = py_internal 39_EXTERNAL_PATH_PREFIX = "external" 40_ZIP_RUNFILES_DIRECTORY_NAME = "runfiles" 41 42BAZEL_EXECUTABLE_ATTRS = union_attrs( 43 IMPORTS_ATTRS, 44 { 45 "legacy_create_init": attr.int( 46 default = -1, 47 values = [-1, 0, 1], 48 doc = """\ 49Whether to implicitly create empty `__init__.py` files in the runfiles tree. 50These are created in every directory containing Python source code or shared 51libraries, and every parent directory of those directories, excluding the repo 52root directory. The default, `-1` (auto), means true unless 53`--incompatible_default_to_explicit_init_py` is used. If false, the user is 54responsible for creating (possibly empty) `__init__.py` files and adding them to 55the `srcs` of Python targets as required. 56 """, 57 ), 58 "_bootstrap_template": attr.label( 59 allow_single_file = True, 60 default = "@bazel_tools//tools/python:python_bootstrap_template.txt", 61 ), 62 "_launcher": attr.label( 63 cfg = "target", 64 # NOTE: This is an executable, but is only used for Windows. It 65 # can't have executable=True because the backing target is an 66 # empty target for other platforms. 67 default = "//tools/launcher:launcher", 68 ), 69 "_py_interpreter": attr.label( 70 # The configuration_field args are validated when called; 71 # we use the precense of py_internal to indicate this Bazel 72 # build has that fragment and name. 73 default = configuration_field( 74 fragment = "bazel_py", 75 name = "python_top", 76 ) if py_internal else None, 77 ), 78 # TODO: This appears to be vestigial. It's only added because 79 # GraphlessQueryTest.testLabelsOperator relies on it to test for 80 # query behavior of implicit dependencies. 81 "_py_toolchain_type": attr.label( 82 default = TARGET_TOOLCHAIN_TYPE, 83 ), 84 "_python_version_flag": attr.label( 85 default = "//python/config_settings:python_version", 86 ), 87 "_windows_launcher_maker": attr.label( 88 default = "@bazel_tools//tools/launcher:launcher_maker", 89 cfg = "exec", 90 executable = True, 91 ), 92 "_zipper": attr.label( 93 cfg = "exec", 94 executable = True, 95 default = "@bazel_tools//tools/zip:zipper", 96 ), 97 }, 98) 99 100def create_executable_rule(*, attrs, **kwargs): 101 return create_base_executable_rule( 102 attrs = dicts.add(BAZEL_EXECUTABLE_ATTRS, attrs), 103 fragments = ["py", "bazel_py"], 104 **kwargs 105 ) 106 107def py_executable_bazel_impl(ctx, *, is_test, inherited_environment): 108 """Common code for executables for Bazel.""" 109 return py_executable_base_impl( 110 ctx = ctx, 111 semantics = create_binary_semantics_bazel(), 112 is_test = is_test, 113 inherited_environment = inherited_environment, 114 ) 115 116def create_binary_semantics_bazel(): 117 return create_binary_semantics_struct( 118 # keep-sorted start 119 create_executable = _create_executable, 120 get_cc_details_for_binary = _get_cc_details_for_binary, 121 get_central_uncachable_version_file = lambda ctx: None, 122 get_coverage_deps = _get_coverage_deps, 123 get_debugger_deps = _get_debugger_deps, 124 get_extra_common_runfiles_for_binary = lambda ctx: ctx.runfiles(), 125 get_extra_providers = _get_extra_providers, 126 get_extra_write_build_data_env = lambda ctx: {}, 127 get_imports = get_imports, 128 get_interpreter_path = _get_interpreter_path, 129 get_native_deps_dso_name = _get_native_deps_dso_name, 130 get_native_deps_user_link_flags = _get_native_deps_user_link_flags, 131 get_stamp_flag = _get_stamp_flag, 132 maybe_precompile = maybe_precompile, 133 should_build_native_deps_dso = lambda ctx: False, 134 should_create_init_files = _should_create_init_files, 135 should_include_build_data = lambda ctx: False, 136 # keep-sorted end 137 ) 138 139def _get_coverage_deps(ctx, runtime_details): 140 _ = ctx, runtime_details # @unused 141 return [] 142 143def _get_debugger_deps(ctx, runtime_details): 144 _ = ctx, runtime_details # @unused 145 return [] 146 147def _get_extra_providers(ctx, main_py, runtime_details): 148 _ = ctx, main_py, runtime_details # @unused 149 return [] 150 151def _get_stamp_flag(ctx): 152 # NOTE: Undocumented API; private to builtins 153 return ctx.configuration.stamp_binaries 154 155def _should_create_init_files(ctx): 156 if ctx.attr.legacy_create_init == -1: 157 return not ctx.fragments.py.default_to_explicit_init_py 158 else: 159 return bool(ctx.attr.legacy_create_init) 160 161def _create_executable( 162 ctx, 163 *, 164 executable, 165 main_py, 166 imports, 167 is_test, 168 runtime_details, 169 cc_details, 170 native_deps_details, 171 runfiles_details): 172 _ = is_test, cc_details, native_deps_details # @unused 173 174 is_windows = target_platform_has_any_constraint(ctx, ctx.attr._windows_constraints) 175 176 if is_windows: 177 if not executable.extension == "exe": 178 fail("Should not happen: somehow we are generating a non-.exe file on windows") 179 base_executable_name = executable.basename[0:-4] 180 else: 181 base_executable_name = executable.basename 182 183 venv = None 184 185 # The check for stage2_bootstrap_template is to support legacy 186 # BuiltinPyRuntimeInfo providers, which is likely to come from 187 # @bazel_tools//tools/python:autodetecting_toolchain, the toolchain used 188 # for workspace builds when no rules_python toolchain is configured. 189 if (BootstrapImplFlag.get_value(ctx) == BootstrapImplFlag.SCRIPT and 190 runtime_details.effective_runtime and 191 hasattr(runtime_details.effective_runtime, "stage2_bootstrap_template")): 192 venv = _create_venv( 193 ctx, 194 output_prefix = base_executable_name, 195 imports = imports, 196 runtime_details = runtime_details, 197 ) 198 199 stage2_bootstrap = _create_stage2_bootstrap( 200 ctx, 201 output_prefix = base_executable_name, 202 output_sibling = executable, 203 main_py = main_py, 204 imports = imports, 205 runtime_details = runtime_details, 206 ) 207 extra_runfiles = ctx.runfiles([stage2_bootstrap] + venv.files_without_interpreter) 208 zip_main = _create_zip_main( 209 ctx, 210 stage2_bootstrap = stage2_bootstrap, 211 runtime_details = runtime_details, 212 venv = venv, 213 ) 214 else: 215 stage2_bootstrap = None 216 extra_runfiles = ctx.runfiles() 217 zip_main = ctx.actions.declare_file(base_executable_name + ".temp", sibling = executable) 218 _create_stage1_bootstrap( 219 ctx, 220 output = zip_main, 221 main_py = main_py, 222 imports = imports, 223 is_for_zip = True, 224 runtime_details = runtime_details, 225 ) 226 227 zip_file = ctx.actions.declare_file(base_executable_name + ".zip", sibling = executable) 228 _create_zip_file( 229 ctx, 230 output = zip_file, 231 original_nonzip_executable = executable, 232 zip_main = zip_main, 233 runfiles = runfiles_details.default_runfiles.merge(extra_runfiles), 234 ) 235 236 extra_files_to_build = [] 237 238 # NOTE: --build_python_zip defaults to true on Windows 239 build_zip_enabled = ctx.fragments.py.build_python_zip 240 241 # When --build_python_zip is enabled, then the zip file becomes 242 # one of the default outputs. 243 if build_zip_enabled: 244 extra_files_to_build.append(zip_file) 245 246 # The logic here is a bit convoluted. Essentially, there are 3 types of 247 # executables produced: 248 # 1. (non-Windows) A bootstrap template based program. 249 # 2. (non-Windows) A self-executable zip file of a bootstrap template based program. 250 # 3. (Windows) A native Windows executable that finds and launches 251 # the actual underlying Bazel program (one of the above). Note that 252 # it implicitly assumes one of the above is located next to it, and 253 # that --build_python_zip defaults to true for Windows. 254 255 should_create_executable_zip = False 256 bootstrap_output = None 257 if not is_windows: 258 if build_zip_enabled: 259 should_create_executable_zip = True 260 else: 261 bootstrap_output = executable 262 else: 263 _create_windows_exe_launcher( 264 ctx, 265 output = executable, 266 use_zip_file = build_zip_enabled, 267 python_binary_path = runtime_details.executable_interpreter_path, 268 ) 269 if not build_zip_enabled: 270 # On Windows, the main executable has an "exe" extension, so 271 # here we re-use the un-extensioned name for the bootstrap output. 272 bootstrap_output = ctx.actions.declare_file(base_executable_name) 273 274 # The launcher looks for the non-zip executable next to 275 # itself, so add it to the default outputs. 276 extra_files_to_build.append(bootstrap_output) 277 278 if should_create_executable_zip: 279 if bootstrap_output != None: 280 fail("Should not occur: bootstrap_output should not be used " + 281 "when creating an executable zip") 282 _create_executable_zip_file( 283 ctx, 284 output = executable, 285 zip_file = zip_file, 286 stage2_bootstrap = stage2_bootstrap, 287 runtime_details = runtime_details, 288 venv = venv, 289 ) 290 elif bootstrap_output: 291 _create_stage1_bootstrap( 292 ctx, 293 output = bootstrap_output, 294 stage2_bootstrap = stage2_bootstrap, 295 runtime_details = runtime_details, 296 is_for_zip = False, 297 imports = imports, 298 main_py = main_py, 299 venv = venv, 300 ) 301 else: 302 # Otherwise, this should be the Windows case of launcher + zip. 303 # Double check this just to make sure. 304 if not is_windows or not build_zip_enabled: 305 fail(("Should not occur: The non-executable-zip and " + 306 "non-bootstrap-template case should have windows and zip " + 307 "both true, but got " + 308 "is_windows={is_windows} " + 309 "build_zip_enabled={build_zip_enabled}").format( 310 is_windows = is_windows, 311 build_zip_enabled = build_zip_enabled, 312 )) 313 314 # The interpreter is added this late in the process so that it isn't 315 # added to the zipped files. 316 if venv: 317 extra_runfiles = extra_runfiles.merge(ctx.runfiles([venv.interpreter])) 318 return create_executable_result_struct( 319 extra_files_to_build = depset(extra_files_to_build), 320 output_groups = {"python_zip_file": depset([zip_file])}, 321 extra_runfiles = extra_runfiles, 322 ) 323 324def _create_zip_main(ctx, *, stage2_bootstrap, runtime_details, venv): 325 python_binary = _runfiles_root_path(ctx, venv.interpreter.short_path) 326 python_binary_actual = venv.interpreter_actual_path 327 328 # The location of this file doesn't really matter. It's added to 329 # the zip file as the top-level __main__.py file and not included 330 # elsewhere. 331 output = ctx.actions.declare_file(ctx.label.name + "_zip__main__.py") 332 ctx.actions.expand_template( 333 template = runtime_details.effective_runtime.zip_main_template, 334 output = output, 335 substitutions = { 336 "%python_binary%": python_binary, 337 "%python_binary_actual%": python_binary_actual, 338 "%stage2_bootstrap%": "{}/{}".format( 339 ctx.workspace_name, 340 stage2_bootstrap.short_path, 341 ), 342 "%workspace_name%": ctx.workspace_name, 343 }, 344 ) 345 return output 346 347def relative_path(from_, to): 348 """Compute a relative path from one path to another. 349 350 Args: 351 from_: {type}`str` the starting directory. Note that it should be 352 a directory because relative-symlinks are relative to the 353 directory the symlink resides in. 354 to: {type}`str` the path that `from_` wants to point to 355 356 Returns: 357 {type}`str` a relative path 358 """ 359 from_parts = from_.split("/") 360 to_parts = to.split("/") 361 362 # Strip common leading parts from both paths 363 n = min(len(from_parts), len(to_parts)) 364 for _ in range(n): 365 if from_parts[0] == to_parts[0]: 366 from_parts.pop(0) 367 to_parts.pop(0) 368 else: 369 break 370 371 # Impossible to compute a relative path without knowing what ".." is 372 if from_parts and from_parts[0] == "..": 373 fail("cannot compute relative path from '%s' to '%s'", from_, to) 374 375 parts = ([".."] * len(from_parts)) + to_parts 376 return paths.join(*parts) 377 378# Create a venv the executable can use. 379# For venv details and the venv startup process, see: 380# * https://docs.python.org/3/library/venv.html 381# * https://snarky.ca/how-virtual-environments-work/ 382# * https://github.com/python/cpython/blob/main/Modules/getpath.py 383# * https://github.com/python/cpython/blob/main/Lib/site.py 384def _create_venv(ctx, output_prefix, imports, runtime_details): 385 venv = "_{}.venv".format(output_prefix.lstrip("_")) 386 387 # The pyvenv.cfg file must be present to trigger the venv site hooks. 388 # Because it's paths are expected to be absolute paths, we can't reliably 389 # put much in it. See https://github.com/python/cpython/issues/83650 390 pyvenv_cfg = ctx.actions.declare_file("{}/pyvenv.cfg".format(venv)) 391 ctx.actions.write(pyvenv_cfg, "") 392 393 runtime = runtime_details.effective_runtime 394 if runtime.interpreter: 395 py_exe_basename = paths.basename(runtime.interpreter.short_path) 396 397 # Even though ctx.actions.symlink() is used, using 398 # declare_symlink() is required to ensure that the resulting file 399 # in runfiles is always a symlink. An RBE implementation, for example, 400 # may choose to write what symlink() points to instead. 401 interpreter = ctx.actions.declare_symlink("{}/bin/{}".format(venv, py_exe_basename)) 402 403 interpreter_actual_path = _runfiles_root_path(ctx, runtime.interpreter.short_path) 404 rel_path = relative_path( 405 # dirname is necessary because a relative symlink is relative to 406 # the directory the symlink resides within. 407 from_ = paths.dirname(_runfiles_root_path(ctx, interpreter.short_path)), 408 to = interpreter_actual_path, 409 ) 410 411 ctx.actions.symlink(output = interpreter, target_path = rel_path) 412 else: 413 py_exe_basename = paths.basename(runtime.interpreter_path) 414 interpreter = ctx.actions.declare_symlink("{}/bin/{}".format(venv, py_exe_basename)) 415 ctx.actions.symlink(output = interpreter, target_path = runtime.interpreter_path) 416 interpreter_actual_path = runtime.interpreter_path 417 418 if runtime.interpreter_version_info: 419 version = "{}.{}".format( 420 runtime.interpreter_version_info.major, 421 runtime.interpreter_version_info.minor, 422 ) 423 else: 424 version_flag = ctx.attr._python_version_flag[config_common.FeatureFlagInfo].value 425 version_flag_parts = version_flag.split(".")[0:2] 426 version = "{}.{}".format(*version_flag_parts) 427 428 # See site.py logic: free-threaded builds append "t" to the venv lib dir name 429 if "t" in runtime.abi_flags: 430 version += "t" 431 432 site_packages = "{}/lib/python{}/site-packages".format(venv, version) 433 pth = ctx.actions.declare_file("{}/bazel.pth".format(site_packages)) 434 ctx.actions.write(pth, "import _bazel_site_init\n") 435 436 site_init = ctx.actions.declare_file("{}/_bazel_site_init.py".format(site_packages)) 437 computed_subs = ctx.actions.template_dict() 438 computed_subs.add_joined("%imports%", imports, join_with = ":", map_each = _map_each_identity) 439 ctx.actions.expand_template( 440 template = runtime.site_init_template, 441 output = site_init, 442 substitutions = { 443 "%import_all%": "True" if ctx.fragments.bazel_py.python_import_all_repositories else "False", 444 "%site_init_runfiles_path%": "{}/{}".format(ctx.workspace_name, site_init.short_path), 445 "%workspace_name%": ctx.workspace_name, 446 }, 447 computed_substitutions = computed_subs, 448 ) 449 450 return struct( 451 interpreter = interpreter, 452 # Runfiles root relative path or absolute path 453 interpreter_actual_path = interpreter_actual_path, 454 files_without_interpreter = [pyvenv_cfg, pth, site_init], 455 ) 456 457def _map_each_identity(v): 458 return v 459 460def _create_stage2_bootstrap( 461 ctx, 462 *, 463 output_prefix, 464 output_sibling, 465 main_py, 466 imports, 467 runtime_details): 468 output = ctx.actions.declare_file( 469 # Prepend with underscore to prevent pytest from trying to 470 # process the bootstrap for files starting with `test_` 471 "_{}_stage2_bootstrap.py".format(output_prefix), 472 sibling = output_sibling, 473 ) 474 runtime = runtime_details.effective_runtime 475 if (ctx.configuration.coverage_enabled and 476 runtime and 477 runtime.coverage_tool): 478 coverage_tool_runfiles_path = "{}/{}".format( 479 ctx.workspace_name, 480 runtime.coverage_tool.short_path, 481 ) 482 else: 483 coverage_tool_runfiles_path = "" 484 485 template = runtime.stage2_bootstrap_template 486 487 ctx.actions.expand_template( 488 template = template, 489 output = output, 490 substitutions = { 491 "%coverage_tool%": coverage_tool_runfiles_path, 492 "%import_all%": "True" if ctx.fragments.bazel_py.python_import_all_repositories else "False", 493 "%imports%": ":".join(imports.to_list()), 494 "%main%": "{}/{}".format(ctx.workspace_name, main_py.short_path), 495 "%target%": str(ctx.label), 496 "%workspace_name%": ctx.workspace_name, 497 }, 498 is_executable = True, 499 ) 500 return output 501 502def _runfiles_root_path(ctx, short_path): 503 """Compute a runfiles-root relative path from `File.short_path` 504 505 Args: 506 ctx: current target ctx 507 short_path: str, a main-repo relative path from `File.short_path` 508 509 Returns: 510 {type}`str`, a runflies-root relative path 511 """ 512 513 # The ../ comes from short_path is for files in other repos. 514 if short_path.startswith("../"): 515 return short_path[3:] 516 else: 517 return "{}/{}".format(ctx.workspace_name, short_path) 518 519def _create_stage1_bootstrap( 520 ctx, 521 *, 522 output, 523 main_py = None, 524 stage2_bootstrap = None, 525 imports = None, 526 is_for_zip, 527 runtime_details, 528 venv = None): 529 runtime = runtime_details.effective_runtime 530 531 if venv: 532 python_binary_path = _runfiles_root_path(ctx, venv.interpreter.short_path) 533 else: 534 python_binary_path = runtime_details.executable_interpreter_path 535 536 if is_for_zip and venv: 537 python_binary_actual = venv.interpreter_actual_path 538 else: 539 python_binary_actual = "" 540 541 subs = { 542 "%is_zipfile%": "1" if is_for_zip else "0", 543 "%python_binary%": python_binary_path, 544 "%python_binary_actual%": python_binary_actual, 545 "%target%": str(ctx.label), 546 "%workspace_name%": ctx.workspace_name, 547 } 548 549 if stage2_bootstrap: 550 subs["%stage2_bootstrap%"] = "{}/{}".format( 551 ctx.workspace_name, 552 stage2_bootstrap.short_path, 553 ) 554 template = runtime.bootstrap_template 555 subs["%shebang%"] = runtime.stub_shebang 556 else: 557 if (ctx.configuration.coverage_enabled and 558 runtime and 559 runtime.coverage_tool): 560 coverage_tool_runfiles_path = "{}/{}".format( 561 ctx.workspace_name, 562 runtime.coverage_tool.short_path, 563 ) 564 else: 565 coverage_tool_runfiles_path = "" 566 if runtime: 567 subs["%shebang%"] = runtime.stub_shebang 568 template = runtime.bootstrap_template 569 else: 570 subs["%shebang%"] = DEFAULT_STUB_SHEBANG 571 template = ctx.file._bootstrap_template 572 573 subs["%coverage_tool%"] = coverage_tool_runfiles_path 574 subs["%import_all%"] = ("True" if ctx.fragments.bazel_py.python_import_all_repositories else "False") 575 subs["%imports%"] = ":".join(imports.to_list()) 576 subs["%main%"] = "{}/{}".format(ctx.workspace_name, main_py.short_path) 577 578 ctx.actions.expand_template( 579 template = template, 580 output = output, 581 substitutions = subs, 582 ) 583 584def _create_windows_exe_launcher( 585 ctx, 586 *, 587 output, 588 python_binary_path, 589 use_zip_file): 590 launch_info = ctx.actions.args() 591 launch_info.use_param_file("%s", use_always = True) 592 launch_info.set_param_file_format("multiline") 593 launch_info.add("binary_type=Python") 594 launch_info.add(ctx.workspace_name, format = "workspace_name=%s") 595 launch_info.add( 596 "1" if py_internal.runfiles_enabled(ctx) else "0", 597 format = "symlink_runfiles_enabled=%s", 598 ) 599 launch_info.add(python_binary_path, format = "python_bin_path=%s") 600 launch_info.add("1" if use_zip_file else "0", format = "use_zip_file=%s") 601 602 launcher = ctx.attr._launcher[DefaultInfo].files_to_run.executable 603 ctx.actions.run( 604 executable = ctx.executable._windows_launcher_maker, 605 arguments = [launcher.path, launch_info, output.path], 606 inputs = [launcher], 607 outputs = [output], 608 mnemonic = "PyBuildLauncher", 609 progress_message = "Creating launcher for %{label}", 610 # Needed to inherit PATH when using non-MSVC compilers like MinGW 611 use_default_shell_env = True, 612 ) 613 614def _create_zip_file(ctx, *, output, original_nonzip_executable, zip_main, runfiles): 615 """Create a Python zipapp (zip with __main__.py entry point).""" 616 workspace_name = ctx.workspace_name 617 legacy_external_runfiles = _py_builtins.get_legacy_external_runfiles(ctx) 618 619 manifest = ctx.actions.args() 620 manifest.use_param_file("@%s", use_always = True) 621 manifest.set_param_file_format("multiline") 622 623 manifest.add("__main__.py={}".format(zip_main.path)) 624 manifest.add("__init__.py=") 625 manifest.add( 626 "{}=".format( 627 _get_zip_runfiles_path("__init__.py", workspace_name, legacy_external_runfiles), 628 ), 629 ) 630 for path in runfiles.empty_filenames.to_list(): 631 manifest.add("{}=".format(_get_zip_runfiles_path(path, workspace_name, legacy_external_runfiles))) 632 633 def map_zip_runfiles(file): 634 if file != original_nonzip_executable and file != output: 635 return "{}={}".format( 636 _get_zip_runfiles_path(file.short_path, workspace_name, legacy_external_runfiles), 637 file.path, 638 ) 639 else: 640 return None 641 642 manifest.add_all(runfiles.files, map_each = map_zip_runfiles, allow_closure = True) 643 644 inputs = [zip_main] 645 if _py_builtins.is_bzlmod_enabled(ctx): 646 zip_repo_mapping_manifest = ctx.actions.declare_file( 647 output.basename + ".repo_mapping", 648 sibling = output, 649 ) 650 _py_builtins.create_repo_mapping_manifest( 651 ctx = ctx, 652 runfiles = runfiles, 653 output = zip_repo_mapping_manifest, 654 ) 655 manifest.add("{}/_repo_mapping={}".format( 656 _ZIP_RUNFILES_DIRECTORY_NAME, 657 zip_repo_mapping_manifest.path, 658 )) 659 inputs.append(zip_repo_mapping_manifest) 660 661 for artifact in runfiles.files.to_list(): 662 # Don't include the original executable because it isn't used by the 663 # zip file, so no need to build it for the action. 664 # Don't include the zipfile itself because it's an output. 665 if artifact != original_nonzip_executable and artifact != output: 666 inputs.append(artifact) 667 668 zip_cli_args = ctx.actions.args() 669 zip_cli_args.add("cC") 670 zip_cli_args.add(output) 671 672 ctx.actions.run( 673 executable = ctx.executable._zipper, 674 arguments = [zip_cli_args, manifest], 675 inputs = depset(inputs), 676 outputs = [output], 677 use_default_shell_env = True, 678 mnemonic = "PythonZipper", 679 progress_message = "Building Python zip: %{label}", 680 ) 681 682def _get_zip_runfiles_path(path, workspace_name, legacy_external_runfiles): 683 if legacy_external_runfiles and path.startswith(_EXTERNAL_PATH_PREFIX): 684 zip_runfiles_path = paths.relativize(path, _EXTERNAL_PATH_PREFIX) 685 else: 686 # NOTE: External runfiles (artifacts in other repos) will have a leading 687 # path component of "../" so that they refer outside the main workspace 688 # directory and into the runfiles root. By normalizing, we simplify e.g. 689 # "workspace/../foo/bar" to simply "foo/bar". 690 zip_runfiles_path = paths.normalize("{}/{}".format(workspace_name, path)) 691 return "{}/{}".format(_ZIP_RUNFILES_DIRECTORY_NAME, zip_runfiles_path) 692 693def _create_executable_zip_file( 694 ctx, 695 *, 696 output, 697 zip_file, 698 stage2_bootstrap, 699 runtime_details, 700 venv): 701 prelude = ctx.actions.declare_file( 702 "{}_zip_prelude.sh".format(output.basename), 703 sibling = output, 704 ) 705 if stage2_bootstrap: 706 _create_stage1_bootstrap( 707 ctx, 708 output = prelude, 709 stage2_bootstrap = stage2_bootstrap, 710 runtime_details = runtime_details, 711 is_for_zip = True, 712 venv = venv, 713 ) 714 else: 715 ctx.actions.write(prelude, "#!/usr/bin/env python3\n") 716 717 ctx.actions.run_shell( 718 command = "cat {prelude} {zip} > {output}".format( 719 prelude = prelude.path, 720 zip = zip_file.path, 721 output = output.path, 722 ), 723 inputs = [prelude, zip_file], 724 outputs = [output], 725 use_default_shell_env = True, 726 mnemonic = "PyBuildExecutableZip", 727 progress_message = "Build Python zip executable: %{label}", 728 ) 729 730def _get_cc_details_for_binary(ctx, extra_deps): 731 cc_info = collect_cc_info(ctx, extra_deps = extra_deps) 732 return create_cc_details_struct( 733 cc_info_for_propagating = cc_info, 734 cc_info_for_self_link = cc_info, 735 cc_info_with_extra_link_time_libraries = None, 736 extra_runfiles = ctx.runfiles(), 737 # Though the rules require the CcToolchain, it isn't actually used. 738 cc_toolchain = None, 739 feature_config = None, 740 ) 741 742def _get_interpreter_path(ctx, *, runtime, flag_interpreter_path): 743 if runtime: 744 if runtime.interpreter_path: 745 interpreter_path = runtime.interpreter_path 746 else: 747 interpreter_path = "{}/{}".format( 748 ctx.workspace_name, 749 runtime.interpreter.short_path, 750 ) 751 752 # NOTE: External runfiles (artifacts in other repos) will have a 753 # leading path component of "../" so that they refer outside the 754 # main workspace directory and into the runfiles root. By 755 # normalizing, we simplify e.g. "workspace/../foo/bar" to simply 756 # "foo/bar" 757 interpreter_path = paths.normalize(interpreter_path) 758 759 elif flag_interpreter_path: 760 interpreter_path = flag_interpreter_path 761 else: 762 fail("Unable to determine interpreter path") 763 764 return interpreter_path 765 766def _get_native_deps_dso_name(ctx): 767 _ = ctx # @unused 768 fail("Building native deps DSO not supported.") 769 770def _get_native_deps_user_link_flags(ctx): 771 _ = ctx # @unused 772 fail("Building native deps DSO not supported.") 773