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 logging 39import sys 40import time 41 42# Format of the output generated by this script. Example: 43# "v2023.1", "SPIRV-Tools v2023.1 0fc5526f2b01a0cc89192c10cf8bef77f1007a62, 2023-01-18T14:51:49" 44OUTPUT_FORMAT = '"{version_tag}", "SPIRV-Tools {version_tag} {description}"\n' 45 46def mkdir_p(directory): 47 """Make the directory, and all its ancestors as required. Any of the 48 directories are allowed to already exist.""" 49 50 if directory == "": 51 # We're being asked to make the current directory. 52 return 53 54 try: 55 os.makedirs(directory) 56 except OSError as e: 57 if e.errno == errno.EEXIST and os.path.isdir(directory): 58 pass 59 else: 60 raise 61 62def command_output(cmd, directory): 63 """Runs a command in a directory and returns its standard output stream. 64 65 Captures the standard error stream. 66 67 Raises a RuntimeError if the command fails to launch or otherwise fails. 68 """ 69 try: 70 # Set shell=True on Windows so that Chromium's git.bat can be found when 71 # 'git' is invoked. 72 p = subprocess.Popen(cmd, 73 cwd=directory, 74 stdout=subprocess.PIPE, 75 stderr=subprocess.PIPE, 76 shell=os.name == 'nt') 77 (stdout, stderr) = p.communicate() 78 if p.returncode != 0: 79 logging.error('Failed to run "{}" in "{}": {}'.format(cmd, directory, stderr.decode())) 80 except Exception as e: 81 logging.error('Failed to run "{}" in "{}": {}'.format(cmd, directory, str(e))) 82 return False, None 83 return p.returncode == 0, stdout 84 85def deduce_software_version(changes_file): 86 """Returns a tuple (success, software version number) parsed from the 87 given CHANGES file. 88 89 Success is set to True if the software version could be deduced. 90 Software version is undefined if success if False. 91 Function expects the CHANGES file to describes most recent versions first. 92 """ 93 94 # Match the first well-formed version-and-date line 95 # Allow trailing whitespace in the checked-out source code has 96 # unexpected carriage returns on a linefeed-only system such as 97 # Linux. 98 pattern = re.compile(r'^(v\d+\.\d+(-dev)?) \d\d\d\d-\d\d-\d\d\s*$') 99 with open(changes_file, mode='r') as f: 100 for line in f.readlines(): 101 match = pattern.match(line) 102 if match: 103 return True, match.group(1) 104 return False, None 105 106 107def describe(repo_path): 108 """Returns a string describing the current Git HEAD version as descriptively 109 as possible. 110 111 Runs 'git describe', or alternately 'git rev-parse HEAD', in directory. If 112 successful, returns the output; otherwise returns 'unknown hash, <date>'.""" 113 114 success, output = command_output(['git', 'describe'], repo_path) 115 if not success: 116 output = command_output(['git', 'rev-parse', 'HEAD'], repo_path) 117 118 if success: 119 # decode() is needed here for Python3 compatibility. In Python2, 120 # str and bytes are the same type, but not in Python3. 121 # Popen.communicate() returns a bytes instance, which needs to be 122 # decoded into text data first in Python3. And this decode() won't 123 # hurt Python2. 124 return output.rstrip().decode() 125 126 # This is the fallback case where git gives us no information, 127 # e.g. because the source tree might not be in a git tree. 128 # In this case, usually use a timestamp. However, to ensure 129 # reproducible builds, allow the builder to override the wall 130 # clock time with environment variable SOURCE_DATE_EPOCH 131 # containing a (presumably) fixed timestamp. 132 timestamp = int(os.environ.get('SOURCE_DATE_EPOCH', time.time())) 133 iso_date = datetime.datetime.utcfromtimestamp(timestamp).isoformat() 134 return "unknown hash, {}".format(iso_date) 135 136def main(): 137 FORMAT = '%(asctime)s %(message)s' 138 logging.basicConfig(format="[%(asctime)s][%(levelname)-8s] %(message)s", datefmt="%H:%M:%S") 139 if len(sys.argv) != 3: 140 logging.error("usage: {} <repo-path> <output-file>".format(sys.argv[0])) 141 sys.exit(1) 142 143 changes_file_path = os.path.realpath(sys.argv[1]) 144 output_file_path = sys.argv[2] 145 146 success, version = deduce_software_version(changes_file_path) 147 if not success: 148 logging.error("Could not deduce latest release version from {}.".format(changes_file_path)) 149 sys.exit(1) 150 151 repo_path = os.path.dirname(changes_file_path) 152 description = describe(repo_path) 153 content = OUTPUT_FORMAT.format(version_tag=version, description=description) 154 155 # Escape file content. 156 content.replace('"', '\\"') 157 158 if os.path.isfile(output_file_path): 159 with open(output_file_path, 'r') as f: 160 if content == f.read(): 161 return 162 163 mkdir_p(os.path.dirname(output_file_path)) 164 with open(output_file_path, 'w') as f: 165 f.write(content) 166 167if __name__ == '__main__': 168 main() 169