-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtravis_yml_generator.py
More file actions
executable file
·319 lines (267 loc) · 10.2 KB
/
travis_yml_generator.py
File metadata and controls
executable file
·319 lines (267 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
#!/usr/bin/env python3
import os
import sys
import argparse
import subprocess
import datetime
def _log(prefix, color, message, color_full_line=True):
if color_full_line:
print(f'{color}{prefix}: {message}\033[0m');
else:
print(f'{color}{prefix}:\033[0m {message}');
def log_t(msg): _log('TRC', '\033[90m', msg, color_full_line=True)
def log_d(msg): _log('DBG', '\033[39m', msg)
def log_i(msg): _log('INF', '\033[94m', msg)
def log_w(msg): _log('WRN', '\033[33m', msg)
def log_e(msg): _log('ERR', '\033[91m', msg)
def shell_exec(cmd, timeout_s=5, check_succ=True):
cmd_copy = []
quoter_parts = []
quoter_type = '"'
for p in cmd.split():
if quoter_parts:
if not p.endswith(quoter_type):
quoter_parts.append(p)
else:
quoter_parts.append(p[:-1])
cmd_copy.append(' '.join(quoter_parts))
quoter_parts.clear()
else:
if p.startswith('"'):
quoter_type = '"'
if p.endswith(quoter_type):
cmd_copy.append(p)
else:
quoter_parts.append(p[1:])
elif p.startswith("'"):
quoter_type = "'"
if p.endswith(quoter_type):
cmd_copy.append(p)
else:
quoter_parts.append(p[1:])
else:
cmd_copy.append(p)
log_t('C>[{}]'.format(' '.join(cmd_copy)))
res = subprocess.run(cmd_copy, capture_output=True, timeout=timeout_s)
ec = res.returncode
out = res.stdout.decode('utf-8').rstrip()
if out and len(out.split('\n')) > 1:
log_t(f'R({ec})<\n{out}')
else:
log_t(f'R({ec})<[{out}]')
if check_succ:
res.check_returncode()
return out
return ec, out
class Config:
CTRL_PREFIXES = ['hw']
GIT_ROOT = '.'
GIT_INIT_COMMIT = 'd829c61855aff7241e12d8578d4a66a7118eb327'
TRAVIS_CI_CFG_NAME = '.travis.yml'
TRAVIS_CI_DIR = '.git/travis-ci'
LAST_GREEN_COMMIT_FNAME = '.git/travis-ci/last_green_build_commit'
STATE_FNAME = '.git/travis-ci/state'
NEED_INIT = False
SET_READY = False
SAVE_COMMIT = None
IS_CONTINUE = False
@staticmethod
def init_cfg():
log_d('class @Config initialisation')
Config.GIT_INIT_COMMIT = shell_exec('git rev-list --max-parents=0 HEAD')
Config.GIT_ROOT = shell_exec('git rev-parse --show-toplevel').strip()
Config.TRAVIS_CI_DIR = f'{Config.GIT_ROOT}/.git/travis-ci'
Config.LAST_GREEN_COMMIT_FNAME = f'{Config.TRAVIS_CI_DIR}/last_green_build_commit'
Config.STATE_FNAME = f'{Config.TRAVIS_CI_DIR}/state'
@staticmethod
def self_path(is_abs=False):
rel_path = 'scripts/travis_yml_generator.py'
return f'{Config.GIT_ROOT}/{rel_path}' if is_abs else rel_path
@staticmethod
def parse_args():
parser = argparse.ArgumentParser(
description='Generator of Tracis-CI configuration before pushing changes to GitHub')
parser.add_argument('--init', action='store_true', help='Initialize repo for developer')
parser.add_argument('--set_ready', action='store_true', help='Approve pushing changes')
parser.add_argument('--set_commit', metavar='HASH', help='Set last \033[92mGREEN\033[0m commit')
parser.add_argument('--exec_main', action='store_true', help='Execute main functionality')
args = parser.parse_args()
Config.NEED_INIT = args.init
Config.SET_READY = args.set_ready
Config.SAVE_COMMIT = args.set_commit
Config.IS_CONTINUE = args.exec_main
@staticmethod
def set_state(state):
assert type(state) == type(True)
with open(Config.STATE_FNAME, 'w') as f:
f.write('READY' if state else 'NOT READY')
@staticmethod
def get_state():
if not os.path.isfile(Config.STATE_FNAME): return False
state = ''
with open(Config.STATE_FNAME, 'r') as f: state = f.readline().strip()
if not state: return False
return state == 'READY'
@staticmethod
def create_working_dir_if_needed():
if not os.path.isdir(Config.TRAVIS_CI_DIR):
os.mkdir(Config.TRAVIS_CI_DIR, mode=0o755)
@staticmethod
def set_commit(commit):
with open(Config.LAST_GREEN_COMMIT_FNAME, 'w') as f:
f.write(commit)
class Project:
def __init__(self, dir_name):
self.sep = '-'
self.dir = dir_name
self.build_dir = dir_name + '/build'
sep_index = dir_name.index(self.sep)
self.prefix = dir_name[0:sep_index]
self.name = dir_name[sep_index+1:]
def full_name(self):
return '{}{}{}'.format(self.prefix, self.sep, self.name)
def has_ut(self):
cmake_cfg = self.dir + '/CMakeLists.txt'
if not os.path.isfile(cmake_cfg): return False
ec, _ = shell_exec(f"grep -w enable_testing {cmake_cfg}", check_succ=False)
return 0 == ec
# for set()
def __hash__(self):
return hash(self.dir)
def __eq__(self, v):
return self.dir == v.dir
# for sorted()
def __lt__(self, v):
return self.dir < v.dir
def gen_warning():
return """# This is autogen file by tools/travis_yml_generator.py
# Do not change it manualy because after a next `git push` command this file
# will be regenerated.
#
"""
def last_green_travis_build():
commit = ''
if os.path.isfile(Config.LAST_GREEN_COMMIT_FNAME):
with open(Config.LAST_GREEN_COMMIT_FNAME, 'r') as f: commit = f.readline()
return commit if commit else Config.GIT_INIT_COMMIT
def gen_travis_yaml(changed_projects):
TRAVIS_CI_CFG_PATH = f'{Config.GIT_ROOT}/{Config.TRAVIS_CI_CFG_NAME}'
if not changed_projects: return False
changed_projects = sorted(changed_projects)
stage_parts = []
for project in changed_projects:
yml_part = ' - script: ' if stage_parts else ' - stage: Build && Test && Deploy\n script: '
cmd_test = "cmake --build . --target test"
commands = [
f"mkdir -p '{project.build_dir}'",
f"pushd '{project.build_dir}'",
"cmake -DGTEST_ROOT=/tmp/gtest-install ..",
"cmake --build .",
cmd_test,
"cmake --build . --target package",
"popd",
]
if not project.has_ut(): commands.remove(cmd_test)
yml_part += ' && '.join(commands)
yml_part += f"""\n deploy:
provider: script
skip_cleanup: true
script: bash scripts/deploy.sh {project.name} {project.build_dir}
on:
branch: main
"""
stage_parts.append(yml_part)
with open(TRAVIS_CI_CFG_PATH, 'w') as f:
f.write(gen_warning())
f.write("""language: cpp
os: linux
dist: focal
compiler: gcc
before_install:
- sudo add-apt-repository --yes ppa:ubuntu-toolchain-r/test
- sudo apt-get update -qq
install:
- sudo apt-get install -qq g++-10
- sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 90 --slave /usr/bin/g++ g++ /usr/bin/g++-10
before_script:
- sudo apt-get install libboost-test-dev -y
- echo "deb http://archive.ubuntu.com/ubuntu xenial main universe" | sudo tee -a /etc/apt/sources.list
- sudo apt-get update -qq
- mkdir /tmp/gtest-src /tmp/gtest-build /tmp/gtest-install
- git clone https://github.com/google/googletest /tmp/gtest-src
- pushd /tmp/gtest-build && cmake -DCMAKE_INSTALL_PREFIX:PATH=/tmp/gtest-install /tmp/gtest-src && cmake --build . && cmake --build . --target install && popd
jobs:
include:
""")
for part in stage_parts:
f.write(part)
f.write('\n')
f.write('\n')
return TRAVIS_CI_CFG_PATH
def commit_changes(yml):
shell_exec(f'git add {yml}')
if [1 for line in shell_exec('git status -uno --porcelain').split('\n')]:
yml_name = yml.split('/')[-1]
return 0 == shell_exec(f'git commit -m "[AUTO] regen {yml_name} before pushing"', check_succ=False)[0]
def InitRepo():
Config.create_working_dir_if_needed()
Config.set_commit(Config.GIT_INIT_COMMIT)
Config.set_state(False)
with open(f'{Config.GIT_ROOT}/.git/hooks/pre-push', 'w') as f:
f.write('''#!/bin/sh
#
# This file was generated by scripts/travis_yml_generator.py
# Creation time point: {1}
#
if [ -f '{0}' ] ; then
python3 '{0}'
else
echo "ERROR: can't find ['{0}']"
exit 1
fi
'''.format(Config.self_path(), datetime.datetime.now().isoformat()))
def main():
Config.parse_args()
Config.init_cfg()
need_exit = False
if Config.NEED_INIT: InitRepo(); need_exit = True
if Config.SET_READY: Config.set_state(True); need_exit = True
if Config.SAVE_COMMIT: Config.set_commit(Config.SAVE_COMMIT); need_exit = True
if Config.IS_CONTINUE: need_exit = False
if need_exit: return
log_i('Getting last green commit')
last_green = last_green_travis_build()
log_i('Collect valued changes')
changed = set()
for line in shell_exec(f'git diff --name-only HEAD {last_green}').split('\n'):
file_root = line.split('/')[0]
for prefix in Config.CTRL_PREFIXES:
if file_root.startswith(prefix):
changed.add(Project(file_root))
break
if changed:
log_i('Generation new CI configuration for {} project(s)'.format(len(changed)))
yml_name = gen_travis_yaml(changed)
if commit_changes(yml_name):
log_w('Disable "git push"')
Config.set_state(False)
if not Config.get_state():
self_path = Config.self_path(is_abs=True)
push_cmd = f'{self_path} --set_ready && git push\n' if changed else ''
log_e('''
=======================================
!!! 'git push' command was disabled !!!
=======================================
Latest saved commit: {0}
Set a new commit: --set_commit NEW_COMMIT
Enabling 'git push': --set_ready
Possible commads:
{1} --set_ready
{1} --set_commit NEW_COMMIT
{1} --set_ready --set_commit NEW_COMMIT
{2}
'''.format(last_green, self_path, push_cmd))
return 1
return 0
if __name__ == '__main__':
sys.exit(main())