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