blob: bc33019970dce2a9cbc48b7753d89a37cfa26637 [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
#
# 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 hashlib
import logging
import os
import shutil
import subprocess
import sys
import tempfile
import urllib
from kudu_util import check_output, confirm_prompt, Colors, get_my_email
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
GET_UPSTREAM_COMMIT_SCRIPT = os.path.join(ROOT, "build-support", "get-upstream-commit.sh")
def check_repo_not_dirty():
"""Check that the git repository isn't dirty."""
dirty_repo = subprocess.call("git diff --quiet && git diff --cached --quiet",
shell=True) != 0
if not dirty_repo:
return
print "The repository does not appear to be clean."
print Colors.RED + "The source release will not include your local changes." + \
Colors.RESET
if not confirm_prompt("Continue?"):
sys.exit(1)
def check_no_local_commits():
"""
Check that there are no local commits which haven't been pushed to the upstream
repo via Jenkins.
"""
upstream_commit = check_output(GET_UPSTREAM_COMMIT_SCRIPT).strip()
cur_commit = check_output(["git", "rev-parse", "HEAD"]).strip()
if upstream_commit == cur_commit:
return
print "The repository appears to have local commits:"
subprocess.check_call(["git", "log", "--oneline", "%s..HEAD" % upstream_commit])
print Colors.RED + "This should not be an official release!" + \
Colors.RESET
if not confirm_prompt("Continue?"):
sys.exit(1)
def get_version_number():
""" Return the current version number of Kudu. """
return file(os.path.join(ROOT, "version.txt")).read().strip()
def create_tarball():
artifact_name = "apache-kudu-%s" % get_version_number()
build_dir = os.path.join(ROOT, "build")
if not os.path.exists(build_dir):
os.path.makedirs(build_dir)
tarball_path = os.path.join(build_dir, artifact_name + ".tar.gz")
print "Exporting source tarball..."
subprocess.check_output(["git", "archive",
"--prefix=%s/" % artifact_name,
"--output=%s" % tarball_path,
"HEAD"])
print Colors.GREEN + "Generated tarball:\t" + Colors.RESET, tarball_path
return tarball_path
def sign_tarball(tarball_path):
""" Prompt the user to GPG-sign the tarball using their Apache GPG key. """
if not confirm_prompt("Would you like to GPG-sign the tarball now?"):
return
email = get_my_email()
if not email.endswith("@apache.org"):
print Colors.YELLOW, "Your email address for the repository is not an @apache.org address."
print "Release signatures should typically be signed by committers with @apache.org GPG keys."
print Colors.RESET,
if not confirm_prompt("Continue?"):
return
try:
subprocess.check_call(["gpg", "--detach-sign", "--armor", "-u", email, tarball_path])
except subprocess.CalledProcessError:
print Colors.RED + "GPG signing failed. Artifact will not be signed." + Colors.RESET
return
print Colors.GREEN + "Generated signature:\t" + Colors.RESET, tarball_path + ".asc"
def checksum_file(summer, path):
"""
Calculates the checksum of the file 'path' using the provided hashlib
digest implementation. Returns the hex form of the digest.
"""
with file(path, "rb") as f:
# Read the file in 4KB chunks until EOF.
for chunk in iter(lambda: f.read(4096), ""):
summer.update(chunk)
return summer.hexdigest()
def gen_checksum_files(tarball_path):
"""
Create md5 and sha files of the tarball.
The output format is compatible with command line tools like 'sha1sum'
and 'md5sum' so they may be used to verify the checksums.
"""
hashes = [(hashlib.sha1, "sha"),
(hashlib.md5, "md5")]
for hash_func, extension in hashes:
digest = checksum_file(hash_func(), tarball_path)
path = tarball_path + "." + extension
with file(path, "w") as f:
print >>f, "%s\t%s" % (digest, os.path.basename(tarball_path))
print Colors.GREEN + ("Generated %s:\t" % extension) + Colors.RESET, path
def run_rat(tarball_path):
"""
Run Apache RAT on the source tarball.
Raises an exception on failure.
"""
if not confirm_prompt("Would you like to run Apache RAT (Release Audit Tool) now?"):
return
# TODO: Cache and call the jar from the maven repo?
rat_url = "http://central.maven.org/maven2/org/apache/rat/apache-rat/0.11/apache-rat-0.11.jar"
tmpdir_path = tempfile.mkdtemp()
rat_report_result = ''
try:
rat_jar_dest = "%s/%s" % (tmpdir_path, os.path.basename(rat_url))
print "> Downloading RAT jar from " + rat_url
urllib.urlretrieve(rat_url, rat_jar_dest)
print "> Running RAT..."
xml = subprocess.check_output(["java", "-jar", rat_jar_dest, "-x", tarball_path])
rat_report_dest = "%s/%s" % (tmpdir_path, "rat_report.xml")
with open(rat_report_dest, "w") as f:
f.write(xml)
print "> Parsing RAT report..."
rat_report_result = subprocess.check_output(
["./build-support/release/check-rat-report.py",
"./build-support/release/rat_exclude_files.txt",
rat_report_dest],
stderr=subprocess.STDOUT)
print Colors.GREEN + "RAT: LICENSES APPROVED" + Colors.RESET
except subprocess.CalledProcessError as e:
print Colors.RED + "RAT: LICENSES NOT APPROVED" + Colors.RESET
print e.output
raise e
finally:
shutil.rmtree(tmpdir_path)
def main():
# Change into the source repo so that we can run git commands without having to
# specify cwd=BUILD_SUPPORT every time.
os.chdir(ROOT)
check_repo_not_dirty()
check_no_local_commits()
tarball_path = create_tarball()
gen_checksum_files(tarball_path)
sign_tarball(tarball_path)
run_rat(tarball_path)
print Colors.GREEN + "Release successfully generated!" + Colors.RESET
print
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
main()