blob: aed66e4c503252886c8f7e966fca5f4186ab9e9d [file] [log] [blame]
# 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.
# ==============================================================================
"""Setup for Tink package with pip."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from distutils import spawn
import glob
import os
import posixpath
import re
import shutil
import subprocess
import sys
import setuptools
from setuptools.command import build_ext
here = os.path.dirname(os.path.abspath(__file__))
def _get_tink_version():
"""Parses the version number from VERSION file."""
with open(os.path.join(here, 'VERSION')) as f:
try:
version_line = next(
line for line in f if line.startswith('TINK_VERSION_LABEL'))
except StopIteration:
raise ValueError('Version not defined in python/VERSION')
else:
return version_line.split(' = ')[-1].strip('\n \'"')
# Check Bazel enviroment and set executable
if spawn.find_executable('bazelisk'):
bazel = 'bazelisk'
elif spawn.find_executable('bazel'):
bazel = 'bazel'
else:
sys.stderr.write('Could not find bazel executable. Please install bazel to'
'compile the Tink Python package.')
sys.exit(-1)
# Find the Protocol Compiler.
if 'PROTOC' in os.environ and os.path.exists(os.environ['PROTOC']):
protoc = os.environ['PROTOC']
else:
protoc = spawn.find_executable('protoc')
def _generate_proto(source):
"""Invokes the Protocol Compiler to generate a _pb2.py."""
output = source.replace('.proto', '_pb2.py')
if (not os.path.exists(output) or
(os.path.exists(source) and
os.path.getmtime(source) > os.path.getmtime(output))):
print('Generating %s...' % output)
if not os.path.exists(source):
sys.stderr.write('Cannot find required file: %s\n' % source)
sys.exit(-1)
if protoc is None:
sys.stderr.write(
'protoc is not installed nor found in ../src. Please compile it '
'or install the binary package.\n')
sys.exit(-1)
protoc_command = [protoc, '-I.', '--python_out=.', source]
if subprocess.call(protoc_command) != 0:
sys.exit(-1)
for proto_file in glob.glob('tink/proto/*.proto'):
_generate_proto(proto_file)
def _parse_requirements(path):
with open(os.path.join(here, path)) as f:
return [
line.rstrip()
for line in f
if not (line.isspace() or line.startswith('#'))
]
def _patch_workspace(workspace_content):
"""Change inclusion of the other WORKSPACEs in Tink to be absolute.
Setuptools builds in a temporary folder, therefore the relative paths can not
be resolved. Instead we use the http_archives during the build.
Args:
workspace_content: The original tink/python WORKSPACE.
Returns:
The workspace_content using http_archive for tink_base and tink_cc.
"""
# This is run by pip from a temporary folder which breaks the WORKSPACE paths.
# This replaces the paths with the latest http_archive.
# In order to override this with a local WORKSPACE use the
# TINK_PYTHON_SETUPTOOLS_OVERRIDE_BASE_PATH environment variable.
if 'TINK_PYTHON_SETUPTOOLS_OVERRIDE_BASE_PATH' in os.environ:
base_path = os.environ['TINK_PYTHON_SETUPTOOLS_OVERRIDE_BASE_PATH']
workspace_content = re.sub(r'(?<="tink_base",\n path = ").*(?=\n)',
base_path + '", # Modified by setup.py',
workspace_content)
workspace_content = re.sub(r'(?<="tink_cc",\n path = ").*(?=\n)',
base_path + '/cc' + '", # Modified by setup.py',
workspace_content)
else:
# If not base is specified use the latest version from GitHub
# Add http_archive load
workspace_lines = workspace_content.split('\n')
http_archive_load = ('load("@bazel_tools//tools/build_defs/repo:http.bzl", '
'"http_archive")')
workspace_content = '\n'.join([workspace_lines[0], http_archive_load] +
workspace_lines[1:])
base = ('local_repository(\n'
' name = "tink_base",\n'
' path = "..",\n'
')\n')
cc = ('local_repository(\n'
' name = "tink_cc",\n'
' path = "../cc",\n'
')\n')
base_patched = (
'# Modified by setup.py\n'
'http_archive(\n'
' name = "tink_base",\n'
' urls = ["https://github.com/google/tink/archive/master.zip"],\n'
' strip_prefix = "tink-master/",\n'
')\n')
cc_patched = (
'# Modified by setup.py\n'
'http_archive(\n'
' name = "tink_cc",\n'
' urls = ["https://github.com/google/tink/archive/master.zip"],\n'
' strip_prefix = "tink-master/cc",\n'
')\n')
workspace_content = workspace_content.replace(base, base_patched)
workspace_content = workspace_content.replace(cc, cc_patched)
return workspace_content
class BazelExtension(setuptools.Extension):
"""A C/C++ extension that is defined as a Bazel BUILD target."""
def __init__(self, bazel_target, target_name=''):
self.bazel_target = bazel_target
self.relpath, self.target_name = (
posixpath.relpath(bazel_target, '//').split(':'))
if target_name:
self.target_name = target_name
ext_name = os.path.join(
self.relpath.replace(posixpath.sep, os.path.sep), self.target_name)
setuptools.Extension.__init__(self, ext_name, sources=[])
class BuildBazelExtension(build_ext.build_ext):
"""A command that runs Bazel to build a C/C++ extension."""
def run(self):
for ext in self.extensions:
self.bazel_build(ext)
build_ext.build_ext.run(self)
def bazel_build(self, ext):
# Change WORKSPACE to include tink_base and tink_cc from an archive
with open('WORKSPACE', 'r') as f:
workspace_contents = f.read()
with open('WORKSPACE', 'w') as f:
f.write(_patch_workspace(workspace_contents))
if not os.path.exists(self.build_temp):
os.makedirs(self.build_temp)
bazel_argv = [
bazel, 'build', ext.bazel_target,
'--compilation_mode=' + ('dbg' if self.debug else 'opt'),
'--incompatible_linkopts_to_linklibs'
# TODO(https://github.com/bazelbuild/bazel/issues/9254): Remove linkopts
# flag when issue is fixed.
]
self.spawn(bazel_argv)
ext_bazel_bin_path = os.path.join('bazel-bin', ext.relpath,
ext.target_name + '.so')
ext_dest_path = self.get_ext_fullpath(ext.name)
ext_dest_dir = os.path.dirname(ext_dest_path)
if not os.path.exists(ext_dest_dir):
os.makedirs(ext_dest_dir)
shutil.copyfile(ext_bazel_bin_path, ext_dest_path)
setuptools.setup(
name='tink',
version=_get_tink_version(),
url='https://github.com/google/tink',
description='A multi-language, cross-platform library that provides '
'cryptographic APIs that are secure, easy to use correctly, '
'and hard(er) to misuse.',
author='Tink Developers',
author_email='tink-users@googlegroups.com',
long_description=open('README.md').read(),
long_description_content_type='text/markdown',
# Contained modules and scripts.
packages=setuptools.find_packages(),
install_requires=_parse_requirements('requirements.txt'),
cmdclass=dict(build_ext=BuildBazelExtension),
ext_modules=[
BazelExtension('//tink/cc/pybind:tink_bindings'),
],
zip_safe=False,
# PyPI package information.
classifiers=[
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Topic :: Software Development :: Libraries',
],
license='Apache 2.0',
keywords='tink cryptography',
)