#!/usr/bin/env python3 # Copyright (c) 2023 Project CHIP Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # This automates ZAP version changes as the zap version is repeated # in many places import logging import os import re import sys from enum import Flag, auto import click import coloredlogs # Supported log levels, mapping string values required for argument # parsing into logging constants __LOG_LEVELS__ = { 'debug': logging.DEBUG, 'info': logging.INFO, 'warn': logging.WARN, 'fatal': logging.FATAL, } # A version is of the form: v2023.01.09-nightly # # At this time we hard-code nightly however we may need to figure out a more # generic version string once we stop using nightly builds ZAP_VERSION_RE = re.compile(r'v(\d\d\d\d)\.(\d\d)\.(\d\d)-nightly') # A list of files where ZAP is maintained. You can get a similar list using: # # rg v2023.01.09-nightly --hidden --no-ignore --files-with-matches # # Excluding THIS file and excluding anything in .environment (logs) # # Set as a separate list to not pay the price of a full grep as the list of # files is not likely to change often USAGE_FILES_DEPENDING_ON_ZAP_VERSION = [ 'integrations/docker/images/chip-cert-bins/Dockerfile', 'scripts/setup/zap.json', 'scripts/setup/zap.version', ] class UpdateChoice(Flag): # Usage updates the CI, chip-cert and execution logic. Generally everything # that would make use of the updated zap version USAGE = auto() __UPDATE_CHOICES__ = { 'usage': UpdateChoice.USAGE, } # Apart from the above files which contain an exact ZAP version, the zap # execution script contains the mimimal zap execution version, which generally # we also enforce to be the current version. # # That line is of the form "MIN_ZAP_VERSION = '2021.1.9'" ZAP_EXECUTION_SCRIPT = 'scripts/tools/zap/zap_execution.py' ZAP_EXECUTION_MIN_RE = re.compile( r'(MIN_ZAP_VERSION = .)(\d\d\d\d\.\d\d?\.\d\d?)(.)') CHIP_ROOT_DIR = os.path.abspath(os.path.join( os.path.dirname(__file__), '..', '..', '..')) @click.command() @click.option( '--log-level', default='INFO', type=click.Choice(__LOG_LEVELS__.keys(), case_sensitive=False), help='Determines the verbosity of script output.') @click.option( '--update', default='usage', type=click.Choice(__UPDATE_CHOICES__.keys(), case_sensitive=False), help='What to update: usage (only choice currently).') @click.option( '--new-version', default=None, help='What version of ZAP to update to (like "v2023.01.09-nightly". If not set, versions will just be printed.') def version_update(log_level, update, new_version): coloredlogs.install(level=__LOG_LEVELS__[ log_level], fmt='%(asctime)s %(levelname)-7s %(message)s') update = __UPDATE_CHOICES__[update] if new_version: parsed = ZAP_VERSION_RE.match(new_version) if not parsed: logging.error( f"Version '{new_version}' does not seem to parse as a ZAP VERSION") sys.exit(1) # get the numeric version for zap_execution # # This makes every group element (date section) to a base 10 integer, # so for 'v2023.01.11-nightly' this gets (2023, 1, 11) zap_min_version = tuple(map(lambda x: int(x, 10), parsed.groups())) files_to_update = [] if UpdateChoice.USAGE in update: files_to_update += USAGE_FILES_DEPENDING_ON_ZAP_VERSION for name in files_to_update: with open(os.path.join(CHIP_ROOT_DIR, name), 'rt') as f: file_data = f.read() # Write out any matches. Note that we only write distinct matches as # zap versions may occur several times in the same file found_versions = set() for m in ZAP_VERSION_RE.finditer(file_data): version = file_data[m.start():m.end()] if version not in found_versions: logging.info('%s currently used in %s', version, name) found_versions.add(version) # If we update, perform the update if new_version: search_pos = 0 need_replace = False m = ZAP_VERSION_RE.search(file_data, search_pos) while m: version = file_data[m.start():m.end()] if version == new_version: logging.warning( "Nothing to replace. Version already %s", version) break file_data = file_data[:m.start()] + \ new_version + file_data[m.end():] need_replace = True search_pos = m.end() # generally ok since our versions are fixed length m = ZAP_VERSION_RE.search(file_data, search_pos) if need_replace: logging.info('Replacing with version %s in %s', new_version, name) with open(os.path.join(CHIP_ROOT_DIR, name), 'wt') as f: f.write(file_data) # Finally, check zap_execution for any version update if UpdateChoice.USAGE in update: with open(os.path.join(CHIP_ROOT_DIR, ZAP_EXECUTION_SCRIPT), 'rt') as f: file_data = f.read() m = ZAP_EXECUTION_MIN_RE.search(file_data) logging.info("Min version %s in %s", m.group(2), ZAP_EXECUTION_SCRIPT) if new_version: new_min_version = ("%d.%d.%d" % zap_min_version) file_data = file_data[:m.start()] + m.group(1) + \ new_min_version + m.group(3) + file_data[m.end():] logging.info('Updating min version to %s in %s', new_min_version, ZAP_EXECUTION_SCRIPT) with open(os.path.join(CHIP_ROOT_DIR, ZAP_EXECUTION_SCRIPT), 'wt') as f: f.write(file_data) if __name__ == '__main__': version_update()