Merge pull request #3021 from qingyuanzNV/fix_opline_prepending_opfunction_with_pp
[platform/upstream/glslang.git] / build_info.py
1 #!/usr/bin/env python3
2
3 # Copyright (c) 2020 Google Inc.
4 #
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
8 #
9 #     http://www.apache.org/licenses/LICENSE-2.0
10 #
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.
16
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
25
26 usage = """{} emits a string to stdout or file with project version information.
27
28 args: <project-dir> [<input-string>] [-i <input-file>] [-o <output-file>]
29
30 Either <input-string> or -i <input-file> needs to be provided.
31
32 The tool will output the provided string or file content with the following
33 tokens substituted:
34
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".
47
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 """
51
52 def mkdir_p(directory):
53     """Make the directory, and all its ancestors as required.  Any of the
54     directories are allowed to already exist."""
55
56     if directory == "":
57         # We're being asked to make the current directory.
58         return
59
60     try:
61         os.makedirs(directory)
62     except OSError as e:
63         if e.errno == errno.EEXIST and os.path.isdir(directory):
64             pass
65         else:
66             raise
67
68
69 def command_output(cmd, directory):
70     """Runs a command in a directory and returns its standard output stream.
71
72     Captures the standard error stream.
73
74     Raises a RuntimeError if the command fails to launch or otherwise fails.
75     """
76     p = subprocess.Popen(cmd,
77                          cwd=directory,
78                          stdout=subprocess.PIPE,
79                          stderr=subprocess.PIPE)
80     (stdout, _) = p.communicate()
81     if p.returncode != 0:
82         raise RuntimeError('Failed to run %s in %s' % (cmd, directory))
83     return stdout
84
85
86 def deduce_software_version(directory):
87     """Returns a software version number parsed from the CHANGES.md file
88     in the given directory.
89
90     The CHANGES.md file describes most recent versions first.
91     """
92
93     # Match the first well-formed version-and-date line.
94     # Allow trailing whitespace in the checked-out source code has
95     # unexpected carriage returns on a linefeed-only system such as
96     # Linux.
97     pattern = re.compile(r'^#* +(\d+)\.(\d+)\.(\d+)(-\w+)? (\d\d\d\d-\d\d-\d\d)? *$')
98     changes_file = os.path.join(directory, 'CHANGES.md')
99     with open(changes_file, mode='r') as f:
100         for line in f.readlines():
101             match = pattern.match(line)
102             if match:
103                 flavor = match.group(4)
104                 if flavor == None:
105                     flavor = ""
106                 return {
107                     "major": match.group(1),
108                     "minor": match.group(2),
109                     "patch": match.group(3),
110                     "flavor": flavor.lstrip("-"),
111                     "-flavor": flavor,
112                     "date": match.group(5),
113                 }
114     raise Exception('No version number found in {}'.format(changes_file))
115
116
117 def describe(directory):
118     """Returns a string describing the current Git HEAD version as descriptively
119     as possible.
120
121     Runs 'git describe', or alternately 'git rev-parse HEAD', in directory.  If
122     successful, returns the output; otherwise returns 'unknown hash, <date>'."""
123     try:
124         # decode() is needed here for Python3 compatibility. In Python2,
125         # str and bytes are the same type, but not in Python3.
126         # Popen.communicate() returns a bytes instance, which needs to be
127         # decoded into text data first in Python3. And this decode() won't
128         # hurt Python2.
129         return command_output(['git', 'describe'], directory).rstrip().decode()
130     except:
131         try:
132             return command_output(
133                 ['git', 'rev-parse', 'HEAD'], directory).rstrip().decode()
134         except:
135             # This is the fallback case where git gives us no information,
136             # e.g. because the source tree might not be in a git tree.
137             # In this case, usually use a timestamp.  However, to ensure
138             # reproducible builds, allow the builder to override the wall
139             # clock time with environment variable SOURCE_DATE_EPOCH
140             # containing a (presumably) fixed timestamp.
141             timestamp = int(os.environ.get('SOURCE_DATE_EPOCH', time.time()))
142             formatted = datetime.datetime.utcfromtimestamp(timestamp).isoformat()
143             return 'unknown hash, {}'.format(formatted)
144
145 def parse_args():
146     directory = None
147     input_string = None
148     input_file = None
149     output_file = None
150
151     if len(sys.argv) < 2:
152         raise Exception("Invalid number of arguments")
153
154     directory = sys.argv[1]
155     i = 2
156
157     if not sys.argv[i].startswith("-"):
158         input_string = sys.argv[i]
159         i = i + 1
160
161     while i < len(sys.argv):
162         opt = sys.argv[i]
163         i = i + 1
164
165         if opt == "-i" or opt == "-o":
166             if i == len(sys.argv):
167                 raise Exception("Expected path after {}".format(opt))
168             val = sys.argv[i]
169             i = i + 1
170             if (opt == "-i"):
171                 input_file = val
172             elif (opt == "-o"):
173                 output_file = val
174             else:
175                 raise Exception("Unknown flag {}".format(opt))
176
177     return {
178         "directory": directory,
179         "input_string": input_string,
180         "input_file": input_file,
181         "output_file": output_file,
182     }
183
184 def main():
185     args = None
186     try:
187         args = parse_args()
188     except Exception as e:
189         print(e)
190         print("\nUsage:\n")
191         print(usage.format(sys.argv[0]))
192         sys.exit(1)
193
194     directory = args["directory"]
195     template = args["input_string"]
196     if template == None:
197         with open(args["input_file"], 'r') as f:
198             template = f.read()
199     output_file = args["output_file"]
200
201     software_version = deduce_software_version(directory)
202     commit = describe(directory)
203     output = template \
204         .replace("@major@", software_version["major"]) \
205         .replace("@minor@", software_version["minor"]) \
206         .replace("@patch@", software_version["patch"]) \
207         .replace("@flavor@", software_version["flavor"]) \
208         .replace("@-flavor@", software_version["-flavor"]) \
209         .replace("@date@", software_version["date"]) \
210         .replace("@commit@", commit)
211
212     if output_file is None:
213         print(output)
214     else:
215         mkdir_p(os.path.dirname(output_file))
216
217         if os.path.isfile(output_file):
218             with open(output_file, 'r') as f:
219                 if output == f.read():
220                     return
221
222         with open(output_file, 'w') as f:
223             f.write(output)
224
225 if __name__ == '__main__':
226     main()