Add license and doc to rpm package
[pyi2ncommon] / make_dist.py
index f8e0022..849e6f5 100755 (executable)
 #!/usr/bin/env python3
 
 """
-Create rpm packages of pyi2ncommon for various python versions.
+Create source tarball, create rpm from it together with spec file.
 
-Usage::
-
-    make_dist.py [RELEASE [PY_VERSION_1 [PY_VERSION_2 ...]]]
-
-RELEASE defaults to current git commit hash, default versions to build for are
-3.6 for avocado and 3.3 for i2n system. All python versions used must be
-installed on local machine.
-
-Calls setup.py with different args. Adds %check section to .spec file. Runs
-unittests first.
-
-This script relies on one feature I accidentally stumbled over: it appears that
-setting install-lib to /usr/lib/python{VERSION}/site-packages is translated by
-setup.py to installation requirement python(abi) = {VERSION}
+Create a source tarball that is compatbile with standard build pipelines. Then
+run rpmbuild on it to create an installable rpm.
 
 .. codeauthor:: Intra2net AG <info@intra2net.com>
 """
 
-import os
-from os.path import join, isfile
 import sys
-from subprocess import call, check_output
-from tempfile import mkstemp
-from configparser import ConfigParser
-from glob import iglob
-import time
-
-INSTALL_DIR_PATTERN = '/usr/lib/python{}/site-packages'
-
-RPM_OPTIONS = dict(packager='Intra2net', group='Intra2net',
-                   vendor='Intra2net AG')
-
-DIST_DIR = 'dist'
-SPEC_FILE = join(DIST_DIR, 'pyi2ncommon.spec')
-
-CFG_FILE = 'setup.cfg'
-
-SPEC_START = b"""
-# spec file created automatically by make_dist.sh -- do not modify!
-
-"""
-
-SPEC_CHECK_SECTION = b"""%check
-PYTHONPATH=./src:$PYTHONPATH && python3 -m unittest discover test
-
-"""
-
-
-def check_py_version(py_version):
-    """Test that python version is installed."""
-    try:
-        result = call(['python{}'.format(py_version), '--version'])
-    except Exception:
-        result = 1
-    if result != 0:
-        raise RuntimeError('Python version {} not installed, '
-                           'run {} VERSION'.format(py_version, sys.argv[0]))
-
-
-def run_unittests(py_version):
-    """Run unittests with given python version. Re-Run with LANG=C."""
-    # Run twice: first with environment copied from call (env=None) and one
-    # in empoverished environment without unicode capability
-    for env in None, dict(LANG='C', PATH=os.environ['PATH']):
-        try:
-            print('Running unittests with python {} and env={}'
-                  .format(py_version, env))
-            result = call(['python{}'.format(py_version), '-m', 'unittest',
-                           'discover'], env=env)
-        except Exception as exc:
-            raise RuntimeError('Unittests with python {} and env={} failed. {}'
-                               .format(py_version, env, exc))
-        if result != 0:
-            raise RuntimeError('Unittests with python {} and env={} failed '
-                               '(command returned {}).'
-                               .format(py_version, env, result))
-
-
-def run_setup(command, cmd_line_args=None, install_options=None,
-              need_zip35=False):
+from os.path import join, basename, dirname, expanduser
+from re import compile as re_compile
+import tarfile
+from shutil import copy
+from subprocess import run
+
+# path to spec file
+BASE_DIR = dirname(__file__)
+SPEC_FILE = join(BASE_DIR, 'pyi2ncommon.spec')
+
+# regular expressions extracting relevant information from spec file
+VERSION_REGEX = re_compile(r'^Version:\s*(\S+)\s*$')
+NAME_REGEX = re_compile(r'^Name:\s*(\S+)\s*$')
+SOURCE_REGEX = re_compile(r'^Source0:\s*(\S+)\s*$')
+
+# dir to contain tarball
+DIST_DIR = join(BASE_DIR, 'dist')
+
+# files and dirs to add to tarball; keep in sync with %files secion in spec
+DATA = (
+    'COPYING.GPL', 'Linking-Exception.txt', 'README', 'CONTRIBUTING',
+    'setup.py', 'src', 'test', 'doc/about_docu.rst', 'doc/conf.py'
+)
+
+# dir where rpmbuild expects its tarballs
+RPM_SOURCE_DIR = expanduser(join('~', 'rpmbuild', 'SOURCES'))
+
+
+def get_spec_info():
+    """Extract name, version and source0 from spec file."""
+    name = None
+    version = None
+    source = None
+    with open(SPEC_FILE, 'rt') as reader:
+        for line in reader:
+            if name is not None and version is not None and source is not None:
+                source = source.replace('%{name}', name)\
+                               .replace('%{version}', version)
+                return name, version, source
+            match = NAME_REGEX.match(line)
+            if match:
+                name = match.group(1)
+                continue
+            match = VERSION_REGEX.match(line)
+            if match:
+                version = match.group(1)
+                continue
+            match = SOURCE_REGEX.match(line)
+            if match:
+                source = match.group(1)
+                continue
+
+
+def tar_add_filter(tarinfo):
     """
-    Run python3 setup.py with command and options.
+    Filter function for adding files to tarball
 
-    Need this function since setup.py bdist_rpm does not accept --prefix and
-    --install-lib options. Need to write them into cfg file from which they
-    are then read ... Grrr!
+    Return `None` for pycache and pyc, meaning "do not add". Return input
+    otherwise to add the file.
     """
-    config = ConfigParser()
-    rpm_options = dict(RPM_OPTIONS.items())
-    if need_zip35:
-        rpm_options['requires'] = 'python3-zipfile35'
-    if rpm_options:
-        config['bdist_rpm'] = rpm_options
-    if install_options:
-        config['install'] = install_options
-    need_delete = False
-    try:
-        with open(CFG_FILE, mode='xt') as write_handle:
-            need_delete = True
-            config.write(write_handle)
-        cmd = ['python3', 'setup.py', command]
-        if cmd_line_args:
-            cmd += cmd_line_args
-        if call(cmd) != 0:
-            raise RuntimeError('Running setup.py failed (cmd: {})'
-                               .format(' '.join(cmd)))
-    finally:
-        if need_delete:
-            os.unlink(CFG_FILE)
-            need_delete = False
-
-
-def create_rpm(py_version, release):
-    """Create rpm that will install pyi2ncommon for given python version."""
-    print('Creating RPM for python version {}'.format(py_version))
-
-    # define options for where to install library;
-    # It appears this is automatically translated into requirement
-    # python(abi) = version   !
-    install_options = {'prefix': '/'}
-    install_options['install-lib'] = INSTALL_DIR_PATTERN.format(py_version)
-
-    # if py_version is smaller than 3.5, need zipfile35 as extra dependency
-    need_zip35 = py_version.startswith('2') or py_version.startswith('3.0') or\
-        py_version.startswith('3.1') or py_version.startswith('3.2') or \
-        py_version.startswith('3.3') or py_version.startswith('3.4')
-
-    # create rpm
-    start_time = time.time()
-    run_setup('bdist_rpm', install_options=install_options,
-              cmd_line_args=['--release', release], need_zip35=need_zip35)
-
-    # find rpm and rename it
-    newest_names = []
-    newest_time = 0
-    for filename in iglob(join(DIST_DIR, 'pyi2ncommon-*.noarch.rpm')):
-        filetime = os.stat(filename).st_mtime
-        if filetime > newest_time:
-            newest_time = filetime
-            newest_names = [filename, ]
-        elif filetime == newest_time:
-            newest_names.append(filename)
-
-    if not newest_names:
-        raise RuntimeError('No pyi2ncommon rpm file found in {}'
-                           .format(DIST_DIR))
-    elif newest_time < start_time:
-        raise RuntimeError('Newest pyi2ncommon rpm file in {} is too old'
-                           .format(DIST_DIR))
-    elif len(newest_names) > 1:
-        raise RuntimeError('Multiple newest pyi2ncommon rpm files: {}'
-                           .format(newest_names))
-    newest_name = newest_names[0]
-    mod_name = newest_name[:-11] + '.py' + py_version.replace('.', '') + \
-               newest_name[-11:]
-    os.rename(newest_name, mod_name)
-    return mod_name
-
-
-def create_spec(release):
-    """Create .spec file and modify it."""
-    print('adapting spec file')
-    # create spec
-    run_setup('bdist_rpm', cmd_line_args=['--spec-only', '--release', release])
-
-    # adapt
-    temp_handle = None
-    temp_name = None
-    try:
-        temp_handle, temp_file = mkstemp(dir=DIST_DIR, text=False,
-                                         prefix='pyi2ncommon-adapt-',
-                                         suffix='.spec')
-        os.write(temp_handle, SPEC_START)
-        did_write_check = False
-        with open(SPEC_FILE, 'rb') as reader:
-            for line in reader:
-                print('spec file: {}'.format(line.rstrip()))
-                if line.strip() == b'%install':
-                    os.write(temp_handle, SPEC_CHECK_SECTION)
-                    did_write_check = True
-                os.write(temp_handle, line)
-        os.close(temp_handle)
-        temp_handle = None
-
-        if not did_write_check:
-            raise RuntimeError('Could not find place to write %check section')
-
-        # replace
-        os.unlink(SPEC_FILE)
-        os.rename(temp_file, SPEC_FILE)
-        temp_file = None
-    finally:
-        # clean up temp
-        if temp_handle is not None:
-            os.close(temp_handle)
-        if temp_name is not None and isfile(temp_file):
-            os.unlink(temp_file)
-    return SPEC_FILE
+    if tarinfo.name.endswith('.pyc'):
+        print(f'Skip {tarinfo.name}')
+        return None
+    if '__pycache__' in tarinfo.name:
+        print(f'Skip {tarinfo.name}')
+        return None
+    print(f'Adding {tarinfo.name}')
+    return tarinfo
+
+
+def create_tarball():
+    """Create tarball, return its full path."""
+    name, version, tarball_file = get_spec_info()
+    tarball_path = join(DIST_DIR, tarball_file)
+    print(f'Creating {tarball_path}')
+    dirname = f'{name}-{version}'
+    with tarfile.open(tarball_path, 'w:gz') as tarball:
+        for entry in DATA:
+            tarball.add(entry, join(dirname, entry), filter=tar_add_filter)
+    return tarball_path
+
+
+def build_rpm(tarball_path):
+    """Create rpm using rpmbuild with spec file and newly created tarball."""
+    copy(tarball_path, RPM_SOURCE_DIR)
+    run(('rpmbuild', '-bb', SPEC_FILE))
 
 
 def main():
     """
-    Main function, called when running file as script
+    Main function, called when running file as script.
 
-    see module doc for more info
+    See module doc for more info.
     """
-    release = None
-    py_versions = ['3.7', ]
-    if len(sys.argv) > 1:
-        if sys.argv[1] in ('--help', '-h'):
-            print('make_dist.py [releasename [py-ver1 [py-ver2...]]]')
-            print('  e.g.: make_dist.py featuretest 3.7')
-            return 2
-        release = sys.argv[1].replace('-', '_')
-    else:
-        release = check_output(['git', 'log', '--pretty=format:%h', '-n', '1'],
-                               universal_newlines=True)
-    if len(sys.argv) > 2:
-        py_versions = sys.argv[2:]
-    files_created = []
-    for py_version in py_versions:
-        check_py_version(py_version)
-        run_unittests(py_version)
-        files_created.append(create_rpm(py_version, release))
-    files_created.append(create_spec(release))
-
-    for filename in files_created:
-        print('Created {}'.format(filename))
-    print('(+ probably source rpm and tar.gz)')
+    tarball_path = create_tarball()
+    build_rpm(tarball_path)
+    print(f'{basename(__file__)} finished successfully.')
+
     return 0
 
 
 if __name__ == '__main__':
     sys.exit(main())
+