1#!/usr/bin/env python3
2# Copyright (C) 2020 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15"""
16Writes the perfetto_version{.gen.h, .ts} files.
17
18This tool is run as part of a genrule from GN, SoonG and Bazel builds. It
19generates a source header (or in the case of --ts_out a TypeScript file) that
20contains:
21- The version number (e.g. v9.0) obtained parsing the CHANGELOG file.
22- The git HEAD's commit-ish (e.g. 6b330b772b0e973f79c70ba2e9bb2b0110c6715d)
23- The number of CLs from the release tag to HEAD.
24
25The latter is concatenated to the version number to distinguish builds made
26fully from release tags (e.g., v9.0.0) vs builds made from the main branch which
27are N cls ahead of the latest monthly release (e.g., v9.0.42).
28"""
29
30import argparse
31import os
32import re
33import sys
34import subprocess
35
36# Note: PROJECT_ROOT is not accurate in bazel builds, where this script is
37# executed in the bazel sandbox.
38PROJECT_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
39SCM_REV_NOT_AVAILABLE = 'N/A'
40
41
42def get_latest_release(changelog_path):
43  """Returns a string like 'v9.0'.
44
45  It does so by searching the latest version mentioned in the CHANGELOG."""
46  if not changelog_path:
47    if os.path.exists('CHANGELOG'):
48      changelog_path = 'CHANGELOG'
49    else:
50      changelog_path = os.path.join(PROJECT_ROOT, 'CHANGELOG')
51  with open(changelog_path) as f:
52    for line in f.readlines():
53      m = re.match('^(v\d+[.]\d+)\s.*$', line)
54      if m is not None:
55        return m.group(1)
56  raise Exception('Failed to fetch Perfetto version from %s' % changelog_path)
57
58
59def get_git_info(last_release_tag):
60  """Returns a tuple ('deadbeef', '1234').
61
62  The first value is the SHA1 of the HEAD. The second is the number of CLs from
63  the passed |last_release_tag| to HEAD."""
64  commit_sha1 = SCM_REV_NOT_AVAILABLE
65  commits_since_release = ''
66  git_dir = os.path.join(PROJECT_ROOT, '.git')
67  if os.path.exists(git_dir):
68    try:
69      commit_sha1 = subprocess.check_output(['git', 'rev-parse', 'HEAD'],
70                                            cwd=PROJECT_ROOT).strip().decode()
71      with open(os.devnull, 'wb') as devnull:
72        commits_since_release = subprocess.check_output(
73            [
74                'git', 'rev-list', '--count',
75                'refs/tags/%s..HEAD' % last_release_tag
76            ],
77            cwd=PROJECT_ROOT,
78            stderr=devnull).strip().decode()
79    except subprocess.CalledProcessError:
80      pass
81
82  return (commit_sha1, commits_since_release)
83
84
85def write_if_unchanged(path, content):
86  prev_content = None
87  if os.path.exists(path):
88    with open(path, 'r') as fprev:
89      prev_content = fprev.read()
90  if prev_content == content:
91    return 0
92  with open(path, 'w') as fout:
93    fout.write(content)
94
95
96def main():
97  parser = argparse.ArgumentParser()
98  parser.add_argument(
99      '--no_git',
100      action='store_true',
101      help='Skips running git rev-parse, emits only the version from CHANGELOG')
102  parser.add_argument('--cpp_out', help='Path of the generated .h file.')
103  parser.add_argument('--ts_out', help='Path of the generated .ts file.')
104  parser.add_argument('--stdout', help='Write to stdout', action='store_true')
105  parser.add_argument('--changelog', help='Path to CHANGELOG.')
106  args = parser.parse_args()
107
108  release = get_latest_release(args.changelog)
109  if args.no_git:
110    git_sha1, commits_since_release = (SCM_REV_NOT_AVAILABLE, '')
111  else:
112    git_sha1, commits_since_release = get_git_info(release)
113
114  # Try to compute the number of commits since the last release. This can fail
115  # in some environments (e.g. in android builds) because the bots pull only
116  # the main branch and don't pull the whole list of tags.
117  if commits_since_release:
118    version = '%s.%s' % (release, commits_since_release)  # e.g., 'v9.0.42'.
119  else:
120    version = release  # e.g., 'v9.0'.
121
122  if args.cpp_out:
123    guard = '%s_' % args.cpp_out.upper()
124    guard = re.sub(r'[^\w]', '_', guard)
125    lines = []
126    lines.append('// Generated by %s' % os.path.basename(__file__))
127    lines.append('')
128    lines.append('#ifndef %s' % guard)
129    lines.append('#define %s' % guard)
130    lines.append('')
131    lines.append('#define PERFETTO_VERSION_STRING() "%s"' % version)
132    lines.append('#define PERFETTO_VERSION_SCM_REVISION() "%s"' % git_sha1)
133    lines.append('')
134    lines.append('#endif  // %s' % guard)
135    lines.append('')
136    content = '\n'.join(lines)
137    write_if_unchanged(args.cpp_out, content)
138
139  if args.ts_out:
140    lines = []
141    lines.append('export const VERSION = "%s";' % version)
142    lines.append('export const SCM_REVISION = "%s";' % git_sha1)
143    content = '\n'.join(lines)
144    write_if_unchanged(args.ts_out, content)
145
146  if args.stdout:
147    print(version)
148
149
150if __name__ == '__main__':
151  sys.exit(main())
152