blob: f12230d269eab8e6960b1e1157e6b41564371ad0 [file] [log] [blame]
# Copyright 2016 Google Inc. 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.
"""Build self-contained python executables."""
load("//:debug.bzl", "dump")
DEFAULT_COMPILER = '//compiler:compiler.par'
def _parfile_impl(ctx):
"""Implementation of parfile() rule"""
# Find the main entry point
py_files = ctx.files.main
if len(py_files) == 0:
fail('Expected exactly one .py file, found none', 'main')
elif len(py_files) > 1:
fail('Expected exactly one .py file, found these: [%s]' % py_files, 'main')
main_py_file = py_files[0]
if main_py_file not in ctx.attr.src.data_runfiles.files:
fail('Main entry point [%s] not listed in srcs' % main_py_file, 'main')
# Find the list of things that must be built before this thing is built
# TODO: also handle ctx.attr.src.data_runfiles.symlinks
inputs = list(ctx.attr.src.default_runfiles.files)
# Make a manifest of files to store in the .par file. The
# runfiles manifest is not quite right, so we make our own.
sources_map = {}
# First, add the zero-length __init__.py files
for empty in ctx.attr.src.default_runfiles.empty_filenames:
stored_path = _prepend_workspace(empty, ctx)
local_path = ''
sources_map[stored_path] = local_path
# Now add the regular (source and generated) files
for input_file in inputs:
stored_path = _prepend_workspace(input_file.short_path, ctx)
local_path = input_file.path
sources_map[stored_path] = local_path
# Now make a nice sorted list
sources_lines = []
for k,v in sorted(sources_map.items()):
sources_lines.append('%s %s' % (k, v))
sources_content = '\n'.join(sources_lines) + '\n'
# Write the list to the manifest file
sources_file = ctx.new_file(ctx.label.name + '_SOURCES')
ctx.file_action(
output=sources_file,
content=sources_content,
executable=False)
# Find the list of directories to add to sys.path
# TODO(b/29227737): Use 'imports' provider from Bazel
stub_file = ctx.attr.src.files_to_run.executable.path
# Inputs to the action, but don't actually get stored in the .par file
extra_inputs = [
sources_file,
ctx.attr.src.files_to_run.executable,
ctx.attr.src.files_to_run.runfiles_manifest,
]
zip_safe = ctx.attr.zip_safe
# Assemble command line for .par compiler
args = [
'--manifest_file', sources_file.path,
'--outputpar', ctx.outputs.executable.path,
'--stub_file', stub_file,
'--zip_safe', str(zip_safe),
main_py_file.path,
]
ctx.action(
inputs=inputs + extra_inputs,
outputs=[ctx.outputs.executable],
progress_message='Building par file %s' % ctx.label,
executable=ctx.executable.compiler,
arguments=args,
mnemonic='PythonCompile',
)
# .par file itself has no runfiles and no providers
return struct()
def _prepend_workspace(path, ctx):
"""Given a path, prepend the workspace name as the parent directory"""
# It feels like there should be an easier, less fragile way.
if path.startswith('../'):
# External workspace, for example
# '../protobuf/python/google/protobuf/any_pb2.py'
stored_path = path[len('../'):]
elif path.startswith('external/'):
# External workspace, for example
# 'external/protobuf/python/__init__.py'
stored_path = path[len('external/'):]
else:
# Main workspace, for example 'mypackage/main.py'
stored_path = ctx.workspace_name + '/' + path
return stored_path
parfile_attrs = {
"src": attr.label(mandatory = True),
"main": attr.label(
mandatory = True,
allow_files = True,
single_file = True,
),
"imports": attr.string_list(default = []),
"default_python_version": attr.string(mandatory = True),
"compiler": attr.label(
default = Label(DEFAULT_COMPILER),
executable = True,
cfg = "host",
),
"zip_safe": attr.bool(default=True),
}
# Rule to create a parfile given a py_binary() as input
parfile = rule(
attrs = parfile_attrs,
executable = True,
implementation = _parfile_impl,
test = False,
)
"""A self-contained, single-file Python program, with a .par file extension.
You probably want to use par_binary() instead of this.
Args:
src: A py_binary() target
main: The name of the source file that is the main entry point of
the application.
See [py_binary.main](http://www.bazel.io/docs/be/python.html#py_binary.main)
imports: List of import directories to be added to the PYTHONPATH.
See [py_binary.imports](http://www.bazel.io/docs/be/python.html#py_binary.imports)
default_python_version: A string specifying the default Python major version to use when building this par file.
See [py_binary.default_python_version](http://www.bazel.io/docs/be/python.html#py_binary.default_python_version)
compiler: Internal use only.
zip_safe: Whether to import Python code and read datafiles directly
from the zip archive. Otherwise, if False, all files are
extracted to a temporary directory on disk each time the
par file executes.
TODO(b/27502830): A directory foo.par.runfiles is also created. This
is a bug, don't use or depend on it.
"""
parfile_test = rule(
attrs = parfile_attrs,
executable = True,
implementation = _parfile_impl,
test = True,
)
"""Identical to par_binary, but the rule is marked as being a test.
You probably want to use par_test() instead of this.
"""
def par_binary(name, **kwargs):
"""An executable Python program.
par_binary() is a drop-in replacement for py_binary() that also
builds a self-contained, single-file executable for the
application, with a .par file extension.
The `name` attribute shouldn't include the `.par` file extension,
it's added automatically. So, for a rule like
`par_binary(name="myname")`, build the file `myname.par` by doing
`bazel build //mypackage:myname.par`
See [py_binary](http://www.bazel.io/docs/be/python.html#py_binary)
for arguments and usage.
"""
compiler = kwargs.pop('compiler', None)
zip_safe = kwargs.pop('zip_safe', True)
native.py_binary(name=name, **kwargs)
main = kwargs.get('main', name + '.py')
imports = kwargs.get('imports')
default_python_version = kwargs.get('default_python_version', 'PY2')
visibility = kwargs.get('visibility')
testonly = kwargs.get('testonly', False)
parfile(
compiler=compiler,
default_python_version=default_python_version,
imports=imports,
main=main,
name=name + '.par',
src=name,
testonly=testonly,
visibility=visibility,
zip_safe=zip_safe,
)
def par_test(name, **kwargs):
"""An executable Python test.
Just like par_binary, but for py_test instead of py_binary. Useful if you
specifically need to test a module's behaviour when used in a .par binary.
"""
compiler = kwargs.pop('compiler', None)
zip_safe = kwargs.pop('zip_safe', True)
native.py_test(name=name, **kwargs)
main = kwargs.get('main', name + '.py')
imports = kwargs.get('imports')
default_python_version = kwargs.get('default_python_version', 'PY2')
visibility = kwargs.get('visibility')
testonly = kwargs.get('testonly', True)
parfile_test(
compiler=compiler,
default_python_version=default_python_version,
imports=imports,
main=main,
name=name + '.par',
src=name,
testonly=testonly,
visibility=visibility,
zip_safe=zip_safe,
)