1#!/usr/bin/env python 2 3# Copyright (c) 2016 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# Updates an output file with version info unless the new content is the same 18# as the existing content. 19# 20# Args: <spirv-tools_dir> <output-file> 21# 22# The output file will contain a line of text consisting of two C source syntax 23# string literals separated by a comma: 24# - The software version deduced from the CHANGES file in the given directory. 25# - A longer string with the project name, the software version number, and 26# git commit information for the directory. The commit information 27# is the output of "git describe" if that succeeds, or "git rev-parse HEAD" 28# if that succeeds, or otherwise a message containing the phrase 29# "unknown hash". 30# The string contents are escaped as necessary. 31 32import datetime 33import errno 34import os 35import os.path 36import re 37import subprocess 38import sys 39import time 40 41 42def mkdir_p(directory): 43 """Make the directory, and all its ancestors as required. Any of the 44 directories are allowed to already exist.""" 45 46 if directory == "": 47 # We're being asked to make the current directory. 48 return 49 50 try: 51 os.makedirs(directory) 52 except OSError as e: 53 if e.errno == errno.EEXIST and os.path.isdir(directory): 54 pass 55 else: 56 raise 57 58 59def command_output(cmd, directory): 60 """Runs a command in a directory and returns its standard output stream. 61 62 Captures the standard error stream. 63 64 Raises a RuntimeError if the command fails to launch or otherwise fails. 65 """ 66 p = subprocess.Popen(cmd, 67 cwd=directory, 68 stdout=subprocess.PIPE, 69 stderr=subprocess.PIPE) 70 (stdout, _) = p.communicate() 71 if p.returncode != 0: 72 raise RuntimeError('Failed to run %s in %s' % (cmd, directory)) 73 return stdout 74 75 76def deduce_software_version(directory): 77 """Returns a software version number parsed from the CHANGES file 78 in the given directory. 79 80 The CHANGES file describes most recent versions first. 81 """ 82 83 # Match the first well-formed version-and-date line. 84 # Allow trailing whitespace in the checked-out source code has 85 # unexpected carriage returns on a linefeed-only system such as 86 # Linux. 87 pattern = re.compile(r'^(v\d+\.\d+(-dev)?) \d\d\d\d-\d\d-\d\d\s*$') 88 changes_file = os.path.join(directory, 'CHANGES') 89 with open(changes_file, mode='r') as f: 90 for line in f.readlines(): 91 match = pattern.match(line) 92 if match: 93 return match.group(1) 94 raise Exception('No version number found in {}'.format(changes_file)) 95 96 97def describe(directory): 98 """Returns a string describing the current Git HEAD version as descriptively 99 as possible. 100 101 Runs 'git describe', or alternately 'git rev-parse HEAD', in directory. If 102 successful, returns the output; otherwise returns 'unknown hash, <date>'.""" 103 try: 104 # decode() is needed here for Python3 compatibility. In Python2, 105 # str and bytes are the same type, but not in Python3. 106 # Popen.communicate() returns a bytes instance, which needs to be 107 # decoded into text data first in Python3. And this decode() won't 108 # hurt Python2. 109 return command_output(['git', 'describe'], directory).rstrip().decode() 110 except: 111 try: 112 return command_output( 113 ['git', 'rev-parse', 'HEAD'], directory).rstrip().decode() 114 except: 115 # This is the fallback case where git gives us no information, 116 # e.g. because the source tree might not be in a git tree. 117 # In this case, usually use a timestamp. However, to ensure 118 # reproducible builds, allow the builder to override the wall 119 # clock time with environment variable SOURCE_DATE_EPOCH 120 # containing a (presumably) fixed timestamp. 121 timestamp = int(os.environ.get('SOURCE_DATE_EPOCH', time.time())) 122 formatted = datetime.datetime.utcfromtimestamp(timestamp).isoformat() 123 return 'unknown hash, {}'.format(formatted) 124 125 126def main(): 127 if len(sys.argv) != 3: 128 print('usage: {} <spirv-tools-dir> <output-file>'.format(sys.argv[0])) 129 sys.exit(1) 130 131 output_file = sys.argv[2] 132 mkdir_p(os.path.dirname(output_file)) 133 134 software_version = deduce_software_version(sys.argv[1]) 135 new_content = '"{}", "SPIRV-Tools {} {}"\n'.format( 136 software_version, software_version, 137 describe(sys.argv[1]).replace('"', '\\"')) 138 139 if os.path.isfile(output_file): 140 with open(output_file, 'r') as f: 141 if new_content == f.read(): 142 return 143 144 with open(output_file, 'w') as f: 145 f.write(new_content) 146 147if __name__ == '__main__': 148 main() 149