405 lines
12 KiB
Python
Executable File
405 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# apiversioner.py
|
|
#
|
|
# This source file is part of the FoundationDB open source project
|
|
#
|
|
# Copyright 2013-2024 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"contrib/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):
|
|
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:
|
|
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)
|