Acceptance tests: do not show canceled test logs on GitLab CI
[qemu/ar7.git] / scripts / ninjatool.py
blob6f0e35c727f74c28ab79762f1237ceff2a7fa3d6
1 #! /bin/sh
3 # Python module for parsing and processing .ninja files.
5 # Author: Paolo Bonzini
7 # Copyright (C) 2019 Red Hat, Inc.
10 # We don't want to put "#! @PYTHON@" as the shebang and
11 # make the file executable, so instead we make this a
12 # Python/shell polyglot. The first line below starts a
13 # multiline string literal for Python, while it is just
14 # ":" for bash. The closing of the multiline string literal
15 # is never parsed by bash since it exits before.
17 '''':
18 case "$0" in
19 /*) me=$0 ;;
20 *) me=$(command -v "$0") ;;
21 esac
22 python="@PYTHON@"
23 case $python in
24 @*) python=python3 ;;
25 esac
26 exec $python "$me" "$@"
27 exit 1
28 '''
31 from collections import namedtuple, defaultdict
32 import sys
33 import os
34 import re
35 import json
36 import argparse
37 import hashlib
38 import shutil
41 class InvalidArgumentError(Exception):
42 pass
44 # faster version of os.path.normpath: do nothing unless there is a double
45 # slash or a "." or ".." component. The filter does not have to be super
46 # precise, but it has to be fast. os.path.normpath is the hottest function
47 # for ninja2make without this optimization!
48 if os.path.sep == '/':
49 def normpath(path, _slow_re=re.compile('/[./]')):
50 return os.path.normpath(path) if _slow_re.search(path) or path[0] == '.' else path
51 else:
52 normpath = os.path.normpath
55 def sha1_text(text):
56 return hashlib.sha1(text.encode()).hexdigest()
58 # ---- lexer and parser ----
60 PATH_RE = r"[^$\s:|]+|\$[$ :]|\$[a-zA-Z0-9_-]+|\$\{[a-zA-Z0-9_.-]+\}"
62 SIMPLE_PATH_RE = re.compile(r"^[^$\s:|]+$")
63 IDENT_RE = re.compile(r"[a-zA-Z0-9_.-]+$")
64 STRING_RE = re.compile(r"(" + PATH_RE + r"|[\s:|])(?:\r?\n)?|.")
65 TOPLEVEL_RE = re.compile(r"([=:#]|\|\|?|^ +|(?:" + PATH_RE + r")+)\s*|.")
66 VAR_RE=re.compile(r'\$\$|\$\{([^}]*)\}')
68 BUILD = 1
69 POOL = 2
70 RULE = 3
71 DEFAULT = 4
72 EQUALS = 5
73 COLON = 6
74 PIPE = 7
75 PIPE2 = 8
76 IDENT = 9
77 INCLUDE = 10
78 INDENT = 11
79 EOL = 12
82 class LexerError(Exception):
83 pass
86 class ParseError(Exception):
87 pass
90 class NinjaParserEvents(object):
91 def __init__(self, parser):
92 self.parser = parser
94 def dollar_token(self, word, in_path=False):
95 return '$$' if word == '$' else word
97 def variable_expansion_token(self, varname):
98 return '${%s}' % varname
100 def variable(self, name, arg):
101 pass
103 def begin_file(self):
104 pass
106 def end_file(self):
107 pass
109 def end_scope(self):
110 pass
112 def begin_pool(self, name):
113 pass
115 def begin_rule(self, name):
116 pass
118 def begin_build(self, out, iout, rule, in_, iin, orderdep):
119 pass
121 def default(self, targets):
122 pass
125 class NinjaParser(object):
127 InputFile = namedtuple('InputFile', 'filename iter lineno')
129 def __init__(self, filename, input):
130 self.stack = []
131 self.top = None
132 self.iter = None
133 self.lineno = None
134 self.match_keyword = False
135 self.push(filename, input)
137 def file_changed(self):
138 self.iter = self.top.iter
139 self.lineno = self.top.lineno
140 if self.top.filename is not None:
141 os.chdir(os.path.dirname(self.top.filename) or '.')
143 def push(self, filename, input):
144 if self.top:
145 self.top.lineno = self.lineno
146 self.top.iter = self.iter
147 self.stack.append(self.top)
148 self.top = self.InputFile(filename=filename or 'stdin',
149 iter=self._tokens(input), lineno=0)
150 self.file_changed()
152 def pop(self):
153 if len(self.stack):
154 self.top = self.stack[-1]
155 self.stack.pop()
156 self.file_changed()
157 else:
158 self.top = self.iter = None
160 def next_line(self, input):
161 line = next(input).rstrip()
162 self.lineno += 1
163 while len(line) and line[-1] == '$':
164 line = line[0:-1] + next(input).strip()
165 self.lineno += 1
166 return line
168 def print_token(self, tok):
169 if tok == EOL:
170 return "end of line"
171 if tok == BUILD:
172 return '"build"'
173 if tok == POOL:
174 return '"pool"'
175 if tok == RULE:
176 return '"rule"'
177 if tok == DEFAULT:
178 return '"default"'
179 if tok == EQUALS:
180 return '"="'
181 if tok == COLON:
182 return '":"'
183 if tok == PIPE:
184 return '"|"'
185 if tok == PIPE2:
186 return '"||"'
187 if tok == INCLUDE:
188 return '"include"'
189 if tok == IDENT:
190 return 'identifier'
191 return '"%s"' % tok
193 def error(self, msg):
194 raise LexerError("%s:%d: %s" % (self.stack[-1].filename, self.lineno, msg))
196 def parse_error(self, msg):
197 raise ParseError("%s:%d: %s" % (self.stack[-1].filename, self.lineno, msg))
199 def expected(self, expected, tok):
200 msg = "found %s, expected " % (self.print_token(tok), )
201 for i, exp_tok in enumerate(expected):
202 if i > 0:
203 msg = msg + (' or ' if i == len(expected) - 1 else ', ')
204 msg = msg + self.print_token(exp_tok)
205 self.parse_error(msg)
207 def _variable_tokens(self, value):
208 for m in STRING_RE.finditer(value):
209 match = m.group(1)
210 if not match:
211 self.error("unexpected '%s'" % (m.group(0), ))
212 yield match
214 def _tokens(self, input):
215 while True:
216 try:
217 line = self.next_line(input)
218 except StopIteration:
219 return
220 for m in TOPLEVEL_RE.finditer(line):
221 match = m.group(1)
222 if not match:
223 self.error("unexpected '%s'" % (m.group(0), ))
224 if match == ':':
225 yield COLON
226 continue
227 if match == '|':
228 yield PIPE
229 continue
230 if match == '||':
231 yield PIPE2
232 continue
233 if match[0] == ' ':
234 yield INDENT
235 continue
236 if match[0] == '=':
237 yield EQUALS
238 value = line[m.start() + 1:].lstrip()
239 yield from self._variable_tokens(value)
240 break
241 if match[0] == '#':
242 break
244 # identifier
245 if self.match_keyword:
246 if match == 'build':
247 yield BUILD
248 continue
249 if match == 'pool':
250 yield POOL
251 continue
252 if match == 'rule':
253 yield RULE
254 continue
255 if match == 'default':
256 yield DEFAULT
257 continue
258 if match == 'include':
259 filename = line[m.start() + 8:].strip()
260 self.push(filename, open(filename, 'r'))
261 break
262 if match == 'subninja':
263 self.error('subninja is not supported')
264 yield match
265 yield EOL
267 def parse(self, events):
268 global_var = True
270 def look_for(*expected):
271 # The last token in the token stream is always EOL. This
272 # is exploited to avoid catching StopIteration everywhere.
273 tok = next(self.iter)
274 if tok not in expected:
275 self.expected(expected, tok)
276 return tok
278 def look_for_ident(*expected):
279 tok = next(self.iter)
280 if isinstance(tok, str):
281 if not IDENT_RE.match(tok):
282 self.parse_error('variable expansion not allowed')
283 elif tok not in expected:
284 self.expected(expected + (IDENT,), tok)
285 return tok
287 def parse_assignment_rhs(gen, expected, in_path):
288 tokens = []
289 for tok in gen:
290 if not isinstance(tok, str):
291 if tok in expected:
292 break
293 self.expected(expected + (IDENT,), tok)
294 if tok[0] != '$':
295 tokens.append(tok)
296 elif tok == '$ ' or tok == '$$' or tok == '$:':
297 tokens.append(events.dollar_token(tok[1], in_path))
298 else:
299 var = tok[2:-1] if tok[1] == '{' else tok[1:]
300 tokens.append(events.variable_expansion_token(var))
301 else:
302 # gen must have raised StopIteration
303 tok = None
305 if tokens:
306 # Fast path avoiding str.join()
307 value = tokens[0] if len(tokens) == 1 else ''.join(tokens)
308 else:
309 value = None
310 return value, tok
312 def look_for_path(*expected):
313 # paths in build rules are parsed one space-separated token
314 # at a time and expanded
315 token = next(self.iter)
316 if not isinstance(token, str):
317 return None, token
318 # Fast path if there are no dollar and variable expansion
319 if SIMPLE_PATH_RE.match(token):
320 return token, None
321 gen = self._variable_tokens(token)
322 return parse_assignment_rhs(gen, expected, True)
324 def parse_assignment(tok):
325 name = tok
326 assert isinstance(name, str)
327 look_for(EQUALS)
328 value, tok = parse_assignment_rhs(self.iter, (EOL,), False)
329 assert tok == EOL
330 events.variable(name, value)
332 def parse_build():
333 # parse outputs
334 out = []
335 iout = []
336 while True:
337 value, tok = look_for_path(COLON, PIPE)
338 if value is None:
339 break
340 out.append(value)
341 if tok == PIPE:
342 while True:
343 value, tok = look_for_path(COLON)
344 if value is None:
345 break
346 iout.append(value)
348 # parse rule
349 assert tok == COLON
350 rule = look_for_ident()
352 # parse inputs and dependencies
353 in_ = []
354 iin = []
355 orderdep = []
356 while True:
357 value, tok = look_for_path(PIPE, PIPE2, EOL)
358 if value is None:
359 break
360 in_.append(value)
361 if tok == PIPE:
362 while True:
363 value, tok = look_for_path(PIPE2, EOL)
364 if value is None:
365 break
366 iin.append(value)
367 if tok == PIPE2:
368 while True:
369 value, tok = look_for_path(EOL)
370 if value is None:
371 break
372 orderdep.append(value)
373 assert tok == EOL
374 events.begin_build(out, iout, rule, in_, iin, orderdep)
375 nonlocal global_var
376 global_var = False
378 def parse_pool():
379 # pool declarations are ignored. Just gobble all the variables
380 ident = look_for_ident()
381 look_for(EOL)
382 events.begin_pool(ident)
383 nonlocal global_var
384 global_var = False
386 def parse_rule():
387 ident = look_for_ident()
388 look_for(EOL)
389 events.begin_rule(ident)
390 nonlocal global_var
391 global_var = False
393 def parse_default():
394 idents = []
395 while True:
396 ident = look_for_ident(EOL)
397 if ident == EOL:
398 break
399 idents.append(ident)
400 events.default(idents)
402 def parse_declaration(tok):
403 if tok == EOL:
404 return
406 nonlocal global_var
407 if tok == INDENT:
408 if global_var:
409 self.parse_error('indented line outside rule or edge')
410 tok = look_for_ident(EOL)
411 if tok == EOL:
412 return
413 parse_assignment(tok)
414 return
416 if not global_var:
417 events.end_scope()
418 global_var = True
419 if tok == POOL:
420 parse_pool()
421 elif tok == BUILD:
422 parse_build()
423 elif tok == RULE:
424 parse_rule()
425 elif tok == DEFAULT:
426 parse_default()
427 elif isinstance(tok, str):
428 parse_assignment(tok)
429 else:
430 self.expected((POOL, BUILD, RULE, INCLUDE, DEFAULT, IDENT), tok)
432 events.begin_file()
433 while self.iter:
434 try:
435 self.match_keyword = True
436 token = next(self.iter)
437 self.match_keyword = False
438 parse_declaration(token)
439 except StopIteration:
440 self.pop()
441 events.end_file()
444 # ---- variable handling ----
446 def expand(x, rule_vars=None, build_vars=None, global_vars=None):
447 if x is None:
448 return None
449 changed = True
450 have_dollar_replacement = False
451 while changed:
452 changed = False
453 matches = list(VAR_RE.finditer(x))
454 if not matches:
455 break
457 # Reverse the match so that expanding later matches does not
458 # invalidate m.start()/m.end() for earlier ones. Do not reduce $$ to $
459 # until all variables are dealt with.
460 for m in reversed(matches):
461 name = m.group(1)
462 if not name:
463 have_dollar_replacement = True
464 continue
465 changed = True
466 if build_vars and name in build_vars:
467 value = build_vars[name]
468 elif rule_vars and name in rule_vars:
469 value = rule_vars[name]
470 elif name in global_vars:
471 value = global_vars[name]
472 else:
473 value = ''
474 x = x[:m.start()] + value + x[m.end():]
475 return x.replace('$$', '$') if have_dollar_replacement else x
478 class Scope(object):
479 def __init__(self, events):
480 self.events = events
482 def on_left_scope(self):
483 pass
485 def on_variable(self, key, value):
486 pass
489 class BuildScope(Scope):
490 def __init__(self, events, out, iout, rule, in_, iin, orderdep, rule_vars):
491 super().__init__(events)
492 self.rule = rule
493 self.out = [events.expand_and_normalize(x) for x in out]
494 self.in_ = [events.expand_and_normalize(x) for x in in_]
495 self.iin = [events.expand_and_normalize(x) for x in iin]
496 self.orderdep = [events.expand_and_normalize(x) for x in orderdep]
497 self.iout = [events.expand_and_normalize(x) for x in iout]
498 self.rule_vars = rule_vars
499 self.build_vars = dict()
500 self._define_variable('out', ' '.join(self.out))
501 self._define_variable('in', ' '.join(self.in_))
503 def expand(self, x):
504 return self.events.expand(x, self.rule_vars, self.build_vars)
506 def on_left_scope(self):
507 self.events.variable('out', self.build_vars['out'])
508 self.events.variable('in', self.build_vars['in'])
509 self.events.end_build(self, self.out, self.iout, self.rule, self.in_,
510 self.iin, self.orderdep)
512 def _define_variable(self, key, value):
513 # The value has been expanded already, quote it for further
514 # expansion from rule variables
515 value = value.replace('$', '$$')
516 self.build_vars[key] = value
518 def on_variable(self, key, value):
519 # in and out are at the top of the lookup order and cannot
520 # be overridden. Also, unlike what the manual says, build
521 # variables only lookup global variables. They never lookup
522 # rule variables, earlier build variables, or in/out.
523 if key not in ('in', 'in_newline', 'out'):
524 self._define_variable(key, self.events.expand(value))
527 class RuleScope(Scope):
528 def __init__(self, events, name, vars_dict):
529 super().__init__(events)
530 self.name = name
531 self.vars_dict = vars_dict
532 self.generator = False
534 def on_left_scope(self):
535 self.events.end_rule(self, self.name)
537 def on_variable(self, key, value):
538 self.vars_dict[key] = value
539 if key == 'generator':
540 self.generator = True
543 class NinjaParserEventsWithVars(NinjaParserEvents):
544 def __init__(self, parser):
545 super().__init__(parser)
546 self.rule_vars = defaultdict(lambda: dict())
547 self.global_vars = dict()
548 self.scope = None
550 def variable(self, name, value):
551 if self.scope:
552 self.scope.on_variable(name, value)
553 else:
554 self.global_vars[name] = self.expand(value)
556 def begin_build(self, out, iout, rule, in_, iin, orderdep):
557 if rule != 'phony' and rule not in self.rule_vars:
558 self.parser.parse_error("undefined rule '%s'" % rule)
560 self.scope = BuildScope(self, out, iout, rule, in_, iin, orderdep, self.rule_vars[rule])
562 def begin_pool(self, name):
563 # pool declarations are ignored. Just gobble all the variables
564 self.scope = Scope(self)
566 def begin_rule(self, name):
567 if name in self.rule_vars:
568 self.parser.parse_error("duplicate rule '%s'" % name)
569 self.scope = RuleScope(self, name, self.rule_vars[name])
571 def end_scope(self):
572 self.scope.on_left_scope()
573 self.scope = None
575 # utility functions:
577 def expand(self, x, rule_vars=None, build_vars=None):
578 return expand(x, rule_vars, build_vars, self.global_vars)
580 def expand_and_normalize(self, x):
581 return normpath(self.expand(x))
583 # extra events not present in the superclass:
585 def end_build(self, scope, out, iout, rule, in_, iin, orderdep):
586 pass
588 def end_rule(self, scope, name):
589 pass
592 # ---- test client that just prints back whatever it parsed ----
594 class Writer(NinjaParserEvents):
595 ARGS = argparse.ArgumentParser(description='Rewrite input build.ninja to stdout.')
597 def __init__(self, output, parser, args):
598 super().__init__(parser)
599 self.output = output
600 self.indent = ''
601 self.had_vars = False
603 def dollar_token(self, word, in_path=False):
604 return '$' + word
606 def print(self, *args, **kwargs):
607 if len(args):
608 self.output.write(self.indent)
609 print(*args, **kwargs, file=self.output)
611 def variable(self, name, value):
612 self.print('%s = %s' % (name, value))
613 self.had_vars = True
615 def begin_scope(self):
616 self.indent = ' '
617 self.had_vars = False
619 def end_scope(self):
620 if self.had_vars:
621 self.print()
622 self.indent = ''
623 self.had_vars = False
625 def begin_pool(self, name):
626 self.print('pool %s' % name)
627 self.begin_scope()
629 def begin_rule(self, name):
630 self.print('rule %s' % name)
631 self.begin_scope()
633 def begin_build(self, outputs, implicit_outputs, rule, inputs, implicit, order_only):
634 all_outputs = list(outputs)
635 all_inputs = list(inputs)
637 if implicit:
638 all_inputs.append('|')
639 all_inputs.extend(implicit)
640 if order_only:
641 all_inputs.append('||')
642 all_inputs.extend(order_only)
643 if implicit_outputs:
644 all_outputs.append('|')
645 all_outputs.extend(implicit_outputs)
647 self.print('build %s: %s' % (' '.join(all_outputs),
648 ' '.join([rule] + all_inputs)))
649 self.begin_scope()
651 def default(self, targets):
652 self.print('default %s' % ' '.join(targets))
655 # ---- emit compile_commands.json ----
657 class Compdb(NinjaParserEventsWithVars):
658 ARGS = argparse.ArgumentParser(description='Emit compile_commands.json.')
659 ARGS.add_argument('rules', nargs='*',
660 help='The ninja rules to emit compilation commands for.')
662 def __init__(self, output, parser, args):
663 super().__init__(parser)
664 self.output = output
665 self.rules = args.rules
666 self.sep = ''
668 def begin_file(self):
669 self.output.write('[')
670 self.directory = os.getcwd()
672 def print_entry(self, **entry):
673 entry['directory'] = self.directory
674 self.output.write(self.sep + json.dumps(entry))
675 self.sep = ',\n'
677 def begin_build(self, out, iout, rule, in_, iin, orderdep):
678 if in_ and rule in self.rules:
679 super().begin_build(out, iout, rule, in_, iin, orderdep)
680 else:
681 self.scope = Scope(self)
683 def end_build(self, scope, out, iout, rule, in_, iin, orderdep):
684 self.print_entry(command=scope.expand('${command}'), file=in_[0])
686 def end_file(self):
687 self.output.write(']\n')
690 # ---- clean output files ----
692 class Clean(NinjaParserEventsWithVars):
693 ARGS = argparse.ArgumentParser(description='Remove output build files.')
694 ARGS.add_argument('-g', dest='generator', action='store_true',
695 help='clean generated files too')
697 def __init__(self, output, parser, args):
698 super().__init__(parser)
699 self.dry_run = args.dry_run
700 self.verbose = args.verbose or args.dry_run
701 self.generator = args.generator
703 def begin_file(self):
704 print('Cleaning... ', end=(None if self.verbose else ''), flush=True)
705 self.cnt = 0
707 def end_file(self):
708 print('%d files' % self.cnt)
710 def do_clean(self, *files):
711 for f in files:
712 if self.dry_run:
713 if os.path.exists(f):
714 self.cnt += 1
715 print('Would remove ' + f)
716 continue
717 else:
718 try:
719 if os.path.isdir(f):
720 shutil.rmtree(f)
721 else:
722 os.unlink(f)
723 self.cnt += 1
724 if self.verbose:
725 print('Removed ' + f)
726 except FileNotFoundError:
727 pass
729 def end_build(self, scope, out, iout, rule, in_, iin, orderdep):
730 if rule == 'phony':
731 return
732 if self.generator:
733 rspfile = scope.expand('${rspfile}')
734 if rspfile:
735 self.do_clean(rspfile)
736 if self.generator or not scope.expand('${generator}'):
737 self.do_clean(*out, *iout)
738 depfile = scope.expand('${depfile}')
739 if depfile:
740 self.do_clean(depfile)
743 # ---- convert build.ninja to makefile ----
745 class Ninja2Make(NinjaParserEventsWithVars):
746 ARGS = argparse.ArgumentParser(description='Convert build.ninja to a Makefile.')
747 ARGS.add_argument('--clean', dest='emit_clean', action='store_true',
748 help='Emit clean/distclean rules.')
749 ARGS.add_argument('--doublecolon', action='store_true',
750 help='Emit double-colon rules for phony targets.')
751 ARGS.add_argument('--omit', metavar='TARGET', nargs='+',
752 help='Targets to omit.')
754 def __init__(self, output, parser, args):
755 super().__init__(parser)
756 self.output = output
758 self.emit_clean = args.emit_clean
759 self.doublecolon = args.doublecolon
760 self.omit = set(args.omit)
762 if self.emit_clean:
763 self.omit.update(['clean', 'distclean'])
765 # Lists of targets are kept in memory and emitted only at the
766 # end because appending is really inefficient in GNU make.
767 # We only do it when it's O(#rules) or O(#variables), but
768 # never when it could be O(#targets).
769 self.depfiles = list()
770 self.rspfiles = list()
771 self.build_vars = defaultdict(lambda: dict())
772 self.rule_targets = defaultdict(lambda: list())
773 self.stamp_targets = defaultdict(lambda: list())
774 self.all_outs = set()
775 self.all_ins = set()
776 self.all_phony = set()
777 self.seen_default = False
779 def print(self, *args, **kwargs):
780 print(*args, **kwargs, file=self.output)
782 def dollar_token(self, word, in_path=False):
783 if in_path and word == ' ':
784 self.parser.parse_error('Make does not support spaces in filenames')
785 return '$$' if word == '$' else word
787 def print_phony(self, outs, ins):
788 targets = ' '.join(outs).replace('$', '$$')
789 deps = ' '.join(ins).replace('$', '$$')
790 deps = deps.strip()
791 if self.doublecolon:
792 self.print(targets + '::' + (' ' if deps else '') + deps + ';@:')
793 else:
794 self.print(targets + ':' + (' ' if deps else '') + deps)
795 self.all_phony.update(outs)
797 def begin_file(self):
798 self.print(r'# This is an automatically generated file, and it shows.')
799 self.print(r'ninja-default:')
800 self.print(r'.PHONY: ninja-default ninja-clean ninja-distclean')
801 if self.emit_clean:
802 self.print(r'ninja-clean:: ninja-clean-start; $(if $V,,@)rm -f ${ninja-depfiles}')
803 self.print(r'ninja-clean-start:; $(if $V,,@echo Cleaning...)')
804 self.print(r'ninja-distclean:: clean; $(if $V,,@)rm -f ${ninja-rspfiles}')
805 self.print(r'.PHONY: ninja-clean-start')
806 self.print_phony(['clean'], ['ninja-clean'])
807 self.print_phony(['distclean'], ['ninja-distclean'])
808 self.print(r'vpath')
809 self.print(r'NULL :=')
810 self.print(r'SPACE := ${NULL} #')
811 self.print(r'MAKEFLAGS += -rR')
812 self.print(r'define NEWLINE')
813 self.print(r'')
814 self.print(r'endef')
815 self.print(r'.var.in_newline = $(subst $(SPACE),$(NEWLINE),${.var.in})')
816 self.print(r"ninja-command = $(if $V,,$(if ${.var.description},@printf '%s\n' '$(subst ','\'',${.var.description})' && ))${.var.command}")
817 self.print(r"ninja-command-restat = $(if $V,,$(if ${.var.description},@printf '%s\n' '$(subst ','\'',${.var.description})' && ))${.var.command} && if test -e $(firstword ${.var.out}); then printf '%s\n' ${.var.out} > $@; fi")
819 def end_file(self):
820 def natural_sort_key(s, _nsre=re.compile('([0-9]+)')):
821 return [int(text) if text.isdigit() else text.lower()
822 for text in _nsre.split(s)]
824 self.print()
825 self.print('ninja-outputdirs :=')
826 for rule in self.rule_vars:
827 if rule == 'phony':
828 continue
829 self.print('ninja-targets-%s := %s' % (rule, ' '.join(self.rule_targets[rule])))
830 self.print('ninja-stamp-%s := %s' % (rule, ' '.join(self.stamp_targets[rule])))
831 self.print('ninja-outputdirs += $(sort $(dir ${ninja-targets-%s}))' % rule)
832 self.print()
833 self.print('dummy := $(shell mkdir -p . $(sort $(ninja-outputdirs)))')
834 self.print('ninja-depfiles :=' + ' '.join(self.depfiles))
835 self.print('ninja-rspfiles :=' + ' '.join(self.rspfiles))
836 self.print('-include ${ninja-depfiles}')
837 self.print()
838 for targets in self.build_vars:
839 for name, value in self.build_vars[targets].items():
840 self.print('%s: private .var.%s := %s' %
841 (targets, name, value.replace('$', '$$')))
842 self.print()
843 if not self.seen_default:
844 default_targets = sorted(self.all_outs - self.all_ins, key=natural_sort_key)
845 self.print('ninja-default: ' + ' '.join(default_targets))
847 # This is a hack... Meson declares input meson.build files as
848 # phony, because Ninja does not have an equivalent of Make's
849 # "path/to/file:" declaration that ignores "path/to/file" even
850 # if it is absent. However, Makefile.ninja wants to depend on
851 # build.ninja, which in turn depends on these phony targets which
852 # would cause Makefile.ninja to be rebuilt in a loop.
853 phony_targets = sorted(self.all_phony - self.all_ins, key=natural_sort_key)
854 self.print('.PHONY: ' + ' '.join(phony_targets))
856 def variable(self, name, value):
857 super().variable(name, value)
858 if self.scope is None:
859 self.global_vars[name] = self.expand(value)
860 self.print('.var.%s := %s' % (name, self.global_vars[name]))
862 def begin_build(self, out, iout, rule, in_, iin, orderdep):
863 if any(x in self.omit for x in out):
864 self.scope = Scope(self)
865 return
867 super().begin_build(out, iout, rule, in_, iin, orderdep)
868 self.current_targets = ' '.join(self.scope.out + self.scope.iout).replace('$', '$$')
870 def end_build(self, scope, out, iout, rule, in_, iin, orderdep):
871 self.rule_targets[rule] += self.scope.out
872 self.rule_targets[rule] += self.scope.iout
874 self.all_outs.update(self.scope.iout)
875 self.all_outs.update(self.scope.out)
876 self.all_ins.update(self.scope.in_)
877 self.all_ins.update(self.scope.iin)
879 targets = self.current_targets
880 self.current_targets = None
881 if rule == 'phony':
882 # Phony rules treat order-only dependencies as normal deps
883 self.print_phony(out + iout, in_ + iin + orderdep)
884 return
886 inputs = ' '.join(in_ + iin).replace('$', '$$')
887 orderonly = ' '.join(orderdep).replace('$', '$$')
889 rspfile = scope.expand('${rspfile}')
890 if rspfile:
891 rspfile_content = scope.expand('${rspfile_content}')
892 with open(rspfile, 'w') as f:
893 f.write(rspfile_content)
894 inputs += ' ' + rspfile
895 self.rspfiles.append(rspfile)
897 restat = 'restat' in self.scope.build_vars or 'restat' in self.rule_vars[rule]
898 depfile = scope.expand('${depfile}')
899 build_vars = {
900 'command': scope.expand('${command}'),
901 'description': scope.expand('${description}'),
902 'out': scope.expand('${out}')
905 if restat and not depfile:
906 if len(out) == 1:
907 stamp = out[0] + '.stamp'
908 else:
909 stamp = '%s@%s.stamp' % (rule, sha1_text(targets)[0:11])
910 self.print('%s: %s; @:' % (targets, stamp))
911 self.print('ifneq (%s, $(wildcard %s))' % (targets, targets))
912 self.print('.PHONY: %s' % (stamp, ))
913 self.print('endif')
914 self.print('%s: %s | %s; ${ninja-command-restat}' % (stamp, inputs, orderonly))
915 self.rule_targets[rule].append(stamp)
916 self.stamp_targets[rule].append(stamp)
917 self.build_vars[stamp] = build_vars
918 else:
919 self.print('%s: %s | %s; ${ninja-command}' % (targets, inputs, orderonly))
920 self.build_vars[targets] = build_vars
921 if depfile:
922 self.depfiles.append(depfile)
924 def end_rule(self, scope, name):
925 # Note that the generator pseudo-variable could also be attached
926 # to a build block rather than a rule. This is not handled here
927 # in order to reduce the number of "rm" invocations. However,
928 # "ninjatool.py -t clean" does that correctly.
929 target = 'distclean' if scope.generator else 'clean'
930 self.print('ninja-%s:: ; $(if $V,,@)rm -f ${ninja-stamp-%s}' % (target, name))
931 if self.emit_clean:
932 self.print('ninja-%s:: ; $(if $V,,@)rm -rf ${ninja-targets-%s}' % (target, name))
934 def default(self, targets):
935 self.print("ninja-default: " + ' '.join(targets))
936 self.seen_default = True
939 # ---- command line parsing ----
941 # we cannot use subparsers because tools are chosen through the "-t"
942 # option.
944 class ToolAction(argparse.Action):
945 def __init__(self, option_strings, dest, choices, metavar='TOOL', nargs=None, **kwargs):
946 if nargs is not None:
947 raise ValueError("nargs not allowed")
948 super().__init__(option_strings, dest, required=True, choices=choices,
949 metavar=metavar, **kwargs)
951 def __call__(self, parser, namespace, value, option_string):
952 tool = self.choices[value]
953 setattr(namespace, self.dest, tool)
954 tool.ARGS.prog = '%s %s %s' % (parser.prog, option_string, value)
957 class ToolHelpAction(argparse.Action):
958 def __init__(self, option_strings, dest, nargs=None, **kwargs):
959 if nargs is not None:
960 raise ValueError("nargs not allowed")
961 super().__init__(option_strings, dest, nargs=0, **kwargs)
963 def __call__(self, parser, namespace, values, option_string=None):
964 if namespace.tool:
965 namespace.tool.ARGS.print_help()
966 else:
967 parser.print_help()
968 parser.exit()
971 tools = {
972 'test': Writer,
973 'ninja2make': Ninja2Make,
974 'compdb': Compdb,
975 'clean': Clean,
978 parser = argparse.ArgumentParser(description='Process and transform build.ninja files.',
979 add_help=False)
980 parser.add_argument('-C', metavar='DIR', dest='dir', default='.',
981 help='change to DIR before doing anything else')
982 parser.add_argument('-f', metavar='FILE', dest='file', default='build.ninja',
983 help='specify input build file [default=build.ninja]')
984 parser.add_argument('-n', dest='dry_run', action='store_true',
985 help='do not actually do anything')
986 parser.add_argument('-v', dest='verbose', action='store_true',
987 help='be more verbose')
989 parser.add_argument('-t', dest='tool', choices=tools, action=ToolAction,
990 help='choose the tool to run')
991 parser.add_argument('-h', '--help', action=ToolHelpAction,
992 help='show this help message and exit')
994 if len(sys.argv) >= 2 and sys.argv[1] == '--version':
995 print('1.8')
996 sys.exit(0)
998 args, tool_args = parser.parse_known_args()
999 args.tool.ARGS.parse_args(tool_args, args)
1001 os.chdir(args.dir)
1002 with open(args.file, 'r') as f:
1003 parser = NinjaParser(args.file, f)
1004 try:
1005 events = args.tool(sys.stdout, parser, args)
1006 except InvalidArgumentError as e:
1007 parser.error(str(e))
1008 parser.parse(events)