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