1+ "Create python zip file https://peps.python.org/pep-0441/ (PEX)"
2+
3+ load ("@rules_python//python:defs.bzl" , "PyInfo" )
4+ load ("//py/private:py_semantics.bzl" , _py_semantics = "semantics" )
5+ load ("//py/private/toolchain:types.bzl" , "PY_TOOLCHAIN" )
6+
7+ def _runfiles_path (file , workspace ):
8+ if file .short_path .startswith ("../" ):
9+ return file .short_path [3 :]
10+ else :
11+ return workspace + "/" + file .short_path
12+
13+ exclude_paths = [
14+ # following two lines will match paths we want to exclude in non-bzlmod setup
15+ "toolchain" ,
16+ "aspect_rules_py/py/tools/" ,
17+ # these will match in bzlmod setup
18+ "rules_python~~python~" ,
19+ "aspect_rules_py~/py/tools/" ,
20+ # these will match in bzlmod setup with --incompatible_use_plus_in_repo_names flag flipped.
21+ "rules_python++python+" ,
22+ "aspect_rules_py+/py/tools/"
23+ ]
24+
25+ # determines if the given file is a `distinfo`, `dep` or a `source`
26+ # this required to allow PEX to put files into different places.
27+ #
28+ # --dep: into `<PEX_UNPACK_ROOT>/.deps/<name_of_the_package>`
29+ # --distinfo: is only used for determining package metadata
30+ # --source: into `<PEX_UNPACK_ROOT>/<relative_path_to_workspace_root>/<file_name>`
31+ def _map_srcs (f , workspace ):
32+ dest_path = _runfiles_path (f , workspace )
33+
34+ # We exclude files from hermetic python toolchain.
35+ for exclude in exclude_paths :
36+ if dest_path .find (exclude ) != - 1 :
37+ return []
38+
39+ site_packages_i = f .path .find ("site-packages" )
40+
41+ # if path contains `site-packages` and there is only two path segments
42+ # after it, it will be treated as third party dep.
43+ # Here are some examples of path we expect and use and ones we ignore.
44+ #
45+ # Match: `external/rules_python~~pip~pypi_39_rtoml/site-packages/rtoml-0.11.0.dist-info/INSTALLER`
46+ # Reason: It has two `/` after first `site-packages` substring.
47+ #
48+ # No Match: `external/rules_python~~pip~pypi_39_rtoml/site-packages/rtoml-0.11.0/src/mod/parse.py`
49+ # Reason: It has three `/` after first `site-packages` substring.
50+ if site_packages_i != - 1 and f .path .count ("/" , site_packages_i ) == 2 :
51+ if f .path .find ("dist-info" , site_packages_i ) != - 1 :
52+ return ["--distinfo={}" .format (f .dirname )]
53+ return ["--dep={}" .format (f .dirname )]
54+
55+ # If the path does not have a `site-packages` in it, then put it into
56+ # the standard runfiles tree.
57+ elif site_packages_i == - 1 :
58+ return ["--source={}={}" .format (f .path , dest_path )]
59+
60+ return []
61+
62+ def _py_python_pex_impl (ctx ):
63+ py_toolchain = _py_semantics .resolve_toolchain (ctx )
64+
65+ binary = ctx .attr .binary
66+ runfiles = binary [DefaultInfo ].data_runfiles
67+
68+ output = ctx .actions .declare_file (ctx .attr .name + ".pex" )
69+
70+ args = ctx .actions .args ()
71+
72+ # Copy workspace name here to prevent ctx
73+ # being transferred to the execution phase.
74+ workspace_name = str (ctx .workspace_name )
75+
76+ args .add_all (
77+ ctx .attr .inject_env .items (),
78+ map_each = lambda e : "--inject-env={}={}" .format (e [0 ], e [1 ]),
79+ # this is needed to allow passing a lambda to map_each
80+ allow_closure = True ,
81+ )
82+
83+ args .add_all (
84+ binary [PyInfo ].imports ,
85+ format_each = "--sys-path=%s"
86+ )
87+
88+ args .add_all (
89+ runfiles .files ,
90+ map_each = lambda f : _map_srcs (f , workspace_name ),
91+ uniquify = True ,
92+ # this is needed to allow passing a lambda (with workspace_name) to map_each
93+ allow_closure = True ,
94+ )
95+ args .add (binary [DefaultInfo ].files_to_run .executable , format = "--executable=%s" )
96+ args .add (ctx .attr .python_shebang , format = "--python-shebang=%s" )
97+ args .add (py_toolchain .python , format = "--python=%s" )
98+
99+ py_version = py_toolchain .interpreter_version_info
100+ args .add_all (
101+ [
102+ constraint .format (major = py_version .major , minor = py_version .minor , patch = py_version .micro )
103+ for constraint in ctx .attr .python_interpreter_constraints
104+ ],
105+ format_each = "--python-version-constraint=%s"
106+ )
107+ args .add (output , format = "--output-file=%s" )
108+
109+ ctx .actions .run (
110+ executable = ctx .executable ._pex ,
111+ inputs = runfiles .files ,
112+ arguments = [args ],
113+ outputs = [output ],
114+ mnemonic = "PyPex" ,
115+ progress_message = "Building PEX binary %{label}" ,
116+ )
117+
118+ return [
119+ DefaultInfo (files = depset ([output ]), executable = output )
120+ ]
121+
122+
123+ _attrs = dict ({
124+ "binary" : attr .label (executable = True , cfg = "target" , mandatory = True , doc = "A py_binary target" ),
125+ "inject_env" : attr .string_dict (
126+ doc = "Environment variables to set when running the pex binary." ,
127+ default = {},
128+ ),
129+ "python_shebang" : attr .string (default = "#!/usr/bin/env python3" ),
130+ "python_interpreter_constraints" : attr .string_list (
131+ default = ["CPython=={major}.{minor}.*" ],
132+ doc = """\
133+ Python interpreter versions this PEX binary is compatible with. A list of semver strings.
134+ The placeholder strings `{major}`, `{minor}`, `{patch}` can be used for gathering version
135+ information from the hermetic python toolchain.
136+
137+ For example, to enforce same interpreter version that Bazel uses, following can be used.
138+
139+ ```starlark
140+ py_pex_binary
141+ python_interpreter_constraints = [
142+ "CPython=={major}.{minor}.{patch}"
143+ ]
144+ )
145+ ```
146+ """ ),
147+ "_pex" : attr .label (executable = True , cfg = "exec" , default = "//py/tools/pex" )
148+ })
149+
150+
151+ py_pex_binary = rule (
152+ doc = "Build a pex executable from a py_binary" ,
153+ implementation = _py_python_pex_impl ,
154+ attrs = _attrs ,
155+ toolchains = [
156+ PY_TOOLCHAIN
157+ ],
158+ executable = True ,
159+ )
0 commit comments