mkr/release.py

191 lines
6.7 KiB
Python

#!/usr/bin/python3
import logging
import sys
import json
import argparse
import os
import hashlib
from shutil import copyfile
from datetime import datetime
logger = logging.getLogger()
handler = logging.StreamHandler(stream=sys.stderr)
formatter = logging.Formatter(
'%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
parser = argparse.ArgumentParser(
prog="mkrelease",
description="Automatically package a firmware release."
)
parser.add_argument('-c', '--config', dest="config_location", action="store", help="The location of the config file.", default="./release-config.json")
parser.add_argument('version', help="Version you wish to release. Use https://semver.org")
parser.add_argument('--verbose', help="Enable verbose logging output.", action="store_true", default=False)
def await_yes_no():
accept_words = ["y", "yes", ""]
deny_words = ["n", "no"]
while True:
print("Do you wish to proceed? [Y/n]: ", end="")
result = input()
if result.lower() in deny_words:
print("Aborted.")
exit(1)
elif result.lower() not in accept_words:
print("Don't understand what you mean.")
else:
print("OK, executing file operations.")
break
def build_version_string(gsd_num : str, out_name:str, version_number:str, release_type:str, file_extension:str):
return f'GSD-{gsd_num}-{out_name}-{version_number}-{release_type}.{file_extension}'
def md5_hash_of(file):
with open(file, 'rb') as f:
file_hash = hashlib.md5()
while chunk := f.read(8192):
file_hash.update(chunk)
return file_hash.hexdigest()
completed_checklists = []
def do_checklists(config):
if config["checklists"] is None or len(config["checklists"]) == 0:
logger.warn("No checklists specified in configuration.")
for (i, checklist) in enumerate(config["checklists"]):
for field in ["checklist_name", "when", "items"]:
if checklist[field] is None:
logger.error(f'Missing field {field} in checklist {i}')
exit(1)
if len(checklist["items"]) == 0:
logger.warn(f'Warning: Checklist "{checklist["checklist_name"]}" has no items."')
print(f'Confirm that you have completed checklist {checklist["checklist_name"]}:')
for item in checklist["items"]:
print(f'- {item}')
await_yes_no()
completed_checklists.append((i, checklist["checklist_name"]))
if __name__ == "__main__":
args = parser.parse_args()
if not args.verbose:
logger.disabled = True
logger.debug("Attempting to read config file...")
config = None
try:
config = json.load(open(args.config_location))
except Exception as e:
logger.error(args.config_location + ": " + str(e))
assert config is not None
do_checklists(config)
logger.debug("Config file read.")
release_dir = f'./{config["releases_directory"]}/{args.version}/'
print("The following releases will be created:")
assert config["releases"] is not None
missingfiles = []
binaries_manifest = []
print(release_dir)
for release in config["releases"]:
for ext in release["copy_extensions"]:
build_file_path = f'./{release["build_directory"]}/{release["build_file_name"]}.{ext}'
output_file_path = build_version_string(config["gsd"]
, config["product_name"]
, args.version
, release["release_name"]
, ext)
try:
os.stat(build_file_path)
except Exception as e:
logging.error(f"Could not stat {build_file_path}:")
logging.error(e)
missingfiles.append(build_file_path)
print('\t'+ output_file_path + f'\t({build_file_path})')
binaries_manifest.append((build_file_path, release_dir + output_file_path))
if len(missingfiles) > 0:
logger.error("Exiting due to missing source files.")
print("The following files were not able to be found:")
[print(f"\t{path}") for path in missingfiles]
exit(1)
print("\nThe following additional files will be created:")
print(f'./{config["releases_directory"]}/{args.version}/')
print("\trelease_notes.txt")
if config["changelog"] is not None:
logger.debug("Detected changelog field. Will also create changelog.")
if not os.path.exists(config["changelog"]):
logger.warning("Config file does not exist.")
print("The changelog file you provided does not exist. Check your configuration file.")
elif os.path.isfile(config["changelog"]):
print("\tchangelog.md (from file)")
elif os.path.isdir(config["changelog"]):
print("\tchangelog.md (from directory of files)")
print("")
await_yes_no()
logger.debug("Ensuring output directories exist.")
os.makedirs(release_dir, exist_ok=True)
for (source,destination) in binaries_manifest:
copyfile(source, destination)
logger.debug(f'Copied {source} to {destination}')
logger.debug(f'Generating changelog')
if os.path.isdir(config["changelog"]):
with open(release_dir + "changelog.md", mode='+w') as out_log:
for f in reversed(os.listdir(config["changelog"])):
if os.path.isfile(f'{config["changelog"]}/{f}'):
with open(f'{config["changelog"]}/{f}') as fi:
for line in fi:
out_log.write(line)
out_log.write("\n")
else:
copyfile(config["changelog"], release_dir + "/changelog.md")
logger.debug(f'Copied {config["changelog"]} to {release_dir}/changelog.txt')
logger.debug("Generating release_notes.txt")
with open(f'{release_dir}/release_notes.txt', mode="+w") as release_notes:
release_notes.write(f'Generated using mkrelease at {datetime.now().strftime("%Y/%m/%d %H:%M:%S")} by user {os.getlogin()}\n')
release_notes.write("Output file manifest (name, md5 hash):\n")
for (_, file) in binaries_manifest:
release_notes.write(f'{file} {md5_hash_of(file)}\n')
release_notes.write("Completed checklists for this release:\n")
for (i,checklist_name) in completed_checklists:
release_notes.write(f'{checklist_name}\n')
for item in config["checklists"][i]["items"]:
release_notes.write(f'- {item}\n')