1#!/usr/bin/env python3 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 Returns (False, None) if the command fails to launch or otherwise fails. 66 """ 67 try: 68 # Set shell=True on Windows so that Chromium's git.bat can be found when 69 # 'git' is invoked. 70 p = subprocess.Popen(cmd, 71 cwd=directory, 72 stdout=subprocess.PIPE, 73 stderr=subprocess.PIPE, 74 shell=os.name == 'nt') 75 (stdout, _) = p.communicate() 76 if p.returncode != 0: 77 return False, None 78 except Exception as e: 79 return False, None 80 return p.returncode == 0, stdout 81 82def deduce_software_version(changes_file): 83 """Returns a tuple (success, software version number) parsed from the 84 given CHANGES file. 85 86 Success is set to True if the software version could be deduced. 87 Software version is undefined if success if False. 88 Function expects the CHANGES file to describes most recent versions first. 89 """ 90 91 # Match the first well-formed version-and-date line 92 # Allow trailing whitespace in the checked-out source code has 93 # unexpected carriage returns on a linefeed-only system such as 94 # Linux. 95 pattern = re.compile(r'^(v\d+\.\d+(-dev)?) \d\d\d\d-\d\d-\d\d\s*$') 96 with open(changes_file, mode='r') as f: 97 for line in f.readlines(): 98 match = pattern.match(line) 99 if match: 100 return True, match.group(1) 101 return False, None 102 103 104def describe(repo_path): 105 """Returns a string describing the current Git HEAD version as descriptively 106 as possible. 107 108 Runs 'git describe', or alternately 'git rev-parse HEAD', in directory. If 109 successful, returns the output; otherwise returns 'unknown hash, <date>'.""" 110 111 # if we're in a git repository, attempt to extract version info 112 success, output = command_output(["git", "rev-parse", "--show-toplevel"], repo_path) 113 if success: 114 success, output = command_output(["git", "describe", "--tags", "--match=v*", "--long"], repo_path) 115 if not success: 116 success, 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 or 128 # git is not available on the system. 129 # In this case, usually use a timestamp. However, to ensure 130 # reproducible builds, allow the builder to override the wall 131 # clock time with environment variable SOURCE_DATE_EPOCH 132 # containing a (presumably) fixed timestamp. 133 timestamp = int(os.environ.get('SOURCE_DATE_EPOCH', time.time())) 134 iso_date = datetime.datetime.fromtimestamp(timestamp, datetime.timezone.utc).isoformat() 135 return "unknown hash, {}".format(iso_date) 136 137def main(): 138 FORMAT = '%(asctime)s %(message)s' 139 logging.basicConfig(format="[%(asctime)s][%(levelname)-8s] %(message)s", datefmt="%H:%M:%S") 140 if len(sys.argv) != 3: 141 logging.error("usage: {} <repo-path> <output-file>".format(sys.argv[0])) 142 sys.exit(1) 143 144 changes_file_path = os.path.realpath(sys.argv[1]) 145 output_file_path = sys.argv[2] 146 147 success, version = deduce_software_version(changes_file_path) 148 if not success: 149 logging.error("Could not deduce latest release version from {}.".format(changes_file_path)) 150 sys.exit(1) 151 152 repo_path = os.path.dirname(changes_file_path) 153 description = describe(repo_path) 154 content = OUTPUT_FORMAT.format(version_tag=version, description=description) 155 156 # Escape file content. 157 content.replace('"', '\\"') 158 159 if os.path.isfile(output_file_path): 160 with open(output_file_path, 'r') as f: 161 if content == f.read(): 162 return 163 164 mkdir_p(os.path.dirname(output_file_path)) 165 with open(output_file_path, 'w') as f: 166 f.write(content) 167 168if __name__ == '__main__': 169 main() 170