• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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