Minor fix: else --> elif
[pyi2ncommon] / make_dist.py
1 #!/usr/bin/env python3
2
3 """
4 Create rpm packages of pyi2ncommon for various python versions.
5
6 Usage::
7
8     make_dist.py [RELEASE [PY_VERSION_1 [PY_VERSION_2 ...]]]
9
10 RELEASE defaults to current git commit hash, default versions to build for are
11 3.6 for avocado and 3.3 for i2n system. All python versions used must be
12 installed on local machine.
13
14 Calls setup.py with different args. Adds %check section to .spec file. Runs
15 unittests first.
16
17 This script relies on one feature I accidentally stumbled over: it appears that
18 setting install-lib to /usr/lib/python{VERSION}/site-packages is translated by
19 setup.py to installation requirement python(abi) = {VERSION}
20
21 .. codeauthor:: Intra2net AG <info@intra2net.com>
22 """
23
24 import os
25 from os.path import join, isfile
26 import sys
27 from subprocess import call, check_output
28 from tempfile import mkstemp
29 from configparser import ConfigParser
30 from glob import iglob
31 import time
32
33 INSTALL_DIR_PATTERN = '/usr/lib/python{}/site-packages'
34
35 RPM_OPTIONS = dict(packager='Intra2net', group='Intra2net',
36                    vendor='Intra2net AG')
37
38 DIST_DIR = 'dist'
39 SPEC_FILE = join(DIST_DIR, 'pyi2ncommon.spec')
40
41 CFG_FILE = 'setup.cfg'
42
43 SPEC_START = b"""
44 # spec file created automatically by make_dist.sh -- do not modify!
45
46 """
47
48 SPEC_CHECK_SECTION = b"""%check
49 PYTHONPATH=./src:$PYTHONPATH && python3 -m unittest discover test
50
51 """
52
53
54 def check_py_version(py_version):
55     """Test that python version is installed."""
56     try:
57         result = call(['python{}'.format(py_version), '--version'])
58     except Exception:
59         result = 1
60     if result != 0:
61         raise RuntimeError('Python version {} not installed, '
62                            'run {} VERSION'.format(py_version, sys.argv[0]))
63
64
65 def run_unittests(py_version):
66     """Run unittests with given python version. Re-Run with LANG=C."""
67     # Run twice: first with environment copied from call (env=None) and one
68     # in empoverished environment without unicode capability
69     for env in None, dict(LANG='C', PATH=os.environ['PATH']):
70         try:
71             print('Running unittests with python {} and env={}'
72                   .format(py_version, env))
73             result = call(['python{}'.format(py_version), '-m', 'unittest',
74                            'discover'], env=env)
75         except Exception as exc:
76             raise RuntimeError('Unittests with python {} and env={} failed. {}'
77                                .format(py_version, env, exc))
78         if result != 0:
79             raise RuntimeError('Unittests with python {} and env={} failed '
80                                '(command returned {}).'
81                                .format(py_version, env, result))
82
83
84 def run_setup(command, cmd_line_args=None, install_options=None,
85               need_zip35=False):
86     """
87     Run python3 setup.py with command and options.
88
89     Need this function since setup.py bdist_rpm does not accept --prefix and
90     --install-lib options. Need to write them into cfg file from which they
91     are then read ... Grrr!
92     """
93     config = ConfigParser()
94     rpm_options = dict(RPM_OPTIONS.items())
95     if need_zip35:
96         rpm_options['requires'] = 'python3-zipfile35'
97     if rpm_options:
98         config['bdist_rpm'] = rpm_options
99     if install_options:
100         config['install'] = install_options
101     need_delete = False
102     try:
103         with open(CFG_FILE, mode='xt') as write_handle:
104             need_delete = True
105             config.write(write_handle)
106         cmd = ['python3', 'setup.py', command]
107         if cmd_line_args:
108             cmd += cmd_line_args
109         if call(cmd) != 0:
110             raise RuntimeError('Running setup.py failed (cmd: {})'
111                                .format(' '.join(cmd)))
112     finally:
113         if need_delete:
114             os.unlink(CFG_FILE)
115             need_delete = False
116
117
118 def create_rpm(py_version, release):
119     """Create rpm that will install pyi2ncommon for given python version."""
120     print('Creating RPM for python version {}'.format(py_version))
121
122     # define options for where to install library;
123     # It appears this is automatically translated into requirement
124     # python(abi) = version   !
125     install_options = {'prefix': '/'}
126     install_options['install-lib'] = INSTALL_DIR_PATTERN.format(py_version)
127
128     # if py_version is smaller than 3.5, need zipfile35 as extra dependency
129     need_zip35 = py_version.startswith('2') or py_version.startswith('3.0') or\
130         py_version.startswith('3.1') or py_version.startswith('3.2') or \
131         py_version.startswith('3.3') or py_version.startswith('3.4')
132
133     # create rpm
134     start_time = time.time()
135     run_setup('bdist_rpm', install_options=install_options,
136               cmd_line_args=['--release', release], need_zip35=need_zip35)
137
138     # find rpm and rename it
139     newest_names = []
140     newest_time = 0
141     for filename in iglob(join(DIST_DIR, 'pyi2ncommon-*.noarch.rpm')):
142         filetime = os.stat(filename).st_mtime
143         if filetime > newest_time:
144             newest_time = filetime
145             newest_names = [filename, ]
146         elif filetime == newest_time:
147             newest_names.append(filename)
148
149     if not newest_names:
150         raise RuntimeError('No pyi2ncommon rpm file found in {}'
151                            .format(DIST_DIR))
152     elif newest_time < start_time:
153         raise RuntimeError('Newest pyi2ncommon rpm file in {} is too old'
154                            .format(DIST_DIR))
155     elif len(newest_names) > 1:
156         raise RuntimeError('Multiple newest pyi2ncommon rpm files: {}'
157                            .format(newest_names))
158     newest_name = newest_names[0]
159     mod_name = newest_name[:-11] + '.py' + py_version.replace('.', '') + \
160                newest_name[-11:]
161     os.rename(newest_name, mod_name)
162     return mod_name
163
164
165 def create_spec(release):
166     """Create .spec file and modify it."""
167     print('adapting spec file')
168     # create spec
169     run_setup('bdist_rpm', cmd_line_args=['--spec-only', '--release', release])
170
171     # adapt
172     temp_handle = None
173     temp_name = None
174     try:
175         temp_handle, temp_file = mkstemp(dir=DIST_DIR, text=False,
176                                          prefix='pyi2ncommon-adapt-',
177                                          suffix='.spec')
178         os.write(temp_handle, SPEC_START)
179         did_write_check = False
180         with open(SPEC_FILE, 'rb') as reader:
181             for line in reader:
182                 print('spec file: {}'.format(line.rstrip()))
183                 if line.strip() == b'%install':
184                     os.write(temp_handle, SPEC_CHECK_SECTION)
185                     did_write_check = True
186                 os.write(temp_handle, line)
187         os.close(temp_handle)
188         temp_handle = None
189
190         if not did_write_check:
191             raise RuntimeError('Could not find place to write %check section')
192
193         # replace
194         os.unlink(SPEC_FILE)
195         os.rename(temp_file, SPEC_FILE)
196         temp_file = None
197     finally:
198         # clean up temp
199         if temp_handle is not None:
200             os.close(temp_handle)
201         if temp_name is not None and isfile(temp_file):
202             os.unlink(temp_file)
203     return SPEC_FILE
204
205
206 def main():
207     """
208     Main function, called when running file as script
209
210     see module doc for more info
211     """
212     release = None
213     py_versions = ['3.7', ]
214     if len(sys.argv) > 1:
215         if sys.argv[1] in ('--help', '-h'):
216             print('make_dist.py [releasename [py-ver1 [py-ver2...]]]')
217             print('  e.g.: make_dist.py featuretest 3.7')
218             return 2
219         release = sys.argv[1].replace('-', '_')
220     else:
221         release = check_output(['git', 'log', '--pretty=format:%h', '-n', '1'],
222                                universal_newlines=True)
223     if len(sys.argv) > 2:
224         py_versions = sys.argv[2:]
225     files_created = []
226     for py_version in py_versions:
227         check_py_version(py_version)
228         run_unittests(py_version)
229         files_created.append(create_rpm(py_version, release))
230     files_created.append(create_spec(release))
231
232     for filename in files_created:
233         print('Created {}'.format(filename))
234     print('(+ probably source rpm and tar.gz)')
235     return 0
236
237
238 if __name__ == '__main__':
239     sys.exit(main())