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: <changes-file> <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 given CHANGES file. 25# - A longer string with the project name, the software version number, and 26# git commit information for the CHANGES file's directory. The commit 27# information is the output of "git describe" if that succeeds, or "git 28# rev-parse HEAD" if that succeeds, or otherwise a message containing the 29# phrase "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(changes_file): 77 """Returns a software version number parsed from the given CHANGES file. 78 79 The CHANGES file describes most recent versions first. 80 """ 81 82 # Match the first well-formed version-and-date line. 83 # Allow trailing whitespace in the checked-out source code has 84 # unexpected carriage returns on a linefeed-only system such as 85 # Linux. 86 pattern = re.compile(r'^(v\d+\.\d+(-dev)?) \d\d\d\d-\d\d-\d\d\s*$') 87 with open(changes_file, mode='r') as f: 88 for line in f.readlines(): 89 match = pattern.match(line) 90 if match: 91 return match.group(1) 92 raise Exception('No version number found in {}'.format(changes_file)) 93 94 95def describe(directory): 96 """Returns a string describing the current Git HEAD version as descriptively 97 as possible. 98 99 Runs 'git describe', or alternately 'git rev-parse HEAD', in directory. If 100 successful, returns the output; otherwise returns 'unknown hash, <date>'.""" 101 try: 102 # decode() is needed here for Python3 compatibility. In Python2, 103 # str and bytes are the same type, but not in Python3. 104 # Popen.communicate() returns a bytes instance, which needs to be 105 # decoded into text data first in Python3. And this decode() won't 106 # hurt Python2. 107 return command_output(['git', 'describe'], directory).rstrip().decode() 108 except: 109 try: 110 return command_output( 111 ['git', 'rev-parse', 'HEAD'], directory).rstrip().decode() 112 except: 113 # This is the fallback case where git gives us no information, 114 # e.g. because the source tree might not be in a git tree. 115 # In this case, usually use a timestamp. However, to ensure 116 # reproducible builds, allow the builder to override the wall 117 # clock time with environment variable SOURCE_DATE_EPOCH 118 # containing a (presumably) fixed timestamp. 119 timestamp = int(os.environ.get('SOURCE_DATE_EPOCH', time.time())) 120 formatted = datetime.datetime.utcfromtimestamp(timestamp).isoformat() 121 return 'unknown hash, {}'.format(formatted) 122 123 124def main(): 125 if len(sys.argv) != 3: 126 print('usage: {} <changes-files> <output-file>'.format(sys.argv[0])) 127 sys.exit(1) 128 129 output_file = sys.argv[2] 130 mkdir_p(os.path.dirname(output_file)) 131 132 software_version = deduce_software_version(sys.argv[1]) 133 directory = os.path.dirname(sys.argv[1]) 134 new_content = '"{}", "SPIRV-Tools {} {}"\n'.format( 135 software_version, software_version, 136 describe(directory).replace('"', '\\"')) 137 138 if os.path.isfile(output_file): 139 with open(output_file, 'r') as f: 140 if new_content == f.read(): 141 return 142 143 with open(output_file, 'w') as f: 144 f.write(new_content) 145 146if __name__ == '__main__': 147 main() 148