1 # Copyright (C) The Arvados Authors. All rights reserved.
3 # SPDX-License-Identifier: Apache-2.0
12 from pathlib import Path, PurePath, PurePosixPath
15 import setuptools.command.build
17 SETUP_DIR = Path(__file__).absolute().parent
18 VERSION_SCRIPT_PATH = PurePath('build', 'version-at-commit.sh')
20 ### Metadata generation
22 @dataclasses.dataclass
23 class ArvadosPythonPackage:
27 dependencies: t.Sequence['ArvadosPythonPackage']
35 def version_file_path(self):
36 return PurePath(self.module_name, '_version.py')
38 def _workspace_path(self, workdir):
40 workspace = Path(os.environ['WORKSPACE'])
41 # This will raise ValueError if they're not related,
42 # in which case we don't want to use this $WORKSPACE.
43 workdir.relative_to(workspace)
44 except (KeyError, ValueError):
46 if (workspace / VERSION_SCRIPT_PATH).exists():
51 def _git_version(self, workdir):
52 workspace = self._workspace_path(workdir)
56 'git', 'log', '-n1', '--format=%H', '--',
57 str(VERSION_SCRIPT_PATH), str(self.src_path),
59 git_log_cmd.extend(str(dep.src_path) for dep in self.dependencies)
60 git_log_proc = subprocess.run(
64 stdout=subprocess.PIPE,
67 version_proc = subprocess.run(
68 [str(VERSION_SCRIPT_PATH), git_log_proc.stdout.rstrip('\n')],
71 stdout=subprocess.PIPE,
74 return version_proc.stdout.rstrip('\n')
76 def _sdist_version(self, workdir):
78 pkg_info = (workdir / 'PKG-INFO').open()
79 except FileNotFoundError:
83 key, _, val = line.partition(': ')
85 return val.rstrip('\n')
86 raise Exception("found PKG-INFO file but not Version metadata in it")
88 def get_version(self, workdir=SETUP_DIR):
90 # If we're building out of a distribution, we should pass that
91 # version through unchanged.
92 self._sdist_version(workdir)
93 # Otherwise follow the usual Arvados versioning rules.
94 or os.environ.get('ARVADOS_BUILDING_VERSION')
95 or self._git_version(workdir)
98 raise Exception(f"no version information available for {self.package_name}")
101 r'(^development-|~dev|~rc)',
102 lambda match: self._VERSION_SUBS[match.group(0)],
106 def get_dependencies_version(self, workdir=SETUP_DIR, version=None):
108 version = self.get_version(workdir)
109 # A packaged development release should be installed with other
110 # development packages built from the same source, but those
111 # dependencies may have earlier "dev" versions (read: less recent
112 # Git commit timestamps). This compatible version dependency
113 # expresses that as closely as possible. Allowing versions
114 # compatible with .dev0 allows any development release.
115 # Regular expression borrowed partially from
116 # <https://packaging.python.org/en/latest/specifications/version-specifiers/#version-specifiers-regex>
117 dep_ver, match_count = re.subn(r'\.dev(0|[1-9][0-9]*)$', '.dev0', version, 1)
118 return ('~=' if match_count else '==', dep_ver)
120 def iter_dependencies(self, workdir=SETUP_DIR, version=None):
121 dep_op, dep_ver = self.get_dependencies_version(workdir, version)
122 for dep in self.dependencies:
123 yield f'{dep.package_name} {dep_op} {dep_ver}'
128 _PYSDK = ArvadosPythonPackage(
129 'arvados-python-client',
131 PurePath('sdk', 'python'),
134 _CRUNCHSTAT_SUMMARY = ArvadosPythonPackage(
135 'crunchstat_summary',
136 'crunchstat_summary',
137 PurePath('tools', 'crunchstat-summary'),
140 ARVADOS_PYTHON_MODULES = {mod.package_name: mod for mod in [
143 ArvadosPythonPackage(
144 'arvados-cluster-activity',
145 'arvados_cluster_activity',
146 PurePath('tools', 'cluster-activity'),
149 ArvadosPythonPackage(
150 'arvados-cwl-runner',
152 PurePath('sdk', 'cwl'),
153 [_PYSDK, _CRUNCHSTAT_SUMMARY],
155 ArvadosPythonPackage(
156 'arvados-docker-cleaner',
158 PurePath('services', 'dockercleaner'),
161 ArvadosPythonPackage(
164 PurePath('services', 'fuse'),
167 ArvadosPythonPackage(
168 'arvados-user-activity',
169 'arvados_user_activity',
170 PurePath('tools', 'user-activity'),
175 ### setuptools integration
177 class BuildArvadosVersion(setuptools.Command):
178 """Write _version.py for an Arvados module"""
179 def initialize_options(self):
180 self.build_lib = None
182 def finalize_options(self):
183 self.set_undefined_options("build_py", ("build_lib", "build_lib"))
184 arv_mod = ARVADOS_PYTHON_MODULES[self.distribution.get_name()]
185 self.out_path = Path(self.build_lib, arv_mod.version_file_path())
188 with self.out_path.open('w') as out_file:
189 print(f'__version__ = {self.distribution.get_version()!r}', file=out_file)
191 def get_outputs(self):
192 return [str(self.out_path)]
194 def get_source_files(self):
197 def get_output_mapping(self):
201 class ArvadosBuildCommand(setuptools.command.build.build):
203 *setuptools.command.build.build.sub_commands,
204 ('build_arvados_version', None),
209 'build': ArvadosBuildCommand,
210 'build_arvados_version': BuildArvadosVersion,