Ctrl K
ComunidadGuías…

build/buildrelease.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/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()