]> git.proxmox.com Git - mirror_qemu.git/blob - scripts/ninjatool.py
Merge remote-tracking branch 'remotes/stefanha/tags/block-pull-request' into staging
[mirror_qemu.git] / scripts / ninjatool.py
1 #! /bin/sh
2
3 # Python module for parsing and processing .ninja files.
4 #
5 # Author: Paolo Bonzini
6 #
7 # Copyright (C) 2019 Red Hat, Inc.
8
9
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.
16
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 '''
29
30
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
39
40
41 class InvalidArgumentError(Exception):
42 pass
43
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
53
54
55 def sha1_text(text):
56 return hashlib.sha1(text.encode()).hexdigest()
57
58 # ---- lexer and parser ----
59
60 PATH_RE = r"[^$\s:|]+|\$[$ :]|\$[a-zA-Z0-9_-]+|\$\{[a-zA-Z0-9_.-]+\}"
61
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'\$\$|\$\{([^}]*)\}')
67
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
80
81
82 class LexerError(Exception):
83 pass
84
85
86 class ParseError(Exception):
87 pass
88
89
90 class NinjaParserEvents(object):
91 def __init__(self, parser):
92 self.parser = parser
93
94 def dollar_token(self, word, in_path=False):
95 return '$$' if word == '$' else word
96
97 def variable_expansion_token(self, varname):
98 return '${%s}' % varname
99
100 def variable(self, name, arg):
101 pass
102
103 def begin_file(self):
104 pass
105
106 def end_file(self):
107 pass
108
109 def end_scope(self):
110 pass
111
112 def begin_pool(self, name):
113 pass
114
115 def begin_rule(self, name):
116 pass
117
118 def begin_build(self, out, iout, rule, in_, iin, orderdep):
119 pass
120
121 def default(self, targets):
122 pass
123
124
125 class NinjaParser(object):
126
127 InputFile = namedtuple('InputFile', 'filename iter lineno')
128
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)
136
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 '.')
142
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()
151
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
159
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
167
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
192
193 def error(self, msg):
194 raise LexerError("%s:%d: %s" % (self.stack[-1].filename, self.lineno, msg))
195
196 def parse_error(self, msg):
197 raise ParseError("%s:%d: %s" % (self.stack[-1].filename, self.lineno, msg))
198
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)
206
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
213
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
243
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
266
267 def parse(self, events):
268 global_var = True
269
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
277
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
286
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
304
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
311
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)
323
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)
331
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)
347
348 # parse rule
349 assert tok == COLON
350 rule = look_for_ident()
351
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
377
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
385
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
392
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)
401
402 def parse_declaration(tok):
403 if tok == EOL:
404 return
405
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
415
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)
431
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()
442
443
444 # ---- variable handling ----
445
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
456
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
476
477
478 class Scope(object):
479 def __init__(self, events):
480 self.events = events
481
482 def on_left_scope(self):
483 pass
484
485 def on_variable(self, key, value):
486 pass
487
488
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_))
502
503 def expand(self, x):
504 return self.events.expand(x, self.rule_vars, self.build_vars)
505
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)
511
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
517
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))
525
526
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
533
534 def on_left_scope(self):
535 self.events.end_rule(self, self.name)
536
537 def on_variable(self, key, value):
538 self.vars_dict[key] = value
539 if key == 'generator':
540 self.generator = True
541
542
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
549
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)
555
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)
559
560 self.scope = BuildScope(self, out, iout, rule, in_, iin, orderdep, self.rule_vars[rule])
561
562 def begin_pool(self, name):
563 # pool declarations are ignored. Just gobble all the variables
564 self.scope = Scope(self)
565
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])
570
571 def end_scope(self):
572 self.scope.on_left_scope()
573 self.scope = None
574
575 # utility functions:
576
577 def expand(self, x, rule_vars=None, build_vars=None):
578 return expand(x, rule_vars, build_vars, self.global_vars)
579
580 def expand_and_normalize(self, x):
581 return normpath(self.expand(x))
582
583 # extra events not present in the superclass:
584
585 def end_build(self, scope, out, iout, rule, in_, iin, orderdep):
586 pass
587
588 def end_rule(self, scope, name):
589 pass
590
591
592 # ---- test client that just prints back whatever it parsed ----
593
594 class Writer(NinjaParserEvents):
595 ARGS = argparse.ArgumentParser(description='Rewrite input build.ninja to stdout.')
596
597 def __init__(self, output, parser, args):
598 super().__init__(parser)
599 self.output = output
600 self.indent = ''
601 self.had_vars = False
602
603 def dollar_token(self, word, in_path=False):
604 return '$' + word
605
606 def print(self, *args, **kwargs):
607 if len(args):
608 self.output.write(self.indent)
609 print(*args, **kwargs, file=self.output)
610
611 def variable(self, name, value):
612 self.print('%s = %s' % (name, value))
613 self.had_vars = True
614
615 def begin_scope(self):
616 self.indent = ' '
617 self.had_vars = False
618
619 def end_scope(self):
620 if self.had_vars:
621 self.print()
622 self.indent = ''
623 self.had_vars = False
624
625 def begin_pool(self, name):
626 self.print('pool %s' % name)
627 self.begin_scope()
628
629 def begin_rule(self, name):
630 self.print('rule %s' % name)
631 self.begin_scope()
632
633 def begin_build(self, outputs, implicit_outputs, rule, inputs, implicit, order_only):
634 all_outputs = list(outputs)
635 all_inputs = list(inputs)
636
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)
646
647 self.print('build %s: %s' % (' '.join(all_outputs),
648 ' '.join([rule] + all_inputs)))
649 self.begin_scope()
650
651 def default(self, targets):
652 self.print('default %s' % ' '.join(targets))
653
654
655 # ---- emit compile_commands.json ----
656
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.')
661
662 def __init__(self, output, parser, args):
663 super().__init__(parser)
664 self.output = output
665 self.rules = args.rules
666 self.sep = ''
667
668 def begin_file(self):
669 self.output.write('[')
670 self.directory = os.getcwd()
671
672 def print_entry(self, **entry):
673 entry['directory'] = self.directory
674 self.output.write(self.sep + json.dumps(entry))
675 self.sep = ',\n'
676
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)
682
683 def end_build(self, scope, out, iout, rule, in_, iin, orderdep):
684 self.print_entry(command=scope.expand('${command}'), file=in_[0])
685
686 def end_file(self):
687 self.output.write(']\n')
688
689
690 # ---- clean output files ----
691
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')
696
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
702
703 def begin_file(self):
704 print('Cleaning... ', end=(None if self.verbose else ''), flush=True)
705 self.cnt = 0
706
707 def end_file(self):
708 print('%d files' % self.cnt)
709
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
728
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)
741
742
743 # ---- convert build.ninja to makefile ----
744
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.')
753
754 def __init__(self, output, parser, args):
755 super().__init__(parser)
756 self.output = output
757
758 self.emit_clean = args.emit_clean
759 self.doublecolon = args.doublecolon
760 self.omit = set(args.omit)
761
762 if self.emit_clean:
763 self.omit.update(['clean', 'distclean'])
764
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
778
779 def print(self, *args, **kwargs):
780 print(*args, **kwargs, file=self.output)
781
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
786
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)
796
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")
818
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)]
823
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))
846
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))
855
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]))
861
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
866
867 super().begin_build(out, iout, rule, in_, iin, orderdep)
868 self.current_targets = ' '.join(self.scope.out + self.scope.iout).replace('$', '$$')
869
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
873
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)
878
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
885
886 inputs = ' '.join(in_ + iin).replace('$', '$$')
887 orderonly = ' '.join(orderdep).replace('$', '$$')
888
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)
896
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}')
903 }
904
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('%s: %s | %s; ${ninja-command-restat}' % (stamp, inputs, orderonly))
912 self.rule_targets[rule].append(stamp)
913 self.stamp_targets[rule].append(stamp)
914 self.build_vars[stamp] = build_vars
915 else:
916 self.print('%s: %s | %s; ${ninja-command}' % (targets, inputs, orderonly))
917 self.build_vars[targets] = build_vars
918 if depfile:
919 self.depfiles.append(depfile)
920
921 def end_rule(self, scope, name):
922 # Note that the generator pseudo-variable could also be attached
923 # to a build block rather than a rule. This is not handled here
924 # in order to reduce the number of "rm" invocations. However,
925 # "ninjatool.py -t clean" does that correctly.
926 target = 'distclean' if scope.generator else 'clean'
927 self.print('ninja-%s:: ; $(if $V,,@)rm -f ${ninja-stamp-%s}' % (target, name))
928 if self.emit_clean:
929 self.print('ninja-%s:: ; $(if $V,,@)rm -rf ${ninja-targets-%s}' % (target, name))
930
931 def default(self, targets):
932 self.print("ninja-default: " + ' '.join(targets))
933 self.seen_default = True
934
935
936 # ---- command line parsing ----
937
938 # we cannot use subparsers because tools are chosen through the "-t"
939 # option.
940
941 class ToolAction(argparse.Action):
942 def __init__(self, option_strings, dest, choices, metavar='TOOL', nargs=None, **kwargs):
943 if nargs is not None:
944 raise ValueError("nargs not allowed")
945 super().__init__(option_strings, dest, required=True, choices=choices,
946 metavar=metavar, **kwargs)
947
948 def __call__(self, parser, namespace, value, option_string):
949 tool = self.choices[value]
950 setattr(namespace, self.dest, tool)
951 tool.ARGS.prog = '%s %s %s' % (parser.prog, option_string, value)
952
953
954 class ToolHelpAction(argparse.Action):
955 def __init__(self, option_strings, dest, nargs=None, **kwargs):
956 if nargs is not None:
957 raise ValueError("nargs not allowed")
958 super().__init__(option_strings, dest, nargs=0, **kwargs)
959
960 def __call__(self, parser, namespace, values, option_string=None):
961 if namespace.tool:
962 namespace.tool.ARGS.print_help()
963 else:
964 parser.print_help()
965 parser.exit()
966
967
968 tools = {
969 'test': Writer,
970 'ninja2make': Ninja2Make,
971 'compdb': Compdb,
972 'clean': Clean,
973 }
974
975 parser = argparse.ArgumentParser(description='Process and transform build.ninja files.',
976 add_help=False)
977 parser.add_argument('-C', metavar='DIR', dest='dir', default='.',
978 help='change to DIR before doing anything else')
979 parser.add_argument('-f', metavar='FILE', dest='file', default='build.ninja',
980 help='specify input build file [default=build.ninja]')
981 parser.add_argument('-n', dest='dry_run', action='store_true',
982 help='do not actually do anything')
983 parser.add_argument('-v', dest='verbose', action='store_true',
984 help='be more verbose')
985
986 parser.add_argument('-t', dest='tool', choices=tools, action=ToolAction,
987 help='choose the tool to run')
988 parser.add_argument('-h', '--help', action=ToolHelpAction,
989 help='show this help message and exit')
990
991 if len(sys.argv) >= 2 and sys.argv[1] == '--version':
992 print('1.8')
993 sys.exit(0)
994
995 args, tool_args = parser.parse_known_args()
996 args.tool.ARGS.parse_args(tool_args, args)
997
998 os.chdir(args.dir)
999 with open(args.file, 'r') as f:
1000 parser = NinjaParser(args.file, f)
1001 try:
1002 events = args.tool(sys.stdout, parser, args)
1003 except InvalidArgumentError as e:
1004 parser.error(str(e))
1005 parser.parse(events)