220 lines
9.8 KiB
Python
Executable File
220 lines
9.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# apiversioner.py
|
|
#
|
|
# This source file is part of the FoundationDB open source project
|
|
#
|
|
# Copyright 2013-2021 Apple Inc. and the FoundationDB project 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.
|
|
#
|
|
|
|
import argparse
|
|
import logging
|
|
import os
|
|
import re
|
|
import sys
|
|
import traceback
|
|
|
|
|
|
LOG_FORMAT = '%(created)f [%(levelname)s] %(message)s'
|
|
|
|
EXCLUDED_FILES = list(map(re.compile, [
|
|
# Output directories
|
|
r'\.git/.*', r'bin/.*', r'packages/.*', r'\.objs/.*', r'\.deps/.*', r'bindings/go/build/.*', r'documentation/sphinx/\.out/.*',
|
|
|
|
# Generated files
|
|
r'.*\.g\.cpp$', r'.*\.g\.h$', r'(^|.*/)generated.mk$', r'.*\.g\.S$',
|
|
r'.*/MutationType\.java', r'.*/generated\.go',
|
|
|
|
# Binary files
|
|
r'.*\.class$', r'.*\.o$', r'.*\.a$', r'.*[\.-]debug', r'.*\.so$', r'.*\.dylib$', r'.*\.dll$', r'.*\.tar[^/]*$', r'.*\.jar$', r'.*pyc$', r'bindings/flow/bin/.*',
|
|
r'.*\.pdf$', r'.*\.jp[e]*g', r'.*\.png', r'.*\.ico',
|
|
r'packaging/msi/art/.*',
|
|
|
|
# Project configuration files
|
|
r'.*foundationdb\.VC\.db$', r'.*foundationdb\.VC\.VC\.opendb$', r'.*iml$',
|
|
|
|
# Source files from someone else
|
|
r'(^|.*/)Hash3\..*', r'(^|.*/)sqlite.*',
|
|
r'bindings/go/godoc-resources/.*',
|
|
r'bindings/go/src/fdb/tuple/testdata/tuples.golden',
|
|
r'fdbcli/linenoise/.*',
|
|
r'fdbrpc/rapidjson/.*', r'fdbrpc/rapidxml/.*', r'fdbrpc/zlib/.*', r'fdbrpc/sha1/.*',
|
|
r'fdbrpc/xml2json.hpp$', r'fdbrpc/libcoroutine/.*', r'fdbrpc/libeio/.*', r'fdbrpc/lib64/.*',
|
|
r'fdbrpc/generated-constants.cpp$',
|
|
|
|
# Miscellaneous
|
|
r'bindings/nodejs/node_modules/.*', r'bindings/go/godoc/.*', r'.*trace.*xml$', r'.*log$', r'.*\.DS_Store$', r'simfdb/\.*', r'.*~$', r'.*.swp$'
|
|
]))
|
|
|
|
SUSPECT_PHRASES = map(re.compile, [
|
|
r'#define\s+FDB_API_VERSION\s+(\d+)',
|
|
r'\.\s*selectApiVersion\s*\(\s*(\d+)\s*\)',
|
|
r'\.\s*APIVersion\s*\(\s*(\d+)\s*\)',
|
|
r'\.\s*MustAPIVersion\s*\(\s*(\d+)\s*\)',
|
|
r'header_version\s+=\s+(\d+)',
|
|
r'\.\s*apiVersion\s*\(\s*(\d+)\s*\)',
|
|
r'API_VERSION\s*=\s*(\d+)',
|
|
r'fdb_select_api_version\s*\((\d+)\)'
|
|
])
|
|
|
|
DIM_CODE = '\033[2m'
|
|
BOLD_CODE = '\033[1m'
|
|
RED_COLOR = '\033[91m'
|
|
GREEN_COLOR = '\033[92m'
|
|
END_COLOR = '\033[0m'
|
|
|
|
|
|
def positive_response(val):
|
|
return val.lower() in {'y', 'yes'}
|
|
|
|
|
|
# Returns: new line list + a dirty flag
|
|
def rewrite_lines(lines, version_re, new_version, suspect_only=True, print_diffs=False, ask_confirm=False, grayscale=False):
|
|
new_lines = []
|
|
dirty = False
|
|
new_str = str(new_version)
|
|
regexes = SUSPECT_PHRASES if suspect_only else [version_re]
|
|
group_index = 1 if suspect_only else 2
|
|
for line_no, line in enumerate(lines):
|
|
new_line = line
|
|
offset = 0
|
|
|
|
for regex in regexes:
|
|
for m in regex.finditer(line):
|
|
# Replace suspect code with new version.
|
|
start = m.start(group_index)
|
|
end = m.end(group_index)
|
|
new_line = new_line[:start + offset] + new_str + new_line[end + offset:]
|
|
offset += len(new_str) - (end - start)
|
|
|
|
if (print_diffs or ask_confirm) and line != new_line:
|
|
print('Rewrite:')
|
|
print('\n'.join(map(lambda pair: ' {:4d}: {}'.format(line_no - 1 + pair[0], pair[1]), enumerate(lines[line_no - 2:line_no]))))
|
|
print((DIM_CODE if grayscale else RED_COLOR) + '-{:4d}: {}'.format(line_no + 1, line) + END_COLOR)
|
|
print((BOLD_CODE if grayscale else GREEN_COLOR) + '+{:4d}: {}'.format(line_no + 1, new_line) + END_COLOR)
|
|
print('\n'.join(map(lambda pair: ' {:4d}: {}'.format(line_no + 2 + pair[0], pair[1]), enumerate(lines[line_no + 1:line_no + 3]))))
|
|
|
|
if ask_confirm:
|
|
text = input('Looks good (y/n)? ')
|
|
if not positive_response(text):
|
|
print('Okay, skipping.')
|
|
new_line = line
|
|
|
|
dirty = dirty or (new_line != line)
|
|
new_lines.append(new_line)
|
|
|
|
return new_lines, dirty
|
|
|
|
|
|
def address_file(base_path, file_path, version, new_version=None, suspect_only=False, show_diffs=False,
|
|
rewrite=False, ask_confirm=True, grayscale=False, paths_only=False):
|
|
if any(map(lambda x: x.match(file_path), EXCLUDED_FILES)):
|
|
logging.debug('skipping file %s as matches excluded list', file_path)
|
|
return True
|
|
|
|
# Look for all instances of the version number where it is not part of a larger number
|
|
version_re = re.compile('(^|[^\\d])(' + str(version) + ')([^\\d]|$)')
|
|
try:
|
|
contents = open(os.path.join(base_path, file_path), 'r').read()
|
|
lines = contents.split('\n')
|
|
new_lines = lines
|
|
dirty = False
|
|
|
|
if suspect_only:
|
|
# Look for suspect lines (lines that attempt to set a version)
|
|
found = False
|
|
for line_no, line in enumerate(lines):
|
|
for suspect_phrase in SUSPECT_PHRASES:
|
|
for match in suspect_phrase.finditer(line):
|
|
curr_version = int(match.groups()[0])
|
|
if (new_version is None and curr_version < version) or (new_version is not None and curr_version < new_version):
|
|
found = True
|
|
logging.info('Old version: %s:%d:%s', file_path, line_no + 1, line)
|
|
|
|
if found and new_version is not None and (show_diffs or rewrite):
|
|
new_lines, dirty = rewrite_lines(lines, version_re, new_version, True, print_diffs=True,
|
|
ask_confirm=(rewrite and ask_confirm), grayscale=grayscale)
|
|
|
|
else:
|
|
matching_lines = filter(lambda pair: version_re.search(pair[1]), enumerate(lines))
|
|
|
|
# Look for lines with the version
|
|
if matching_lines:
|
|
if paths_only:
|
|
logging.info('File %s matches', file_path)
|
|
else:
|
|
for line_no, line in matching_lines:
|
|
logging.info('Match: %s:%d:%s', file_path, line_no + 1, line)
|
|
if new_version is not None and (show_diffs or rewrite):
|
|
new_lines, dirty = rewrite_lines(lines, version_re, new_version, False, print_diffs=True,
|
|
ask_confirm=(rewrite and ask_confirm), grayscale=grayscale)
|
|
else:
|
|
logging.debug('File %s does not match', file_path)
|
|
|
|
if dirty and rewrite:
|
|
logging.info('Rewriting %s', os.path.join(base_path, file_path))
|
|
with open(os.path.join(base_path, file_path), 'w') as fout:
|
|
fout.write('\n'.join(new_lines))
|
|
|
|
return True
|
|
except (OSError, UnicodeDecodeError) as e:
|
|
logging.exception('Unable to read file %s due to OSError', os.path.join(base_path, file_path))
|
|
return False
|
|
|
|
|
|
def address_path(path, version, new_version=None, suspect_only=False, show_diffs=False, rewrite=False, ask_confirm=True, grayscale=False, paths_only=False):
|
|
try:
|
|
if os.path.exists(path):
|
|
if os.path.isdir(path):
|
|
status = True
|
|
for dir_path, dir_names, file_names in os.walk(path):
|
|
for file_name in file_names:
|
|
file_path = os.path.relpath(os.path.join(dir_path, file_name), path)
|
|
status = address_file(path, file_path, version, new_version, suspect_only, show_diffs,
|
|
rewrite, ask_confirm, grayscale, paths_only) and status
|
|
return status
|
|
else:
|
|
base_name, file_name = os.path.split(path)
|
|
return address_file(base_name, file_name, version, new_version, suspect_only, show_diffs, rewrite, ask_confirm, grayscale)
|
|
else:
|
|
logging.error('Path %s does not exist', path)
|
|
return False
|
|
except OSError as e:
|
|
logging.exception('Unable to find all API versions due to OSError')
|
|
return False
|
|
|
|
|
|
def run(arg_list):
|
|
parser = argparse.ArgumentParser(description='finds and rewrites the API version in FDB source files')
|
|
parser.add_argument('path', help='path to search for FDB source files')
|
|
parser.add_argument('version', type=int, help='current/old version to search for')
|
|
parser.add_argument('--new-version', type=int, default=None, help='new version to update to')
|
|
parser.add_argument('--suspect-only', action='store_true', default=False, help='only look for phrases trying to set the API version')
|
|
parser.add_argument('--show-diffs', action='store_true', default=False, help='show suggested diffs for fixing version')
|
|
parser.add_argument('--rewrite', action='store_true', default=False, help='rewrite offending files')
|
|
parser.add_argument('-y', '--skip-confirm', action='store_true', default=False, help='do not ask for confirmation before rewriting')
|
|
parser.add_argument('--grayscale', action='store_true', default=False,
|
|
help='print diffs using grayscale output instead of red and green')
|
|
parser.add_argument('--paths-only', action='store_true', default=False, help='display only the path instead of the offending lines')
|
|
args = parser.parse_args(arg_list)
|
|
return address_path(args.path, args.version, args.new_version, args.suspect_only, args.show_diffs,
|
|
args.rewrite, not args.skip_confirm, args.grayscale, args.paths_only)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
logging.basicConfig(format=LOG_FORMAT, level=logging.INFO)
|
|
if not run(sys.argv[1:]):
|
|
exit(1)
|