1*e7b1675dSTing-Kang Chang# Copyright 2020 Google LLC 2*e7b1675dSTing-Kang Chang# 3*e7b1675dSTing-Kang Chang# Licensed under the Apache License, Version 2.0 (the "License"); 4*e7b1675dSTing-Kang Chang# you may not use this file except in compliance with the License. 5*e7b1675dSTing-Kang Chang# You may obtain a copy of the License at 6*e7b1675dSTing-Kang Chang# 7*e7b1675dSTing-Kang Chang# http://www.apache.org/licenses/LICENSE-2.0 8*e7b1675dSTing-Kang Chang# 9*e7b1675dSTing-Kang Chang# Unless required by applicable law or agreed to in writing, software 10*e7b1675dSTing-Kang Chang# distributed under the License is distributed on an "AS IS" BASIS, 11*e7b1675dSTing-Kang Chang# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12*e7b1675dSTing-Kang Chang# See the License for the specific language governing permissions and 13*e7b1675dSTing-Kang Chang# limitations under the License. 14*e7b1675dSTing-Kang Chang# ============================================================================== 15*e7b1675dSTing-Kang Chang"""Setup for Tink package with pip.""" 16*e7b1675dSTing-Kang Changfrom __future__ import absolute_import 17*e7b1675dSTing-Kang Changfrom __future__ import division 18*e7b1675dSTing-Kang Changfrom __future__ import print_function 19*e7b1675dSTing-Kang Chang 20*e7b1675dSTing-Kang Changimport glob 21*e7b1675dSTing-Kang Changimport os 22*e7b1675dSTing-Kang Changimport posixpath 23*e7b1675dSTing-Kang Changimport re 24*e7b1675dSTing-Kang Changimport shutil 25*e7b1675dSTing-Kang Changimport subprocess 26*e7b1675dSTing-Kang Changimport textwrap 27*e7b1675dSTing-Kang Chang 28*e7b1675dSTing-Kang Changimport setuptools 29*e7b1675dSTing-Kang Changfrom setuptools.command import build_ext 30*e7b1675dSTing-Kang Changfrom setuptools.command import sdist 31*e7b1675dSTing-Kang Chang 32*e7b1675dSTing-Kang Chang 33*e7b1675dSTing-Kang Chang_PROJECT_BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 34*e7b1675dSTing-Kang Chang 35*e7b1675dSTing-Kang Chang 36*e7b1675dSTing-Kang Changdef _get_tink_version(): 37*e7b1675dSTing-Kang Chang """Parses the version number from VERSION file.""" 38*e7b1675dSTing-Kang Chang with open(os.path.join(_PROJECT_BASE_DIR, 'VERSION')) as f: 39*e7b1675dSTing-Kang Chang try: 40*e7b1675dSTing-Kang Chang version_line = next( 41*e7b1675dSTing-Kang Chang line for line in f if line.startswith('TINK_VERSION_LABEL')) 42*e7b1675dSTing-Kang Chang except StopIteration: 43*e7b1675dSTing-Kang Chang raise ValueError( 44*e7b1675dSTing-Kang Chang 'Version not defined in {}/VERSION'.format(_PROJECT_BASE_DIR)) 45*e7b1675dSTing-Kang Chang else: 46*e7b1675dSTing-Kang Chang return version_line.split(' = ')[-1].strip('\n \'"') 47*e7b1675dSTing-Kang Chang 48*e7b1675dSTing-Kang Chang 49*e7b1675dSTing-Kang Chang_TINK_VERSION = _get_tink_version() 50*e7b1675dSTing-Kang Chang 51*e7b1675dSTing-Kang Chang 52*e7b1675dSTing-Kang Changdef _get_bazel_command(): 53*e7b1675dSTing-Kang Chang """Finds the bazel command.""" 54*e7b1675dSTing-Kang Chang if shutil.which('bazelisk'): 55*e7b1675dSTing-Kang Chang return 'bazelisk' 56*e7b1675dSTing-Kang Chang elif shutil.which('bazel'): 57*e7b1675dSTing-Kang Chang return 'bazel' 58*e7b1675dSTing-Kang Chang raise FileNotFoundError('Could not find bazel executable. Please install ' 59*e7b1675dSTing-Kang Chang 'bazel to compile the Tink Python package.') 60*e7b1675dSTing-Kang Chang 61*e7b1675dSTing-Kang Chang 62*e7b1675dSTing-Kang Changdef _get_protoc_command(): 63*e7b1675dSTing-Kang Chang """Finds the protoc command.""" 64*e7b1675dSTing-Kang Chang if 'PROTOC' in os.environ and os.path.exists(os.environ['PROTOC']): 65*e7b1675dSTing-Kang Chang return os.environ['PROTOC'] 66*e7b1675dSTing-Kang Chang protoc_path = shutil.which('protoc') 67*e7b1675dSTing-Kang Chang if protoc_path is None: 68*e7b1675dSTing-Kang Chang raise FileNotFoundError('Could not find protoc executable. Please install ' 69*e7b1675dSTing-Kang Chang 'protoc to compile the Tink Python package.') 70*e7b1675dSTing-Kang Chang return protoc_path 71*e7b1675dSTing-Kang Chang 72*e7b1675dSTing-Kang Chang 73*e7b1675dSTing-Kang Changdef _generate_proto(protoc, source): 74*e7b1675dSTing-Kang Chang """Invokes the Protocol Compiler to generate a _pb2.py.""" 75*e7b1675dSTing-Kang Chang 76*e7b1675dSTing-Kang Chang if not os.path.exists(source): 77*e7b1675dSTing-Kang Chang raise FileNotFoundError('Cannot find required file: {}'.format(source)) 78*e7b1675dSTing-Kang Chang 79*e7b1675dSTing-Kang Chang output = source.replace('.proto', '_pb2.py') 80*e7b1675dSTing-Kang Chang 81*e7b1675dSTing-Kang Chang if (os.path.exists(output) and 82*e7b1675dSTing-Kang Chang os.path.getmtime(source) < os.path.getmtime(output)): 83*e7b1675dSTing-Kang Chang # No need to regenerate if output is newer than source. 84*e7b1675dSTing-Kang Chang return 85*e7b1675dSTing-Kang Chang 86*e7b1675dSTing-Kang Chang print('Generating {}...'.format(output)) 87*e7b1675dSTing-Kang Chang protoc_args = [protoc, '-I.', '--python_out=.', source] 88*e7b1675dSTing-Kang Chang subprocess.run(args=protoc_args, check=True) 89*e7b1675dSTing-Kang Chang 90*e7b1675dSTing-Kang Chang 91*e7b1675dSTing-Kang Changdef _parse_requirements(filename): 92*e7b1675dSTing-Kang Chang with open(os.path.join(_PROJECT_BASE_DIR, filename)) as f: 93*e7b1675dSTing-Kang Chang return [ 94*e7b1675dSTing-Kang Chang line.rstrip() 95*e7b1675dSTing-Kang Chang for line in f 96*e7b1675dSTing-Kang Chang if not (line.isspace() or line.startswith('#')) 97*e7b1675dSTing-Kang Chang ] 98*e7b1675dSTing-Kang Chang 99*e7b1675dSTing-Kang Chang 100*e7b1675dSTing-Kang Changdef _patch_workspace(workspace_file): 101*e7b1675dSTing-Kang Chang """Update the Bazel workspace with valid repository references. 102*e7b1675dSTing-Kang Chang 103*e7b1675dSTing-Kang Chang When installing the sdist, e.g., with `pip install tink --no-binary` or 104*e7b1675dSTing-Kang Chang `python3 -m pip install -v path/to/sdist.tar.gz`, setuptools unpacks the 105*e7b1675dSTing-Kang Chang sdist in a temporary folder that contains only the python/ folder, and then 106*e7b1675dSTing-Kang Chang builds it. As a consequence, relative local_repository paths that are set by 107*e7b1675dSTing-Kang Chang default in python/WORKSPACE don't exist (or worst, they may exist by 108*e7b1675dSTing-Kang Chang chance!). 109*e7b1675dSTing-Kang Chang 110*e7b1675dSTing-Kang Chang By default, the local_repository() rules will be replaced with http_archive() 111*e7b1675dSTing-Kang Chang rules which contain URLs that point to an archive of the Tink GitHub 112*e7b1675dSTing-Kang Chang repository as of the latest commit as of the master branch. 113*e7b1675dSTing-Kang Chang 114*e7b1675dSTing-Kang Chang This behavior can be modified via the following environment variables, in 115*e7b1675dSTing-Kang Chang order of precedence: 116*e7b1675dSTing-Kang Chang 117*e7b1675dSTing-Kang Chang * TINK_PYTHON_SETUPTOOLS_OVERRIDE_BASE_PATH 118*e7b1675dSTing-Kang Chang Instead of using http_archive() rules, update the local_repository() 119*e7b1675dSTing-Kang Chang rules with the specified alternative local path. This allows for 120*e7b1675dSTing-Kang Chang building using a local copy of the project (e.g. for testing). 121*e7b1675dSTing-Kang Chang 122*e7b1675dSTing-Kang Chang * TINK_PYTHON_SETUPTOOLS_TAGGED_VERSION 123*e7b1675dSTing-Kang Chang Instead of providing a URL of an archive of the current master branch, 124*e7b1675dSTing-Kang Chang instead link to an archive that correspond with the tagged version (e.g. 125*e7b1675dSTing-Kang Chang for creating release artifacts). 126*e7b1675dSTing-Kang Chang 127*e7b1675dSTing-Kang Chang Args: 128*e7b1675dSTing-Kang Chang workspace_file: The tink/python WORKSPACE. 129*e7b1675dSTing-Kang Chang """ 130*e7b1675dSTing-Kang Chang 131*e7b1675dSTing-Kang Chang with open(workspace_file, 'r') as f: 132*e7b1675dSTing-Kang Chang workspace_content = f.read() 133*e7b1675dSTing-Kang Chang 134*e7b1675dSTing-Kang Chang if 'TINK_PYTHON_SETUPTOOLS_OVERRIDE_BASE_PATH' in os.environ: 135*e7b1675dSTing-Kang Chang base_path = os.environ['TINK_PYTHON_SETUPTOOLS_OVERRIDE_BASE_PATH'] 136*e7b1675dSTing-Kang Chang workspace_content = _patch_with_local_path(workspace_content, base_path) 137*e7b1675dSTing-Kang Chang 138*e7b1675dSTing-Kang Chang elif 'TINK_PYTHON_SETUPTOOLS_TAGGED_VERSION' in os.environ: 139*e7b1675dSTing-Kang Chang tagged_version = os.environ['TINK_PYTHON_SETUPTOOLS_TAGGED_VERSION'] 140*e7b1675dSTing-Kang Chang archive_filename = 'v{}.zip'.format(tagged_version) 141*e7b1675dSTing-Kang Chang archive_prefix = 'tink-{}'.format(tagged_version) 142*e7b1675dSTing-Kang Chang workspace_content = _patch_with_http_archive(workspace_content, 143*e7b1675dSTing-Kang Chang archive_filename, 144*e7b1675dSTing-Kang Chang archive_prefix) 145*e7b1675dSTing-Kang Chang else: 146*e7b1675dSTing-Kang Chang workspace_content = _patch_with_http_archive(workspace_content, 147*e7b1675dSTing-Kang Chang 'master.zip', 'tink-master') 148*e7b1675dSTing-Kang Chang 149*e7b1675dSTing-Kang Chang with open(workspace_file, 'w') as f: 150*e7b1675dSTing-Kang Chang f.write(workspace_content) 151*e7b1675dSTing-Kang Chang 152*e7b1675dSTing-Kang Chang 153*e7b1675dSTing-Kang Changdef _patch_with_local_path(workspace_content, base_path): 154*e7b1675dSTing-Kang Chang """Replaces the base paths in the local_repository() rules.""" 155*e7b1675dSTing-Kang Chang return re.sub( 156*e7b1675dSTing-Kang Chang r'(?<="tink_cc",\n path = ").*(?=\n)', 157*e7b1675dSTing-Kang Chang base_path + '/cc", # Modified by setup.py', 158*e7b1675dSTing-Kang Chang workspace_content, 159*e7b1675dSTing-Kang Chang ) 160*e7b1675dSTing-Kang Chang 161*e7b1675dSTing-Kang Chang 162*e7b1675dSTing-Kang Changdef _patch_with_http_archive(workspace_content, filename, prefix): 163*e7b1675dSTing-Kang Chang """Replaces local_repository() rules with http_archive() rules.""" 164*e7b1675dSTing-Kang Chang 165*e7b1675dSTing-Kang Chang workspace_lines = workspace_content.split('\n') 166*e7b1675dSTing-Kang Chang http_archive_load = ( 167*e7b1675dSTing-Kang Chang 'load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")') 168*e7b1675dSTing-Kang Chang 169*e7b1675dSTing-Kang Chang if http_archive_load not in workspace_content: 170*e7b1675dSTing-Kang Chang workspace_content = '\n'.join([workspace_lines[0], http_archive_load] + 171*e7b1675dSTing-Kang Chang workspace_lines[1:]) 172*e7b1675dSTing-Kang Chang 173*e7b1675dSTing-Kang Chang cc = textwrap.dedent( 174*e7b1675dSTing-Kang Chang '''\ 175*e7b1675dSTing-Kang Chang local_repository( 176*e7b1675dSTing-Kang Chang name = "tink_cc", 177*e7b1675dSTing-Kang Chang path = "../cc", 178*e7b1675dSTing-Kang Chang ) 179*e7b1675dSTing-Kang Chang ''') 180*e7b1675dSTing-Kang Chang 181*e7b1675dSTing-Kang Chang cc_patched = textwrap.dedent( 182*e7b1675dSTing-Kang Chang '''\ 183*e7b1675dSTing-Kang Chang # Modified by setup.py 184*e7b1675dSTing-Kang Chang http_archive( 185*e7b1675dSTing-Kang Chang name = "tink_cc", 186*e7b1675dSTing-Kang Chang urls = ["https://github.com/google/tink/archive/{}"], 187*e7b1675dSTing-Kang Chang strip_prefix = "{}/cc", 188*e7b1675dSTing-Kang Chang ) 189*e7b1675dSTing-Kang Chang '''.format(filename, prefix)) 190*e7b1675dSTing-Kang Chang 191*e7b1675dSTing-Kang Chang return workspace_content.replace(cc, cc_patched) 192*e7b1675dSTing-Kang Chang 193*e7b1675dSTing-Kang Chang 194*e7b1675dSTing-Kang Changclass BazelExtension(setuptools.Extension): 195*e7b1675dSTing-Kang Chang """A C/C++ extension that is defined as a Bazel BUILD target.""" 196*e7b1675dSTing-Kang Chang 197*e7b1675dSTing-Kang Chang def __init__(self, bazel_target, target_name=''): 198*e7b1675dSTing-Kang Chang self.bazel_target = bazel_target 199*e7b1675dSTing-Kang Chang self.relpath, self.target_name = ( 200*e7b1675dSTing-Kang Chang posixpath.relpath(bazel_target, '//').split(':')) 201*e7b1675dSTing-Kang Chang if target_name: 202*e7b1675dSTing-Kang Chang self.target_name = target_name 203*e7b1675dSTing-Kang Chang ext_name = os.path.join( 204*e7b1675dSTing-Kang Chang self.relpath.replace(posixpath.sep, os.path.sep), self.target_name) 205*e7b1675dSTing-Kang Chang setuptools.Extension.__init__(self, ext_name, sources=[]) 206*e7b1675dSTing-Kang Chang 207*e7b1675dSTing-Kang Chang 208*e7b1675dSTing-Kang Changclass BuildBazelExtension(build_ext.build_ext): 209*e7b1675dSTing-Kang Chang """A command that runs Bazel to build a C/C++ extension.""" 210*e7b1675dSTing-Kang Chang 211*e7b1675dSTing-Kang Chang def __init__(self, dist): 212*e7b1675dSTing-Kang Chang super(BuildBazelExtension, self).__init__(dist) 213*e7b1675dSTing-Kang Chang self.bazel_command = _get_bazel_command() 214*e7b1675dSTing-Kang Chang 215*e7b1675dSTing-Kang Chang def run(self): 216*e7b1675dSTing-Kang Chang for ext in self.extensions: 217*e7b1675dSTing-Kang Chang self.bazel_build(ext) 218*e7b1675dSTing-Kang Chang build_ext.build_ext.run(self) 219*e7b1675dSTing-Kang Chang 220*e7b1675dSTing-Kang Chang def bazel_build(self, ext): 221*e7b1675dSTing-Kang Chang # Change WORKSPACE to include tink_cc from an archive 222*e7b1675dSTing-Kang Chang _patch_workspace('WORKSPACE') 223*e7b1675dSTing-Kang Chang 224*e7b1675dSTing-Kang Chang if not os.path.exists(self.build_temp): 225*e7b1675dSTing-Kang Chang os.makedirs(self.build_temp) 226*e7b1675dSTing-Kang Chang 227*e7b1675dSTing-Kang Chang # Ensure no artifacts from previous builds are reused (i.e. from builds 228*e7b1675dSTing-Kang Chang # using a different Python version). 229*e7b1675dSTing-Kang Chang bazel_clean_argv = [self.bazel_command, 'clean', '--expunge'] 230*e7b1675dSTing-Kang Chang self.spawn(bazel_clean_argv) 231*e7b1675dSTing-Kang Chang 232*e7b1675dSTing-Kang Chang bazel_argv = [ 233*e7b1675dSTing-Kang Chang self.bazel_command, 'build', ext.bazel_target, 234*e7b1675dSTing-Kang Chang '--compilation_mode=' + ('dbg' if self.debug else 'opt') 235*e7b1675dSTing-Kang Chang ] 236*e7b1675dSTing-Kang Chang self.spawn(bazel_argv) 237*e7b1675dSTing-Kang Chang ext_bazel_bin_path = os.path.join('bazel-bin', ext.relpath, 238*e7b1675dSTing-Kang Chang ext.target_name + '.so') 239*e7b1675dSTing-Kang Chang ext_dest_path = self.get_ext_fullpath(ext.name) 240*e7b1675dSTing-Kang Chang ext_dest_dir = os.path.dirname(ext_dest_path) 241*e7b1675dSTing-Kang Chang if not os.path.exists(ext_dest_dir): 242*e7b1675dSTing-Kang Chang os.makedirs(ext_dest_dir) 243*e7b1675dSTing-Kang Chang shutil.copyfile(ext_bazel_bin_path, ext_dest_path) 244*e7b1675dSTing-Kang Chang 245*e7b1675dSTing-Kang Chang 246*e7b1675dSTing-Kang Changclass SdistCmd(sdist.sdist): 247*e7b1675dSTing-Kang Chang """A command that patches the workspace before creating an sdist.""" 248*e7b1675dSTing-Kang Chang 249*e7b1675dSTing-Kang Chang def run(self): 250*e7b1675dSTing-Kang Chang _patch_workspace('WORKSPACE') 251*e7b1675dSTing-Kang Chang sdist.sdist.run(self) 252*e7b1675dSTing-Kang Chang 253*e7b1675dSTing-Kang Chang 254*e7b1675dSTing-Kang Changdef main(): 255*e7b1675dSTing-Kang Chang # Generate compiled protocol buffers. 256*e7b1675dSTing-Kang Chang protoc_command = _get_protoc_command() 257*e7b1675dSTing-Kang Chang for proto_file in glob.glob('tink/proto/*.proto'): 258*e7b1675dSTing-Kang Chang _generate_proto(protoc_command, proto_file) 259*e7b1675dSTing-Kang Chang 260*e7b1675dSTing-Kang Chang setuptools.setup( 261*e7b1675dSTing-Kang Chang name='tink', 262*e7b1675dSTing-Kang Chang version=_TINK_VERSION, 263*e7b1675dSTing-Kang Chang url='https://github.com/google/tink', 264*e7b1675dSTing-Kang Chang description='A multi-language, cross-platform library that provides ' 265*e7b1675dSTing-Kang Chang 'cryptographic APIs that are secure, easy to use correctly, ' 266*e7b1675dSTing-Kang Chang 'and hard(er) to misuse.', 267*e7b1675dSTing-Kang Chang author='Tink Developers', 268*e7b1675dSTing-Kang Chang author_email='[email protected]', 269*e7b1675dSTing-Kang Chang long_description=open('README.md').read(), 270*e7b1675dSTing-Kang Chang long_description_content_type='text/markdown', 271*e7b1675dSTing-Kang Chang # Contained modules and scripts. 272*e7b1675dSTing-Kang Chang packages=setuptools.find_packages(), 273*e7b1675dSTing-Kang Chang install_requires=_parse_requirements('requirements.in'), 274*e7b1675dSTing-Kang Chang cmdclass={ 275*e7b1675dSTing-Kang Chang 'build_ext': BuildBazelExtension, 276*e7b1675dSTing-Kang Chang 'sdist': SdistCmd, 277*e7b1675dSTing-Kang Chang }, 278*e7b1675dSTing-Kang Chang ext_modules=[ 279*e7b1675dSTing-Kang Chang BazelExtension('//tink/cc/pybind:tink_bindings'), 280*e7b1675dSTing-Kang Chang ], 281*e7b1675dSTing-Kang Chang zip_safe=False, 282*e7b1675dSTing-Kang Chang # PyPI package information. 283*e7b1675dSTing-Kang Chang classifiers=[ 284*e7b1675dSTing-Kang Chang 'Programming Language :: Python :: 3.7', 285*e7b1675dSTing-Kang Chang 'Programming Language :: Python :: 3.8', 286*e7b1675dSTing-Kang Chang 'Programming Language :: Python :: 3.9', 287*e7b1675dSTing-Kang Chang 'Programming Language :: Python :: 3.10', 288*e7b1675dSTing-Kang Chang 'Topic :: Software Development :: Libraries', 289*e7b1675dSTing-Kang Chang ], 290*e7b1675dSTing-Kang Chang license='Apache 2.0', 291*e7b1675dSTing-Kang Chang keywords='tink cryptography', 292*e7b1675dSTing-Kang Chang ) 293*e7b1675dSTing-Kang Chang 294*e7b1675dSTing-Kang Chang 295*e7b1675dSTing-Kang Changif __name__ == '__main__': 296*e7b1675dSTing-Kang Chang main() 297