build/buildrelease.py
Rama master · réplica de lectura del repositorio original.
#!/usr/bin/python3 -u
import getopt
import hashlib
import os
from os import path
import re
import shutil
import subprocess
import sys
import tempfile
# Script options
options = "hcdv:s:"
long_options = ["help", "clean", "docbook", "version=", "suffix="]
# Absolute path to docbook-manual.py
manualscript = path.dirname(path.abspath(__file__)) + '/docbook-manual.py'
# List of files and dirs to exclude from the release tarball
exclude_list = (
# System / build files
".git*",
".mailmap",
".imgbotconfig",
".travis.yml",
"build/",
"api/rest/generated/",
"composer.json",
"composer.lock",
# User custom files
"config_inc.php",
"custom_constant*_inc.php",
"custom_functions_inc.php",
"custom_strings_inc.php",
"custom_relationships_inc.php",
"mantis_offline.php",
"mc_config_inc.php",
# Directories
"docbook/",
"tests/"
)
# Checksum types to includ in digest filese
checksum_types = ['sha512', 'sha256', 'sha1', 'md5']
def usage():
print(f'''Builds a release (zip/tarball)
Usage: {path.basename(__file__)} [options] /path/for/tarballs [/path/to/mantisbt]
Options:
-h | --help Show this usage message
-c | --clean Remove build directory when completed
-d | --docbook Build and include DocBook manuals, REST API, and SOAP API documentation
-v | --version <version> Override version name detection
-s | --suffix <suffix> Include version suffix in config file
''')
# end usage()
def gpg_sign_tarball(filename):
"""
Sign the file using GPG
The private key's passphrase is read from a file named 'gpg-passphrase' in
the user's home directory. If that file is not present, gpg will fall back
to using gpg-agent, which may request the passphrase interactively.
"""
gpgsign = [
'gpg',
'--detach-sign',
'--armor',
'--batch',
'--yes',
path.abspath(path.join(os.curdir, filename)),
]
# Insert passphrase option if file exists
passphrase = path.expanduser('~/gpg-passphrase')
if path.isfile(passphrase):
pos = len(gpgsign) - 1
gpgsign[pos:pos] = ['--pinentry=loopback',
'--passphrase-file=' + passphrase]
try:
subprocess.run(gpgsign, check=True)
except subprocess.CalledProcessError:
# Remove batch-specific options for warning display
gpgsign[3:len(gpgsign) - 1] = []
print("WARNING: GPG signature failed; to sign manually, run")
print(" " + " ".join(gpgsign))
def generate_checksum(filename):
"""
Generate digest file with checksums for the given filename
"""
# Initialize hash objects for each checksum type
checksums = dict()
for method in checksum_types:
checksums[method] = hashlib.new(method)
# Read the file and calculate checksums
with open(filename, 'rb') as file:
for chunk in file:
for method in checksum_types:
checksums[method].update(chunk)
# Print results and generate digests file
f = open(filename + ".digests", 'w')
for method in checksum_types:
checksum = checksums[method].hexdigest()
f.write(f"{checksum} *{filename}\n")
f.close()
def generate_api_docs(mantis_path, release_dir):
"""Generate static REST API documentation for inclusion in a release."""
npm = shutil.which('npm')
if npm is None:
print("ERROR: npm executable not found; it is required to build REST API documentation.")
sys.exit(1)
print("Generating REST API documentation...\n")
try:
subprocess.run([npm, 'ci'], cwd=mantis_path, check=True)
with tempfile.TemporaryDirectory() as api_build_dir:
subprocess.run(
[npm, 'run', 'api:generate', '--', '--output', api_build_dir],
cwd=mantis_path,
check=True,
)
source_dir = path.join(api_build_dir, 'docs')
destination_dir = path.join(release_dir, 'doc', 'en-US', 'rest-api')
shutil.copytree(source_dir, destination_dir)
shutil.copy2(
path.join(api_build_dir, 'mantisbt.postman_collection.json'),
destination_dir,
)
shutil.copytree(
path.join(api_build_dir, 'bruno'),
path.join(destination_dir, 'bruno'),
)
except subprocess.CalledProcessError as e:
print("ERROR: failed to generate REST API documentation:", e)
sys.exit(1)
print(f"REST API documentation copied to '{destination_dir}'\n")
def generate_soap_docs(mantis_path, release_dir):
"""Generate static SOAP API documentation and bundle its WSDL contract."""
xsltproc = shutil.which('xsltproc')
if xsltproc is None:
print("ERROR: xsltproc executable not found; it is required to build SOAP API documentation.")
sys.exit(1)
wsdl_file = path.join(mantis_path, 'api', 'soap', 'mantisconnect.wsdl')
stylesheet_file = path.join(mantis_path, 'api', 'soap', 'wsdl-viewer.xsl')
destination_dir = path.join(release_dir, 'doc', 'en-US', 'soap-api')
print("Generating SOAP API documentation...\n")
try:
os.makedirs(destination_dir)
subprocess.run(
[xsltproc, '--nonet', '--output', path.join(destination_dir, 'index.html'), stylesheet_file, wsdl_file],
check=True,
)
shutil.copy2(wsdl_file, destination_dir)
shutil.copy2(stylesheet_file, destination_dir)
except (OSError, subprocess.CalledProcessError) as e:
print("ERROR: failed to generate SOAP API documentation:", e)
sys.exit(1)
print(f"SOAP API documentation copied to '{destination_dir}'\n")
def remove_build_dir(release_dir):
print("Removing build directory...")
shutil.rmtree(release_dir)
def main():
try:
opts, args = getopt.gnu_getopt(sys.argv[1:], options, long_options)
except getopt.GetoptError as err:
print(err)
usage()
sys.exit(2)
build_docbook = False
clean_build = False
mantis_version = ""
version_suffix = ""
for opt, val in opts:
if opt in ("-h", "--help"):
usage()
sys.exit(0)
elif opt in ("-c", "--clean"):
clean_build = True
elif opt in ("-d", "--docbook"):
build_docbook = True
elif opt in ("-v", "--version"):
mantis_version = val
elif opt in ("-s", "--suffix"):
version_suffix = val
if len(args) < 1:
usage()
sys.exit(1)
release_path = args[0]
mantis_path = "."
if len(args) > 1:
mantis_path = args[1]
# 'Standard' umask
old_umask = os.umask(0o002)
# Check paths
if not path.isdir(release_path):
print("Creating release path...")
os.mkdir(release_path)
if not path.isdir(mantis_path):
print("Error: mantis path is not a directory or does not exist.")
sys.exit(3)
if (
not path.isfile(path.join(mantis_path, "core.php")) or
not path.isdir(path.join(mantis_path, "core")) or
not path.isfile(path.join(mantis_path, "core", "constant_inc.php"))
):
print(f"Error: '{mantis_path}' does not appear to be a valid Mantis directory.")
sys.exit(3)
# Find Mantis version
if not mantis_version:
f = open(path.join(mantis_path, "core", "constant_inc.php"))
content = f.read()
f.close()
mantis_version = re.search(r"'MANTIS_VERSION'[,\s]+'([^']+)'",
content).group(1)
# Generate release name
release_name = 'mantisbt-' + mantis_version
if version_suffix:
release_name += '-' + version_suffix
# Copy to release path, excluding unwanted files
release_dir = path.abspath(path.join(release_path, release_name))
print(f"\nBuilding release '{release_name}' in path '{release_dir}'")
print(f" Source repository: '{mantis_path}'\n")
if path.exists(release_dir):
print(f"Error: release path already contains {release_name}.")
sys.exit(3)
# Generate temp file with list of exclusions
fp = tempfile.NamedTemporaryFile(mode='wt', delete=False)
print(" Excluded files and directories:")
for name in exclude_list:
print(" " + name)
fp.write(name + "\n")
fp.close()
# Copy the files from the source repo, then delete temp file
rsync = ['rsync', '-rltD', f'--exclude-from={fp.name}', mantis_path + '/', release_dir]
subprocess.run(rsync, check=True)
os.unlink(fp.name)
print(" Copy complete.\n")
# Apply version suffix
if version_suffix:
print("Applying version suffix...")
sed_cmd = ['sed', '-r', '-i.bak',
fr"s/(g_version_suffix\s*=\s*)'.*'/\1'{version_suffix}'/",
path.join(release_dir, "config_defaults_inc.php")]
subprocess.run(sed_cmd, check=True)
# Build documentation for release
if build_docbook:
print("Building docbook manuals...\n")
try:
cmd = [manualscript,
"--release",
path.join(mantis_path, "docbook"),
path.join(release_dir, "doc")]
subprocess.run(cmd, check=True)
except subprocess.CalledProcessError as e:
sys.exit(1)
generate_api_docs(mantis_path, release_dir)
generate_soap_docs(mantis_path, release_dir)
# Create tarballs and sign them
print("Creating release tarballs...")
os.chdir(release_path)
tarball_ext = ("tar.gz", "zip")
for ext in tarball_ext:
tarball = f"{release_name}.{ext}"
print(" " + tarball)
if ext == "tar.gz":
tar_cmd = ['tar', '-czf']
else:
tar_cmd = ['zip', '-rq']
tar_cmd.extend([tarball, release_name])
subprocess.run(tar_cmd, check=True)
print(" Signing the tarball")
gpg_sign_tarball(tarball)
print(f" Generating checksums ({', '.join(checksum_types)})")
generate_checksum(tarball)
# Cleanup
if clean_build:
print()
remove_build_dir(release_dir)
# Restore previous umask
os.umask(old_umask)
print("Done!\n")
# end main()
if __name__ == "__main__":
main()