kokoro: use ndk-r27c
[glslang.git] / build_info.py
blobc8163c7a2c93db2fb2758bdd48a37ef65e2a50db
1 #!/usr/bin/env python3
3 # Copyright (c) 2020 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 import datetime
18 import errno
19 import os
20 import os.path
21 import re
22 import subprocess
23 import sys
24 import time
26 usage = """{} emits a string to stdout or file with project version information.
28 args: <project-dir> [<input-string>] [-i <input-file>] [-o <output-file>]
30 Either <input-string> or -i <input-file> needs to be provided.
32 The tool will output the provided string or file content with the following
33 tokens substituted:
35 <major> - The major version point parsed from the CHANGES.md file.
36 <minor> - The minor version point parsed from the CHANGES.md file.
37 <patch> - The point version point parsed from the CHANGES.md file.
38 <flavor> - The optional dash suffix parsed from the CHANGES.md file (excluding
39 dash prefix).
40 <-flavor> - The optional dash suffix parsed from the CHANGES.md file (including
41 dash prefix).
42 <date> - The optional date of the release in the form YYYY-MM-DD
43 <commit> - The git commit information for the directory taken from
44 "git describe" if that succeeds, or "git rev-parse HEAD"
45 if that succeeds, or otherwise a message containing the phrase
46 "unknown hash".
48 -o is an optional flag for writing the output string to the given file. If
49 ommitted then the string is printed to stdout.
50 """
52 try:
53 utc = datetime.timezone.utc
54 except AttributeError:
55 # Python 2? In datetime.date.today().year? Yes.
56 class UTC(datetime.tzinfo):
57 ZERO = datetime.timedelta(0)
59 def utcoffset(self, dt):
60 return self.ZERO
62 def tzname(self, dt):
63 return "UTC"
65 def dst(self, dt):
66 return self.ZERO
67 utc = UTC()
70 def mkdir_p(directory):
71 """Make the directory, and all its ancestors as required. Any of the
72 directories are allowed to already exist."""
74 if directory == "":
75 # We're being asked to make the current directory.
76 return
78 try:
79 os.makedirs(directory)
80 except OSError as e:
81 if e.errno == errno.EEXIST and os.path.isdir(directory):
82 pass
83 else:
84 raise
87 def command_output(cmd, directory):
88 """Runs a command in a directory and returns its standard output stream.
90 Captures the standard error stream.
92 Raises a RuntimeError if the command fails to launch or otherwise fails.
93 """
94 p = subprocess.Popen(cmd,
95 cwd=directory,
96 stdout=subprocess.PIPE,
97 stderr=subprocess.PIPE)
98 (stdout, _) = p.communicate()
99 if p.returncode != 0:
100 raise RuntimeError('Failed to run %s in %s' % (cmd, directory))
101 return stdout
104 def deduce_software_version(directory):
105 """Returns a software version number parsed from the CHANGES.md file
106 in the given directory.
108 The CHANGES.md file describes most recent versions first.
111 # Match the first well-formed version-and-date line.
112 # Allow trailing whitespace in the checked-out source code has
113 # unexpected carriage returns on a linefeed-only system such as
114 # Linux.
115 pattern = re.compile(r'^#* +(\d+)\.(\d+)\.(\d+)(-\w+)? (\d\d\d\d-\d\d-\d\d)? *$')
116 changes_file = os.path.join(directory, 'CHANGES.md')
117 with open(changes_file, mode='r') as f:
118 for line in f.readlines():
119 match = pattern.match(line)
120 if match:
121 flavor = match.group(4)
122 if flavor == None:
123 flavor = ""
124 return {
125 "major": match.group(1),
126 "minor": match.group(2),
127 "patch": match.group(3),
128 "flavor": flavor.lstrip("-"),
129 "-flavor": flavor,
130 "date": match.group(5),
132 raise Exception('No version number found in {}'.format(changes_file))
135 def describe(directory):
136 """Returns a string describing the current Git HEAD version as descriptively
137 as possible.
139 Runs 'git describe', or alternately 'git rev-parse HEAD', in directory. If
140 successful, returns the output; otherwise returns 'unknown hash, <date>'."""
141 try:
142 # decode() is needed here for Python3 compatibility. In Python2,
143 # str and bytes are the same type, but not in Python3.
144 # Popen.communicate() returns a bytes instance, which needs to be
145 # decoded into text data first in Python3. And this decode() won't
146 # hurt Python2.
147 return command_output(['git', 'describe'], directory).rstrip().decode()
148 except:
149 try:
150 return command_output(
151 ['git', 'rev-parse', 'HEAD'], directory).rstrip().decode()
152 except:
153 # This is the fallback case where git gives us no information,
154 # e.g. because the source tree might not be in a git tree.
155 # In this case, usually use a timestamp. However, to ensure
156 # reproducible builds, allow the builder to override the wall
157 # clock time with environment variable SOURCE_DATE_EPOCH
158 # containing a (presumably) fixed timestamp.
159 timestamp = int(os.environ.get('SOURCE_DATE_EPOCH', time.time()))
160 formatted = datetime.datetime.fromtimestamp(timestamp, utc).isoformat()
161 return 'unknown hash, {}'.format(formatted)
163 def parse_args():
164 directory = None
165 input_string = None
166 input_file = None
167 output_file = None
169 if len(sys.argv) < 2:
170 raise Exception("Invalid number of arguments")
172 directory = sys.argv[1]
173 i = 2
175 if not sys.argv[i].startswith("-"):
176 input_string = sys.argv[i]
177 i = i + 1
179 while i < len(sys.argv):
180 opt = sys.argv[i]
181 i = i + 1
183 if opt == "-i" or opt == "-o":
184 if i == len(sys.argv):
185 raise Exception("Expected path after {}".format(opt))
186 val = sys.argv[i]
187 i = i + 1
188 if (opt == "-i"):
189 input_file = val
190 elif (opt == "-o"):
191 output_file = val
192 else:
193 raise Exception("Unknown flag {}".format(opt))
195 return {
196 "directory": directory,
197 "input_string": input_string,
198 "input_file": input_file,
199 "output_file": output_file,
202 def main():
203 args = None
204 try:
205 args = parse_args()
206 except Exception as e:
207 print(e)
208 print("\nUsage:\n")
209 print(usage.format(sys.argv[0]))
210 sys.exit(1)
212 directory = args["directory"]
213 template = args["input_string"]
214 if template == None:
215 with open(args["input_file"], 'r') as f:
216 template = f.read()
217 output_file = args["output_file"]
219 software_version = deduce_software_version(directory)
220 commit = describe(directory)
221 output = template \
222 .replace("@major@", software_version["major"]) \
223 .replace("@minor@", software_version["minor"]) \
224 .replace("@patch@", software_version["patch"]) \
225 .replace("@flavor@", software_version["flavor"]) \
226 .replace("@-flavor@", software_version["-flavor"]) \
227 .replace("@date@", software_version["date"]) \
228 .replace("@commit@", commit)
230 if output_file is None:
231 print(output)
232 else:
233 mkdir_p(os.path.dirname(output_file))
235 if os.path.isfile(output_file):
236 with open(output_file, 'r') as f:
237 if output == f.read():
238 return
240 with open(output_file, 'w') as f:
241 f.write(output)
243 if __name__ == '__main__':
244 main()