blob: 24762cc9a8d46a7ab42cea8555e75c83e34dd878 [file] [log] [blame]
#!/usr/bin/env python
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# Script which checks Java API compatibility between two revisions of the
# Java client.
# Compare current commit against the previous commit:
# ./build-support/ HEAD~1..HEAD
# Compare current commit against a release tag:
# ./build-support/ 1.13.0..HEAD
# Compare current commit against a release tag:
# ./build-support/ 1.13.0..HEAD
# Compare two releases:
# ./build-support/ 1.12.0..1.13.0
# NOTE: You can only compare against releases which support a Gradle build (1.5+)
import argparse
import logging
import os
import re
import shutil
import subprocess
import sys
from kudu_util import check_output, init_logging
JAPICMP_JAR="japicmp-" + JAPICMP_VERSION + "-jar-with-dependencies.jar"
# Paths are ".../kudu/java/<artifact-name>/build/..."
# Various relative paths
PATH_TO_BUILD_DIR = "../build/compat-check"
def get_repo_dir():
""" Return the path to the top of the repo. """
dirname, _ = os.path.split(os.path.abspath(__file__))
return os.path.abspath(os.path.join(dirname, PATH_TO_REPO_DIR))
def get_scratch_dir():
""" Return the path to the scratch dir that we build within. """
dirname, _ = os.path.split(os.path.abspath(__file__))
return os.path.abspath(os.path.join(dirname, PATH_TO_BUILD_DIR))
def clean_scratch_dir(scratch_dir):
""" Clean up and re-create the scratch directory. """
if os.path.exists(scratch_dir):"Removing scratch dir %s...", scratch_dir)
shutil.rmtree(scratch_dir)"Creating empty scratch dir %s...", scratch_dir)
def checkout_source_tree(rev, path):
""" Check out the Java source tree for the given revision into the given path. """"Checking out %s in %s", rev, path)
# Extract source.
subprocess.check_call(["bash", '-o', 'pipefail', "-c",
("git archive --format=tar %s | " +
"tar -C \"%s\" -xf -") % (rev, path)],
def get_git_hash(revname):
""" Convert 'revname' to its SHA-1 hash. """
return check_output(["git", "rev-parse", revname],
def build_tree(path):
""" Run the Java build within 'path'. """
java_path = os.path.join(path, "java")"Building in %s...", java_path)
subprocess.check_call(["./gradlew", "assemble"], cwd=java_path)
def get_japicmp_path():
""" Return the path where we download the japicmp jar. """
return os.path.join(get_repo_dir(), "thirdparty/src/" + JAPICMP_JAR)
def download_japicmp(force):
""" Download the japicmp jar. """
if os.path.exists(get_japicmp_path()):"japicmp is already downloaded.")
if not force:
return"Forcing re-download.")
subprocess.check_call(["curl", "--retry", "3", "-L", "-o",
get_japicmp_path(), JAPICMP_URL], cwd=get_repo_dir())
def find_client_jars(path):
""" Return a list of jars within 'path' to be checked for compatibility. """
all_jars = set(check_output(["find", path, "-name", "*.jar"]).decode('utf-8').splitlines())
return [j for j in all_jars if (
"-javadoc" not in j and
"-sources" not in j and
"-test-sources" not in j and
"-tests" not in j and
"-unshaded" not in j and
"buildSrc" not in j and
"gradle-wrapper" not in j and
"kudu-backup" not in j and
"kudu-hive" not in j and
"kudu-jepsen" not in j and
"kudu-proto" not in j and
"kudu-subprocess" not in j)]
def get_artifact_name(jar_path):
""" Return the artifact name given a full jar path. """
def run_japicmp(src, dst, opts):
""" Run the compliance checker to compare 'src' and 'dst'. """
src_jars = find_client_jars(src)
dst_jars = find_client_jars(dst)"Will check compatibility between original jars:\n%s\n" +
"and new jars:\n%s",
excludes = [
# Exclude shaded/relocated packages.
# Exclude inner Protobuf classes since the annotation filter doesn't handle these.
# Exclude generated Protobuf code.
# Exclude unstable code.
# Exclude private code.
# Exclude limited private code.
# Exclude Scala Generated code.
reports = []
for src_jar in src_jars:
src_name = get_artifact_name(src_jar)
for dst_jar in dst_jars:
dst_name = get_artifact_name(dst_jar)
if src_name == dst_name:
reports.append((src_name, src_jar, dst_jar))
# CLI tool documentation:
for (name, src_jar, dst_jar) in reports:
out_path = os.path.join(get_scratch_dir(), name + "-report.html")
# Add the extra flags.
cmd = ["java", "-jar", get_japicmp_path(),
"--old", src_jar,
"--new", dst_jar,
"--include", "org.apache.kudu.*",
"--exclude", ";".join(excludes),
"--html-file", out_path,
if (opts.only_incompatible):
if (opts.error_on_binary_incompatibility):
if (opts.error_on_source_incompatibility):
def parse_args():
""" Parse command-line arguments """
parser = argparse.ArgumentParser(description='Check and report on Kudu API compatibility',
help="The source revision to compare against. Likely the previous release tag.")
help="The destination revision to compare against the source.")
parser.add_argument('--only-incompatible', action='store_true',
help='Outputs only classes/methods that are binary incompatible. '
'If not given, all classes and methods are printed.')
parser.add_argument('--error-on-binary-incompatibility', action='store_true',
help='Exit with an error if a binary incompatibility is detected.')
parser.add_argument('--error-on-source-incompatibility', action='store_true',
help='Exit with an error if a source incompatibility is detected.')
parser.add_argument("--force-download-deps", action='store_true',
help="Download dependencies (i.e. japicmp) even if they are already present")
return parser.parse_args()
def main():
opts = parse_args()
src_rev = get_git_hash(opts.source)
dst_rev = get_git_hash(opts.destination)"Source revision: %s", src_rev)"Destination revision: %s", dst_rev)
# Set up the build.
scratch_dir = get_scratch_dir()
# Download japicmp.
# Check out the src and dst source trees.
src_dir = os.path.join(scratch_dir, "src")
dst_dir = os.path.join(scratch_dir, "dst")
checkout_source_tree(src_rev, src_dir)
checkout_source_tree(dst_rev, dst_dir)
# Run the build in each tree.
run_japicmp(src_dir, dst_dir, opts)
if __name__ == "__main__":