1617a3babSopenharmony_ci#!/usr/bin/env python3 2617a3babSopenharmony_ci 3617a3babSopenharmony_ci# Copyright (c) 2020 Google Inc. 4617a3babSopenharmony_ci# 5617a3babSopenharmony_ci# Licensed under the Apache License, Version 2.0 (the "License"); 6617a3babSopenharmony_ci# you may not use this file except in compliance with the License. 7617a3babSopenharmony_ci# You may obtain a copy of the License at 8617a3babSopenharmony_ci# 9617a3babSopenharmony_ci# http://www.apache.org/licenses/LICENSE-2.0 10617a3babSopenharmony_ci# 11617a3babSopenharmony_ci# Unless required by applicable law or agreed to in writing, software 12617a3babSopenharmony_ci# distributed under the License is distributed on an "AS IS" BASIS, 13617a3babSopenharmony_ci# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14617a3babSopenharmony_ci# See the License for the specific language governing permissions and 15617a3babSopenharmony_ci# limitations under the License. 16617a3babSopenharmony_ci 17617a3babSopenharmony_ciimport datetime 18617a3babSopenharmony_ciimport errno 19617a3babSopenharmony_ciimport os 20617a3babSopenharmony_ciimport os.path 21617a3babSopenharmony_ciimport re 22617a3babSopenharmony_ciimport subprocess 23617a3babSopenharmony_ciimport sys 24617a3babSopenharmony_ciimport time 25617a3babSopenharmony_ci 26617a3babSopenharmony_ciusage = """{} emits a string to stdout or file with project version information. 27617a3babSopenharmony_ci 28617a3babSopenharmony_ciargs: <project-dir> [<input-string>] [-i <input-file>] [-o <output-file>] 29617a3babSopenharmony_ci 30617a3babSopenharmony_ciEither <input-string> or -i <input-file> needs to be provided. 31617a3babSopenharmony_ci 32617a3babSopenharmony_ciThe tool will output the provided string or file content with the following 33617a3babSopenharmony_citokens substituted: 34617a3babSopenharmony_ci 35617a3babSopenharmony_ci <major> - The major version point parsed from the CHANGES.md file. 36617a3babSopenharmony_ci <minor> - The minor version point parsed from the CHANGES.md file. 37617a3babSopenharmony_ci <patch> - The point version point parsed from the CHANGES.md file. 38617a3babSopenharmony_ci <flavor> - The optional dash suffix parsed from the CHANGES.md file (excluding 39617a3babSopenharmony_ci dash prefix). 40617a3babSopenharmony_ci <-flavor> - The optional dash suffix parsed from the CHANGES.md file (including 41617a3babSopenharmony_ci dash prefix). 42617a3babSopenharmony_ci <date> - The optional date of the release in the form YYYY-MM-DD 43617a3babSopenharmony_ci <commit> - The git commit information for the directory taken from 44617a3babSopenharmony_ci "git describe" if that succeeds, or "git rev-parse HEAD" 45617a3babSopenharmony_ci if that succeeds, or otherwise a message containing the phrase 46617a3babSopenharmony_ci "unknown hash". 47617a3babSopenharmony_ci 48617a3babSopenharmony_ci-o is an optional flag for writing the output string to the given file. If 49617a3babSopenharmony_ci ommitted then the string is printed to stdout. 50617a3babSopenharmony_ci""" 51617a3babSopenharmony_ci 52617a3babSopenharmony_cidef mkdir_p(directory): 53617a3babSopenharmony_ci """Make the directory, and all its ancestors as required. Any of the 54617a3babSopenharmony_ci directories are allowed to already exist.""" 55617a3babSopenharmony_ci 56617a3babSopenharmony_ci if directory == "": 57617a3babSopenharmony_ci # We're being asked to make the current directory. 58617a3babSopenharmony_ci return 59617a3babSopenharmony_ci 60617a3babSopenharmony_ci try: 61617a3babSopenharmony_ci os.makedirs(directory) 62617a3babSopenharmony_ci except OSError as e: 63617a3babSopenharmony_ci if e.errno == errno.EEXIST and os.path.isdir(directory): 64617a3babSopenharmony_ci pass 65617a3babSopenharmony_ci else: 66617a3babSopenharmony_ci raise 67617a3babSopenharmony_ci 68617a3babSopenharmony_ci 69617a3babSopenharmony_cidef command_output(cmd, directory): 70617a3babSopenharmony_ci """Runs a command in a directory and returns its standard output stream. 71617a3babSopenharmony_ci 72617a3babSopenharmony_ci Captures the standard error stream. 73617a3babSopenharmony_ci 74617a3babSopenharmony_ci Raises a RuntimeError if the command fails to launch or otherwise fails. 75617a3babSopenharmony_ci """ 76617a3babSopenharmony_ci p = subprocess.Popen(cmd, 77617a3babSopenharmony_ci cwd=directory, 78617a3babSopenharmony_ci stdout=subprocess.PIPE, 79617a3babSopenharmony_ci stderr=subprocess.PIPE) 80617a3babSopenharmony_ci (stdout, _) = p.communicate() 81617a3babSopenharmony_ci if p.returncode != 0: 82617a3babSopenharmony_ci raise RuntimeError('Failed to run %s in %s' % (cmd, directory)) 83617a3babSopenharmony_ci return stdout 84617a3babSopenharmony_ci 85617a3babSopenharmony_ci 86617a3babSopenharmony_cidef deduce_software_version(directory): 87617a3babSopenharmony_ci """Returns a software version number parsed from the CHANGES.md file 88617a3babSopenharmony_ci in the given directory. 89617a3babSopenharmony_ci 90617a3babSopenharmony_ci The CHANGES.md file describes most recent versions first. 91617a3babSopenharmony_ci """ 92617a3babSopenharmony_ci 93617a3babSopenharmony_ci # Match the first well-formed version-and-date line. 94617a3babSopenharmony_ci # Allow trailing whitespace in the checked-out source code has 95617a3babSopenharmony_ci # unexpected carriage returns on a linefeed-only system such as 96617a3babSopenharmony_ci # Linux. 97617a3babSopenharmony_ci pattern = re.compile(r'^#* +(\d+)\.(\d+)\.(\d+)(-\w+)? (\d\d\d\d-\d\d-\d\d)? *$') 98617a3babSopenharmony_ci changes_file = os.path.join(directory, 'CHANGES.md') 99617a3babSopenharmony_ci with open(changes_file, mode='r') as f: 100617a3babSopenharmony_ci for line in f.readlines(): 101617a3babSopenharmony_ci match = pattern.match(line) 102617a3babSopenharmony_ci if match: 103617a3babSopenharmony_ci flavor = match.group(4) 104617a3babSopenharmony_ci if flavor == None: 105617a3babSopenharmony_ci flavor = "" 106617a3babSopenharmony_ci return { 107617a3babSopenharmony_ci "major": match.group(1), 108617a3babSopenharmony_ci "minor": match.group(2), 109617a3babSopenharmony_ci "patch": match.group(3), 110617a3babSopenharmony_ci "flavor": flavor.lstrip("-"), 111617a3babSopenharmony_ci "-flavor": flavor, 112617a3babSopenharmony_ci "date": match.group(5), 113617a3babSopenharmony_ci } 114617a3babSopenharmony_ci raise Exception('No version number found in {}'.format(changes_file)) 115617a3babSopenharmony_ci 116617a3babSopenharmony_ci 117617a3babSopenharmony_cidef describe(directory): 118617a3babSopenharmony_ci """Returns a string describing the current Git HEAD version as descriptively 119617a3babSopenharmony_ci as possible. 120617a3babSopenharmony_ci 121617a3babSopenharmony_ci Runs 'git describe', or alternately 'git rev-parse HEAD', in directory. If 122617a3babSopenharmony_ci successful, returns the output; otherwise returns 'unknown hash, <date>'.""" 123617a3babSopenharmony_ci try: 124617a3babSopenharmony_ci # decode() is needed here for Python3 compatibility. In Python2, 125617a3babSopenharmony_ci # str and bytes are the same type, but not in Python3. 126617a3babSopenharmony_ci # Popen.communicate() returns a bytes instance, which needs to be 127617a3babSopenharmony_ci # decoded into text data first in Python3. And this decode() won't 128617a3babSopenharmony_ci # hurt Python2. 129617a3babSopenharmony_ci return command_output(['git', 'describe'], directory).rstrip().decode() 130617a3babSopenharmony_ci except: 131617a3babSopenharmony_ci try: 132617a3babSopenharmony_ci return command_output( 133617a3babSopenharmony_ci ['git', 'rev-parse', 'HEAD'], directory).rstrip().decode() 134617a3babSopenharmony_ci except: 135617a3babSopenharmony_ci # This is the fallback case where git gives us no information, 136617a3babSopenharmony_ci # e.g. because the source tree might not be in a git tree. 137617a3babSopenharmony_ci # In this case, usually use a timestamp. However, to ensure 138617a3babSopenharmony_ci # reproducible builds, allow the builder to override the wall 139617a3babSopenharmony_ci # clock time with environment variable SOURCE_DATE_EPOCH 140617a3babSopenharmony_ci # containing a (presumably) fixed timestamp. 141617a3babSopenharmony_ci timestamp = int(os.environ.get('SOURCE_DATE_EPOCH', time.time())) 142617a3babSopenharmony_ci formatted = datetime.datetime.utcfromtimestamp(timestamp).isoformat() 143617a3babSopenharmony_ci return 'unknown hash, {}'.format(formatted) 144617a3babSopenharmony_ci 145617a3babSopenharmony_cidef parse_args(): 146617a3babSopenharmony_ci directory = None 147617a3babSopenharmony_ci input_string = None 148617a3babSopenharmony_ci input_file = None 149617a3babSopenharmony_ci output_file = None 150617a3babSopenharmony_ci 151617a3babSopenharmony_ci if len(sys.argv) < 2: 152617a3babSopenharmony_ci raise Exception("Invalid number of arguments") 153617a3babSopenharmony_ci 154617a3babSopenharmony_ci directory = sys.argv[1] 155617a3babSopenharmony_ci i = 2 156617a3babSopenharmony_ci 157617a3babSopenharmony_ci if not sys.argv[i].startswith("-"): 158617a3babSopenharmony_ci input_string = sys.argv[i] 159617a3babSopenharmony_ci i = i + 1 160617a3babSopenharmony_ci 161617a3babSopenharmony_ci while i < len(sys.argv): 162617a3babSopenharmony_ci opt = sys.argv[i] 163617a3babSopenharmony_ci i = i + 1 164617a3babSopenharmony_ci 165617a3babSopenharmony_ci if opt == "-i" or opt == "-o": 166617a3babSopenharmony_ci if i == len(sys.argv): 167617a3babSopenharmony_ci raise Exception("Expected path after {}".format(opt)) 168617a3babSopenharmony_ci val = sys.argv[i] 169617a3babSopenharmony_ci i = i + 1 170617a3babSopenharmony_ci if (opt == "-i"): 171617a3babSopenharmony_ci input_file = val 172617a3babSopenharmony_ci elif (opt == "-o"): 173617a3babSopenharmony_ci output_file = val 174617a3babSopenharmony_ci else: 175617a3babSopenharmony_ci raise Exception("Unknown flag {}".format(opt)) 176617a3babSopenharmony_ci 177617a3babSopenharmony_ci return { 178617a3babSopenharmony_ci "directory": directory, 179617a3babSopenharmony_ci "input_string": input_string, 180617a3babSopenharmony_ci "input_file": input_file, 181617a3babSopenharmony_ci "output_file": output_file, 182617a3babSopenharmony_ci } 183617a3babSopenharmony_ci 184617a3babSopenharmony_cidef main(): 185617a3babSopenharmony_ci args = None 186617a3babSopenharmony_ci try: 187617a3babSopenharmony_ci args = parse_args() 188617a3babSopenharmony_ci except Exception as e: 189617a3babSopenharmony_ci print(e) 190617a3babSopenharmony_ci print("\nUsage:\n") 191617a3babSopenharmony_ci print(usage.format(sys.argv[0])) 192617a3babSopenharmony_ci sys.exit(1) 193617a3babSopenharmony_ci 194617a3babSopenharmony_ci directory = args["directory"] 195617a3babSopenharmony_ci template = args["input_string"] 196617a3babSopenharmony_ci if template == None: 197617a3babSopenharmony_ci with open(args["input_file"], 'r') as f: 198617a3babSopenharmony_ci template = f.read() 199617a3babSopenharmony_ci output_file = args["output_file"] 200617a3babSopenharmony_ci 201617a3babSopenharmony_ci software_version = deduce_software_version(directory) 202617a3babSopenharmony_ci commit = describe(directory) 203617a3babSopenharmony_ci output = template \ 204617a3babSopenharmony_ci .replace("@major@", software_version["major"]) \ 205617a3babSopenharmony_ci .replace("@minor@", software_version["minor"]) \ 206617a3babSopenharmony_ci .replace("@patch@", software_version["patch"]) \ 207617a3babSopenharmony_ci .replace("@flavor@", software_version["flavor"]) \ 208617a3babSopenharmony_ci .replace("@-flavor@", software_version["-flavor"]) \ 209617a3babSopenharmony_ci .replace("@date@", software_version["date"]) \ 210617a3babSopenharmony_ci .replace("@commit@", commit) 211617a3babSopenharmony_ci 212617a3babSopenharmony_ci if output_file is None: 213617a3babSopenharmony_ci print(output) 214617a3babSopenharmony_ci else: 215617a3babSopenharmony_ci mkdir_p(os.path.dirname(output_file)) 216617a3babSopenharmony_ci 217617a3babSopenharmony_ci if os.path.isfile(output_file): 218617a3babSopenharmony_ci with open(output_file, 'r') as f: 219617a3babSopenharmony_ci if output == f.read(): 220617a3babSopenharmony_ci return 221617a3babSopenharmony_ci 222617a3babSopenharmony_ci with open(output_file, 'w') as f: 223617a3babSopenharmony_ci f.write(output) 224617a3babSopenharmony_ci 225617a3babSopenharmony_ciif __name__ == '__main__': 226617a3babSopenharmony_ci main() 227