v / cmd / tools
Raw file | 370 loc (345 sloc) | 11.55 KB | Latest commit hash ef5be22f8
1module main
2
3import os
4import log
5import flag
6import time
7import vweb
8import net.urllib
9
10// This tool regenerates V's bootstrap .c files
11// every time the V master branch is updated.
12// if run with the --serve flag it will run in webhook
13// server mode awaiting a request to http://host:port/genhook
14// available command line flags:
15// --work-dir gen_vc's working directory
16// --purge force purge the local repositories
17// --serve run in webhook server mode
18// --port port for http server to listen on
19// --log-to either 'file' or 'terminal'
20// --log-file path to log file used when --log-to is 'file'
21// --dry-run dont push anything to remote repo
22// --force force update even if already up to date
23
24// git credentials
25const (
26 git_username = os.getenv('GITUSER')
27 git_password = os.getenv('GITPASS')
28)
29
30// repository
31const (
32 // git repo
33 git_repo_v = 'github.com/vlang/v'
34 git_repo_vc = 'github.com/vlang/vc'
35 // local repo directories
36 git_repo_dir_v = 'v'
37 git_repo_dir_vc = 'vc'
38)
39
40// gen_vc
41const (
42 // name
43 app_name = 'gen_vc'
44 // version
45 app_version = '0.1.3'
46 // description
47 app_description = "This tool regenerates V's bootstrap .c files every time the V master branch is updated."
48 // assume something went wrong if file size less than this
49 too_short_file_limit = 5000
50 // create a .c file for these os's
51 vc_build_oses = [
52 'nix',
53 // all nix based os
54 'windows',
55 ]
56)
57
58// default options (overridden by flags)
59const (
60 // gen_vc working directory
61 work_dir = '/tmp/gen_vc'
62 // dont push anything to remote repo
63 dry_run = false
64 // server port
65 server_port = 7171
66 // log file
67 log_file = '${work_dir}/log.txt'
68 // log_to is either 'file' or 'terminal'
69 log_to = 'terminal'
70)
71
72// errors
73const (
74 err_msg_build = 'error building'
75 err_msg_make = 'make failed'
76 err_msg_gen_c = 'failed to generate .c file'
77 err_msg_cmd_x = 'error running cmd'
78)
79
80struct GenVC {
81 // logger
82 // flag options
83 options FlagOptions
84mut:
85 logger &log.Log = unsafe { nil }
86 // true if error was experienced running generate
87 gen_error bool
88}
89
90// webhook server
91struct WebhookServer {
92 vweb.Context
93mut:
94 gen_vc &GenVC = unsafe { nil } // initialized in init_server
95}
96
97// storage for flag options
98struct FlagOptions {
99 work_dir string
100 purge bool
101 serve bool
102 port int
103 log_to string
104 log_file string
105 dry_run bool
106 force bool
107}
108
109fn main() {
110 mut fp := flag.new_flag_parser(os.args.clone())
111 fp.application(app_name)
112 fp.version(app_version)
113 fp.description(app_description)
114 fp.skip_executable()
115 show_help := fp.bool('help', 0, false, 'Show this help screen\n')
116 flag_options := parse_flags(mut fp)
117 if show_help {
118 println(fp.usage())
119 exit(0)
120 }
121 fp.finalize() or {
122 eprintln(err)
123 println(fp.usage())
124 return
125 }
126 // webhook server mode
127 if flag_options.serve {
128 vweb.run[WebhookServer](&WebhookServer{}, flag_options.port)
129 } else {
130 // cmd mode
131 mut gen_vc := new_gen_vc(flag_options)
132 gen_vc.init()
133 gen_vc.generate()
134 }
135}
136
137// new GenVC
138fn new_gen_vc(flag_options FlagOptions) &GenVC {
139 mut logger := &log.Log{}
140 logger.set_level(.debug)
141 if flag_options.log_to == 'file' {
142 logger.set_full_logpath(flag_options.log_file)
143 }
144 return &GenVC{
145 options: flag_options
146 logger: logger
147 }
148}
149
150// WebhookServer init
151pub fn (mut ws WebhookServer) init_server() {
152 mut fp := flag.new_flag_parser(os.args.clone())
153 flag_options := parse_flags(mut fp)
154 ws.gen_vc = new_gen_vc(flag_options)
155 ws.gen_vc.init()
156 // ws.gen_vc = new_gen_vc(flag_options)
157}
158
159pub fn (mut ws WebhookServer) index() {
160 eprintln('WebhookServer.index() called')
161}
162
163// gen webhook
164pub fn (mut ws WebhookServer) genhook() {
165 // request data
166 // println(ws.vweb.req.data)
167 // TODO: parse request. json or urlencoded
168 // json.decode or net.urllib.parse
169 ws.gen_vc.generate()
170 // error in generate
171 if ws.gen_vc.gen_error {
172 ws.json('{status: "failed"}')
173 return
174 }
175 ws.json('{status: "ok"}')
176}
177
178pub fn (ws &WebhookServer) reset() {
179}
180
181// parse flags to FlagOptions struct
182fn parse_flags(mut fp flag.FlagParser) FlagOptions {
183 return FlagOptions{
184 serve: fp.bool('serve', 0, false, 'run in webhook server mode')
185 work_dir: fp.string('work-dir', 0, work_dir, 'gen_vc working directory')
186 purge: fp.bool('purge', 0, false, 'force purge the local repositories')
187 port: fp.int('port', 0, server_port, 'port for web server to listen on')
188 log_to: fp.string('log-to', 0, log_to, "log to is 'file' or 'terminal'")
189 log_file: fp.string('log-file', 0, log_file, "log file to use when log-to is 'file'")
190 dry_run: fp.bool('dry-run', 0, dry_run, 'when specified dont push anything to remote repo')
191 force: fp.bool('force', 0, false, 'force update even if already up to date')
192 }
193}
194
195fn (mut gen_vc GenVC) init() {
196 // purge repos if flag is passed
197 if gen_vc.options.purge {
198 gen_vc.purge_repos()
199 }
200}
201
202// regenerate
203fn (mut gen_vc GenVC) generate() {
204 // set errors to false
205 gen_vc.gen_error = false
206 // check if gen_vc dir exists
207 if !os.is_dir(gen_vc.options.work_dir) {
208 // try create
209 os.mkdir(gen_vc.options.work_dir) or { panic(err) }
210 // still dosen't exist... we have a problem
211 if !os.is_dir(gen_vc.options.work_dir) {
212 gen_vc.logger.error('error creating directory: ${gen_vc.options.work_dir}')
213 gen_vc.gen_error = true
214 return
215 }
216 }
217 // cd to gen_vc dir
218 os.chdir(gen_vc.options.work_dir) or {}
219 // if we are not running with the --serve flag (webhook server)
220 // rather than deleting and re-downloading the repo each time
221 // first check to see if the local v repo is behind master
222 // if it isn't behind theres no point continuing further
223 if !gen_vc.options.serve && os.is_dir(git_repo_dir_v) {
224 gen_vc.cmd_exec('git -C ${git_repo_dir_v} checkout master')
225 // fetch the remote repo just in case there are newer commits there
226 gen_vc.cmd_exec('git -C ${git_repo_dir_v} fetch')
227 git_status := gen_vc.cmd_exec('git -C ${git_repo_dir_v} status')
228 if !git_status.contains('behind') && !gen_vc.options.force {
229 gen_vc.logger.warn('v repository is already up to date.')
230 return
231 }
232 }
233 // delete repos
234 gen_vc.purge_repos()
235 // clone repos
236 gen_vc.cmd_exec('git clone --filter=blob:none https://${git_repo_v} ${git_repo_dir_v}')
237 gen_vc.cmd_exec('git clone --filter=blob:none https://${git_repo_vc} ${git_repo_dir_vc}')
238 // get output of git log -1 (last commit)
239 git_log_v := gen_vc.cmd_exec('git -C ${git_repo_dir_v} log -1 --format="commit %H%nDate: %ci%nDate Unix: %ct%nSubject: %s"')
240 git_log_vc := gen_vc.cmd_exec('git -C ${git_repo_dir_vc} log -1 --format="Commit %H%nDate: %ci%nDate Unix: %ct%nSubject: %s"')
241 // date of last commit in each repo
242 ts_v := git_log_v.find_between('Date:', '\n').trim_space()
243 ts_vc := git_log_vc.find_between('Date:', '\n').trim_space()
244 // parse time as string to time.Time
245 last_commit_time_v := time.parse(ts_v) or { panic(err) }
246 last_commit_time_vc := time.parse(ts_vc) or { panic(err) }
247 // git dates are in users local timezone and v time.parse does not parse
248 // timezones at the moment, so for now get unix timestamp from output also
249 t_unix_v := git_log_v.find_between('Date Unix:', '\n').trim_space().int()
250 t_unix_vc := git_log_vc.find_between('Date Unix:', '\n').trim_space().int()
251 // last commit hash in v repo
252 last_commit_hash_v := git_log_v.find_between('commit', '\n').trim_space()
253 last_commit_hash_v_short := last_commit_hash_v[..7]
254 // subject
255 last_commit_subject := git_log_v.find_between('Subject:', '\n').trim_space().replace("'",
256 '"')
257 // log some info
258 gen_vc.logger.debug('last commit time (${git_repo_v}): ' + last_commit_time_v.format_ss())
259 gen_vc.logger.debug('last commit time (${git_repo_vc}): ' + last_commit_time_vc.format_ss())
260 gen_vc.logger.debug('last commit hash (${git_repo_v}): ${last_commit_hash_v}')
261 gen_vc.logger.debug('last commit subject (${git_repo_v}): ${last_commit_subject}')
262 // if vc repo already has a newer commit than the v repo, assume it's up to date
263 if t_unix_vc >= t_unix_v && !gen_vc.options.force {
264 gen_vc.logger.warn('vc repository is already up to date.')
265 return
266 }
267 // try build v for current os (linux in this case)
268 gen_vc.cmd_exec('make -C ${git_repo_dir_v}')
269 v_exec := '${git_repo_dir_v}/v'
270 // check if make was successful
271 gen_vc.assert_file_exists_and_is_not_too_short(v_exec, err_msg_make)
272 // build v.c for each os
273 for os_name in vc_build_oses {
274 c_file := if os_name == 'nix' { 'v.c' } else { 'v_win.c' }
275 v_flags := if os_name == 'nix' { '-os cross' } else { '-os ${os_name}' }
276 // try generate .c file
277 gen_vc.cmd_exec('${v_exec} ${v_flags} -o ${c_file} ${git_repo_dir_v}/cmd/v')
278 // check if the c file seems ok
279 gen_vc.assert_file_exists_and_is_not_too_short(c_file, err_msg_gen_c)
280 // embed the latest v commit hash into the c file
281 gen_vc.cmd_exec('sed -i \'1s/^/#define V_COMMIT_HASH "${last_commit_hash_v_short}"\\n/\' ${c_file}')
282 // move to vc repo
283 gen_vc.cmd_exec('mv ${c_file} ${git_repo_dir_vc}/${c_file}')
284 // add new .c file to local vc repo
285 gen_vc.cmd_exec('git -C ${git_repo_dir_vc} add ${c_file}')
286 }
287 // check if the vc repo actually changed
288 git_status := gen_vc.cmd_exec('git -C ${git_repo_dir_vc} status')
289 if git_status.contains('nothing to commit') {
290 gen_vc.logger.error('no changes to vc repo: something went wrong.')
291 gen_vc.gen_error = true
292 }
293 // commit changes to local vc repo
294 gen_vc.cmd_exec_safe("git -C ${git_repo_dir_vc} commit -m '[v:master] ${last_commit_hash_v_short} - ${last_commit_subject}'")
295 // push changes to remote vc repo
296 gen_vc.cmd_exec_safe('git -C ${git_repo_dir_vc} push https://${urllib.query_escape(git_username)}:${urllib.query_escape(git_password)}@${git_repo_vc} master')
297}
298
299// only execute when dry_run option is false, otherwise just log
300fn (mut gen_vc GenVC) cmd_exec_safe(cmd string) string {
301 return gen_vc.command_execute(cmd, gen_vc.options.dry_run)
302}
303
304// always execute command
305fn (mut gen_vc GenVC) cmd_exec(cmd string) string {
306 return gen_vc.command_execute(cmd, false)
307}
308
309// execute command
310fn (mut gen_vc GenVC) command_execute(cmd string, dry bool) string {
311 // if dry is true then dont execute, just log
312 if dry {
313 return gen_vc.command_execute_dry(cmd)
314 }
315 gen_vc.logger.info('cmd: ${cmd}')
316 r := os.execute(cmd)
317 if r.exit_code < 0 {
318 gen_vc.logger.error('${err_msg_cmd_x}: "${cmd}" could not start.')
319 gen_vc.logger.error(r.output)
320 // something went wrong, better start fresh next time
321 gen_vc.purge_repos()
322 gen_vc.gen_error = true
323 return ''
324 }
325 if r.exit_code != 0 {
326 gen_vc.logger.error('${err_msg_cmd_x}: "${cmd}" failed.')
327 gen_vc.logger.error(r.output)
328 // something went wrong, better start fresh next time
329 gen_vc.purge_repos()
330 gen_vc.gen_error = true
331 return ''
332 }
333 return r.output
334}
335
336// just log cmd, dont execute
337fn (mut gen_vc GenVC) command_execute_dry(cmd string) string {
338 gen_vc.logger.info('cmd (dry): "${cmd}"')
339 return ''
340}
341
342// delete repo directories
343fn (mut gen_vc GenVC) purge_repos() {
344 // delete old repos (better to be fully explicit here, since these are destructive operations)
345 mut repo_dir := '${gen_vc.options.work_dir}/${git_repo_dir_v}'
346 if os.is_dir(repo_dir) {
347 gen_vc.logger.info('purging local repo: "${repo_dir}"')
348 gen_vc.cmd_exec('rm -rf ${repo_dir}')
349 }
350 repo_dir = '${gen_vc.options.work_dir}/${git_repo_dir_vc}'
351 if os.is_dir(repo_dir) {
352 gen_vc.logger.info('purging local repo: "${repo_dir}"')
353 gen_vc.cmd_exec('rm -rf ${repo_dir}')
354 }
355}
356
357// check if file size is too short
358fn (mut gen_vc GenVC) assert_file_exists_and_is_not_too_short(f string, emsg string) {
359 if !os.exists(f) {
360 gen_vc.logger.error('${err_msg_build}: ${emsg} .')
361 gen_vc.gen_error = true
362 return
363 }
364 fsize := os.file_size(f)
365 if fsize < too_short_file_limit {
366 gen_vc.logger.error('${err_msg_build}: ${f} exists, but is too short: only ${fsize} bytes.')
367 gen_vc.gen_error = true
368 return
369 }
370}