1 | module main |
2 | |
3 | import os |
4 | import log |
5 | import flag |
6 | import time |
7 | import vweb |
8 | import 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 |
25 | const ( |
26 | git_username = os.getenv('GITUSER') |
27 | git_password = os.getenv('GITPASS') |
28 | ) |
29 | |
30 | // repository |
31 | const ( |
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 |
41 | const ( |
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) |
59 | const ( |
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 |
73 | const ( |
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 | |
80 | struct GenVC { |
81 | // logger |
82 | // flag options |
83 | options FlagOptions |
84 | mut: |
85 | logger &log.Log = unsafe { nil } |
86 | // true if error was experienced running generate |
87 | gen_error bool |
88 | } |
89 | |
90 | // webhook server |
91 | struct WebhookServer { |
92 | vweb.Context |
93 | mut: |
94 | gen_vc &GenVC = unsafe { nil } // initialized in init_server |
95 | } |
96 | |
97 | // storage for flag options |
98 | struct 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 | |
109 | fn 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 |
138 | fn 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 |
151 | pub 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 | |
159 | pub fn (mut ws WebhookServer) index() { |
160 | eprintln('WebhookServer.index() called') |
161 | } |
162 | |
163 | // gen webhook |
164 | pub 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 | |
178 | pub fn (ws &WebhookServer) reset() { |
179 | } |
180 | |
181 | // parse flags to FlagOptions struct |
182 | fn 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 | |
195 | fn (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 |
203 | fn (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 |
300 | fn (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 |
305 | fn (mut gen_vc GenVC) cmd_exec(cmd string) string { |
306 | return gen_vc.command_execute(cmd, false) |
307 | } |
308 | |
309 | // execute command |
310 | fn (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 |
337 | fn (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 |
343 | fn (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 |
358 | fn (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 | } |