21121: Right align numeric column, add note about how stuff is derived
[arvados.git] / tools / cluster-activity / arvados_version.py
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: Apache-2.0
4 #
5 # This file runs in one of three modes:
6 #
7 # 1. If the ARVADOS_BUILDING_VERSION environment variable is set, it writes
8 #    _version.py and generates dependencies based on that value.
9 # 2. If running from an arvados Git checkout, it writes _version.py
10 #    and generates dependencies from Git.
11 # 3. Otherwise, we expect this is source previously generated from Git, and
12 #    it reads _version.py and generates dependencies from it.
13
14 import os
15 import re
16 import runpy
17 import subprocess
18 import sys
19
20 from pathlib import Path
21
22 # These maps explain the relationships between different Python modules in
23 # the arvados repository. We use these to help generate setup.py.
24 PACKAGE_DEPENDENCY_MAP = {
25     'arvados-cwl-runner': ['arvados-python-client', 'crunchstat_summary'],
26     'arvados-user-activity': ['arvados-python-client'],
27     'arvados_fuse': ['arvados-python-client'],
28     'crunchstat_summary': ['arvados-python-client'],
29     'arvados_cluster_activity': ['arvados-python-client', 'crunchstat_summary'],
30 }
31 PACKAGE_MODULE_MAP = {
32     'arvados-cwl-runner': 'arvados_cwl',
33     'arvados-docker-cleaner': 'arvados_docker',
34     'arvados-python-client': 'arvados',
35     'arvados-user-activity': 'arvados_user_activity',
36     'arvados_fuse': 'arvados_fuse',
37     'crunchstat_summary': 'crunchstat_summary',
38     'arvados_cluster_activity': 'arvados_cluster_activity',
39 }
40 PACKAGE_SRCPATH_MAP = {
41     'arvados-cwl-runner': Path('sdk', 'cwl'),
42     'arvados-docker-cleaner': Path('services', 'dockercleaner'),
43     'arvados-python-client': Path('sdk', 'python'),
44     'arvados-user-activity': Path('tools', 'user-activity'),
45     'arvados_fuse': Path('services', 'fuse'),
46     'crunchstat_summary': Path('tools', 'crunchstat-summary'),
47     'arvados_cluster_activity': Path('tools', 'cluster-activity'),
48 }
49
50 ENV_VERSION = os.environ.get("ARVADOS_BUILDING_VERSION")
51 SETUP_DIR = Path(__file__).absolute().parent
52 try:
53     REPO_PATH = Path(subprocess.check_output(
54         ['git', '-C', str(SETUP_DIR), 'rev-parse', '--show-toplevel'],
55         stderr=subprocess.DEVNULL,
56         text=True,
57     ).rstrip('\n'))
58 except (subprocess.CalledProcessError, OSError):
59     REPO_PATH = None
60 else:
61     # Verify this is the arvados monorepo
62     if all((REPO_PATH / path).exists() for path in PACKAGE_SRCPATH_MAP.values()):
63         PACKAGE_NAME, = (
64             pkg_name for pkg_name, path in PACKAGE_SRCPATH_MAP.items()
65             if (REPO_PATH / path) == SETUP_DIR
66         )
67         MODULE_NAME = PACKAGE_MODULE_MAP[PACKAGE_NAME]
68         VERSION_SCRIPT_PATH = Path(REPO_PATH, 'build', 'version-at-commit.sh')
69     else:
70         REPO_PATH = None
71 if REPO_PATH is None:
72     (PACKAGE_NAME, MODULE_NAME), = (
73         (pkg_name, mod_name)
74         for pkg_name, mod_name in PACKAGE_MODULE_MAP.items()
75         if (SETUP_DIR / mod_name).is_dir()
76     )
77
78 def git_log_output(path, *args):
79     return subprocess.check_output(
80         ['git', '-C', str(REPO_PATH),
81          'log', '--first-parent', '--max-count=1',
82          *args, str(path)],
83         text=True,
84     ).rstrip('\n')
85
86 def choose_version_from():
87     ver_paths = [SETUP_DIR, VERSION_SCRIPT_PATH, *(
88         PACKAGE_SRCPATH_MAP[pkg]
89         for pkg in PACKAGE_DEPENDENCY_MAP.get(PACKAGE_NAME, ())
90     )]
91     getver = max(ver_paths, key=lambda path: git_log_output(path, '--format=format:%ct'))
92     print(f"Using {getver} for version number calculation of {SETUP_DIR}", file=sys.stderr)
93     return getver
94
95 def git_version_at_commit():
96     curdir = choose_version_from()
97     myhash = git_log_output(curdir, '--format=%H')
98     return subprocess.check_output(
99         [str(VERSION_SCRIPT_PATH), myhash],
100         text=True,
101     ).rstrip('\n')
102
103 def save_version(setup_dir, module, v):
104     with Path(setup_dir, module, '_version.py').open('w') as fp:
105         print(f"__version__ = {v!r}", file=fp)
106
107 def read_version(setup_dir, module):
108     file_vars = runpy.run_path(Path(setup_dir, module, '_version.py'))
109     return file_vars['__version__']
110
111 def get_version(setup_dir=SETUP_DIR, module=MODULE_NAME):
112     if ENV_VERSION:
113         version = ENV_VERSION
114     elif REPO_PATH is None:
115         return read_version(setup_dir, module)
116     else:
117         version = git_version_at_commit()
118     version = version.replace("~dev", ".dev").replace("~rc", "rc")
119     save_version(setup_dir, module, version)
120     return version
121
122 def iter_dependencies(version=None):
123     if version is None:
124         version = get_version()
125     # A packaged development release should be installed with other
126     # development packages built from the same source, but those
127     # dependencies may have earlier "dev" versions (read: less recent
128     # Git commit timestamps). This compatible version dependency
129     # expresses that as closely as possible. Allowing versions
130     # compatible with .dev0 allows any development release.
131     # Regular expression borrowed partially from
132     # <https://packaging.python.org/en/latest/specifications/version-specifiers/#version-specifiers-regex>
133     dep_ver, match_count = re.subn(r'\.dev(0|[1-9][0-9]*)$', '.dev0', version, 1)
134     dep_op = '~=' if match_count else '=='
135     for dep_pkg in PACKAGE_DEPENDENCY_MAP.get(PACKAGE_NAME, ()):
136         yield f'{dep_pkg}{dep_op}{dep_ver}'
137
138 # Called from calculate_python_sdk_cwl_package_versions() in run-library.sh
139 if __name__ == '__main__':
140     print(get_version())