David Ostrovsky | 2b5fe09 | 2021-03-03 11:52:30 +0100 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
Christian Aistleitner | 95bfeba | 2020-06-11 11:29:50 +0200 | [diff] [blame] | 2 | |
| 3 | # This is a variant of the `workspace_status.py` script that in addition to |
| 4 | # plain `git describe` implements a few heuristics to arrive at more to the |
| 5 | # point stamps for directories. But due to the implemented heuristics, it will |
| 6 | # typically take longer to run (especially if you use lots of plugins that |
| 7 | # come without tags) and might slow down your development cycle when used |
| 8 | # as default. |
| 9 | # |
| 10 | # To use it, simply add |
| 11 | # |
David Ostrovsky | 2b5fe09 | 2021-03-03 11:52:30 +0100 | [diff] [blame] | 12 | # --workspace_status_command="python3 ./tools/workspace_status_release.py" |
Christian Aistleitner | 95bfeba | 2020-06-11 11:29:50 +0200 | [diff] [blame] | 13 | # |
| 14 | # to your bazel command. So for example instead of |
| 15 | # |
| 16 | # bazel build release.war |
| 17 | # |
| 18 | # use |
| 19 | # |
David Ostrovsky | 2b5fe09 | 2021-03-03 11:52:30 +0100 | [diff] [blame] | 20 | # bazel build --workspace_status_command="python3 ./tools/workspace_status_release.py" release.war |
Christian Aistleitner | 95bfeba | 2020-06-11 11:29:50 +0200 | [diff] [blame] | 21 | # |
| 22 | # Alternatively, you can add |
| 23 | # |
David Ostrovsky | 2b5fe09 | 2021-03-03 11:52:30 +0100 | [diff] [blame] | 24 | # build --workspace_status_command="python3 ./tools/workspace_status_release.py" |
Christian Aistleitner | 95bfeba | 2020-06-11 11:29:50 +0200 | [diff] [blame] | 25 | # |
| 26 | # to `.bazelrc` in your home directory. |
| 27 | # |
| 28 | # If the script exits with non-zero code, it's considered as a failure |
| 29 | # and the output will be discarded. |
| 30 | |
| 31 | from __future__ import print_function |
| 32 | import os |
| 33 | import subprocess |
| 34 | import sys |
| 35 | import re |
| 36 | |
| 37 | ROOT = os.path.abspath(__file__) |
| 38 | while not os.path.exists(os.path.join(ROOT, 'WORKSPACE')): |
| 39 | ROOT = os.path.dirname(ROOT) |
| 40 | REVISION_CMD = ['git', 'describe', '--always', '--dirty'] |
| 41 | |
| 42 | |
| 43 | def run(command): |
| 44 | try: |
| 45 | return subprocess.check_output(command).strip().decode("utf-8") |
| 46 | except OSError as err: |
| 47 | print('could not invoke %s: %s' % (command[0], err), file=sys.stderr) |
| 48 | sys.exit(1) |
| 49 | except subprocess.CalledProcessError: |
| 50 | # ignore "not a git repository error" to report unknown version |
| 51 | return None |
| 52 | |
| 53 | |
| 54 | def revision_with_match(pattern=None, prefix=False, all_refs=False, |
| 55 | return_unmatched=False): |
| 56 | """Return a description of the current commit |
| 57 | |
| 58 | Keyword arguments: |
| 59 | pattern -- (Default: None) Use only refs that match this pattern. |
| 60 | prefix -- (Default: False) If True, the pattern is considered a prefix |
| 61 | and does not require an exact match. |
| 62 | all_refs -- (Default: False) If True, consider all refs, not just tags. |
| 63 | return_unmatched -- (Default: False) If False and a pattern is given that |
| 64 | cannot be matched, return the empty string. If True, return |
| 65 | the unmatched description nonetheless. |
| 66 | """ |
| 67 | |
| 68 | command = REVISION_CMD[:] |
| 69 | if pattern: |
| 70 | command += ['--match', pattern + ('*' if prefix else '')] |
| 71 | if all_refs: |
| 72 | command += ['--all', '--long'] |
| 73 | |
| 74 | description = run(command) |
| 75 | |
| 76 | if pattern and not return_unmatched and not description.startswith(pattern): |
| 77 | return '' |
| 78 | return description |
| 79 | |
| 80 | |
| 81 | def branch_with_match(pattern): |
| 82 | for ref_kind in ['origin/', 'gerrit/', '']: |
| 83 | description = revision_with_match(ref_kind + pattern, all_refs=True, |
| 84 | return_unmatched=True) |
| 85 | for cutoff in ['heads/', 'remotes/', ref_kind]: |
| 86 | if description.startswith(cutoff): |
| 87 | description = description[len(cutoff):] |
| 88 | if description.startswith(pattern): |
| 89 | return description |
| 90 | return '' |
| 91 | |
| 92 | |
| 93 | def revision(template=None): |
| 94 | if template: |
| 95 | # We use the version `v2.16.19-1-gec686a6352` as running example for the |
| 96 | # below comments. First, we split into ['v2', '16', '19'] |
| 97 | parts = template.split('-')[0].split('.') |
| 98 | |
| 99 | # Although we have releases with version tags containing 4 numbers, we |
| 100 | # treat only the first three numbers for simplicity. See discussion on |
| 101 | # Ib1681b2730cf2c443a3cb55fe6e282f6484e18de. |
| 102 | |
| 103 | if len(parts) >= 3: |
| 104 | # Match for v2.16.19 |
| 105 | version_part = '.'.join(parts[0:3]) |
| 106 | description = revision_with_match(version_part) |
| 107 | if description: |
| 108 | return description |
| 109 | |
| 110 | if len(parts) >= 2: |
| 111 | version_part = '.'.join(parts[0:2]) |
| 112 | |
| 113 | # Match for v2.16.* |
| 114 | description = revision_with_match(version_part + '.', prefix=True) |
| 115 | if description: |
| 116 | return description |
| 117 | |
| 118 | # Match for v2.16 |
| 119 | description = revision_with_match(version_part) |
| 120 | if description.startswith(version_part): |
| 121 | return description |
| 122 | |
| 123 | if template.startswith('v'): |
| 124 | # Match for stable-2.16 branches |
| 125 | branch = 'stable-' + version_part[1:] |
| 126 | description = branch_with_match(branch) |
| 127 | if description: |
| 128 | return description |
| 129 | |
| 130 | # None of the template based methods worked out, so we're falling back to |
| 131 | # generic matches. |
| 132 | |
| 133 | # Match for master branch |
| 134 | description = branch_with_match('master') |
| 135 | if description: |
| 136 | return description |
| 137 | |
| 138 | # Match for anything that looks like a version tag |
| 139 | description = revision_with_match('v[0-9].', return_unmatched=True) |
| 140 | if description.startswith('v'): |
| 141 | return description |
| 142 | |
| 143 | # Still no good tag, so we re-try without any matching |
| 144 | return revision_with_match() |
| 145 | |
| 146 | |
| 147 | # prints the stamps for the current working directory |
| 148 | def print_stamps_for_cwd(name, template): |
| 149 | workspace_status_script = os.path.join( |
| 150 | 'tools', 'workspace_status_release.py') |
| 151 | if os.path.isfile(workspace_status_script): |
| 152 | # directory has own workspace_status_command, so we use stamps from that |
David Ostrovsky | 2b5fe09 | 2021-03-03 11:52:30 +0100 | [diff] [blame] | 153 | for line in run(["python3", workspace_status_script]).split('\n'): |
Christian Aistleitner | 95bfeba | 2020-06-11 11:29:50 +0200 | [diff] [blame] | 154 | if re.search("^STABLE_[a-zA-Z0-9().:@/_ -]*$", line): |
| 155 | print(line) |
| 156 | else: |
| 157 | # directory lacks own workspace_status_command, so we create a default |
| 158 | # stamp |
| 159 | v = revision(template) |
| 160 | print('STABLE_BUILD_%s_LABEL %s' % (name.upper(), |
| 161 | v if v else 'unknown')) |
| 162 | |
| 163 | |
| 164 | # os.chdir is different from plain `cd` in shells in that it follows symlinks |
| 165 | # and does not update the PWD environment. So when using os.chdir to change into |
| 166 | # a symlinked directory from gerrit's `plugins` or `modules` directory, we |
| 167 | # cannot recover gerrit's directory. This prevents the plugins'/modules' |
| 168 | # `workspace_status_release.py` scripts to detect the name they were symlinked |
| 169 | # as (E.g.: it-* plugins sometimes get linked in more than once under different |
| 170 | # names) and to detect gerrit's root directory. To work around this problem, we |
| 171 | # mimic the `cd` of ordinary shells. By using this function, symlink information |
| 172 | # is preserved in the `PWD` environment variable (as it is for example also done |
| 173 | # in bash) and plugin/module `workspace_status_release.py` scripts can pick up |
| 174 | # the needed information from there. |
| 175 | def cd(absolute_path): |
| 176 | os.environ['PWD'] = absolute_path |
| 177 | os.chdir(absolute_path) |
| 178 | |
| 179 | |
| 180 | def print_stamps(): |
| 181 | cd(ROOT) |
| 182 | gerrit_version = revision() |
| 183 | print("STABLE_BUILD_GERRIT_LABEL %s" % gerrit_version) |
| 184 | for kind in ['modules', 'plugins']: |
| 185 | kind_dir = os.path.join(ROOT, kind) |
| 186 | for d in os.listdir(kind_dir) if os.path.isdir(kind_dir) else []: |
| 187 | p = os.path.join(kind_dir, d) |
| 188 | if os.path.isdir(p): |
| 189 | cd(p) |
| 190 | name = os.path.basename(p) |
| 191 | print_stamps_for_cwd(name, gerrit_version) |
| 192 | |
| 193 | |
| 194 | if __name__ == '__main__': |
| 195 | print_stamps() |