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.
20 *) me
=$
(command -v "$0") ;;
26 exec $python "$me" "$@"
31 from collections import namedtuple, defaultdict
41 class InvalidArgumentError(Exception):
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
52 normpath = os.path.normpath
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'\$\$|\$\
{([^
}]*)\
}')
82 class LexerError(Exception):
86 class ParseError(Exception):
90 class NinjaParserEvents(object):
91 def __init__(self, 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):
103 def begin_file(self):
112 def begin_pool(self, name):
115 def begin_rule(self, name):
118 def begin_build(self, out, iout, rule, in_, iin, orderdep):
121 def default(self, targets):
125 class NinjaParser(object):
127 InputFile = namedtuple('InputFile
', 'filename iter lineno
')
129 def __init__(self, filename, input):
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):
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)
154 self.top = self.stack[-1]
158 self.top = self.iter = None
160 def next_line(self, input):
161 line = next(input).rstrip()
163 while len(line) and line[-1] == '$
':
164 line = line[0:-1] + next(input).strip()
168 def print_token(self, 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):
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):
211 self.error("unexpected '%s
'" % (m.group(0), ))
214 def _tokens(self, input):
217 line = self.next_line(input)
218 except StopIteration:
220 for m in TOPLEVEL_RE.finditer(line):
223 self.error("unexpected '%s
'" % (m.group(0), ))
238 value = line[m.start() + 1:].lstrip()
239 yield from self._variable_tokens(value)
245 if self.match_keyword
:
255 if match
== 'default':
258 if match
== 'include':
259 filename
= line
[m.start
() + 8:].strip
()
260 self.push
(filename
, open
(filename
, 'r'))
262 if match
== 'subninja':
263 self.error
('subninja is not supported')
267 def parse
(self
, events
):
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
)
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
)
287 def parse_assignment_rhs
(gen
, expected
, in_path
):
290 if not isinstance
(tok
, str
):
293 self.expected
(expected
+ (IDENT
,), tok
)
296 elif tok
== '$ ' or tok
== '$$' or tok
== '$:':
297 tokens.append
(events.dollar_token
(tok
[1], in_path
))
299 var
= tok
[2:-1] if tok
[1] == '{' else tok
[1:]
300 tokens.append
(events.variable_expansion_token
(var
))
302 # gen must have raised StopIteration
306 # Fast path avoiding str.join()
307 value
= tokens
[0] if len
(tokens
) == 1 else ''.
join(tokens
)
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
):
318 # Fast path if there are no dollar and variable expansion
319 if SIMPLE_PATH_RE.match
(token
):
321 gen
= self._variable_tokens
(token
)
322 return parse_assignment_rhs
(gen
, expected
, True
)
324 def parse_assignment
(tok
):
326 assert isinstance
(name
, str
)
328 value
, tok
= parse_assignment_rhs
(self.iter
, (EOL
,), False
)
330 events.variable
(name
, value
)
337 value
, tok
= look_for_path
(COLON
, PIPE
)
343 value
, tok
= look_for_path
(COLON
)
350 rule
= look_for_ident
()
352 # parse inputs and dependencies
357 value
, tok
= look_for_path
(PIPE
, PIPE2
, EOL
)
363 value
, tok
= look_for_path
(PIPE2
, EOL
)
369 value
, tok
= look_for_path
(EOL
)
372 orderdep.append
(value
)
374 events.begin_build
(out
, iout
, rule
, in_
, iin
, orderdep
)
379 # pool declarations are ignored. Just gobble all the variables
380 ident
= look_for_ident
()
382 events.begin_pool
(ident
)
387 ident
= look_for_ident
()
389 events.begin_rule
(ident
)
396 ident
= look_for_ident
(EOL
)
400 events.default
(idents
)
402 def parse_declaration
(tok
):
409 self.parse_error
('indented line outside rule or edge')
410 tok
= look_for_ident
(EOL
)
413 parse_assignment
(tok
)
427 elif isinstance
(tok
, str
):
428 parse_assignment
(tok
)
430 self.expected
((POOL
, BUILD
, RULE
, INCLUDE
, DEFAULT
, IDENT
), tok
)
435 self.match_keyword
= True
436 token
= next
(self.iter
)
437 self.match_keyword
= False
438 parse_declaration
(token
)
439 except StopIteration
:
444 # ---- variable handling ----
446 def
expand(x
, rule_vars
=None
, build_vars
=None
, global_vars
=None
):
450 have_dollar_replacement
= False
453 matches
= list
(VAR_RE.finditer
(x
))
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
):
463 have_dollar_replacement
= 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
]
474 x
= x
[:m.start
()] + value
+ x
[m.end
():]
475 return x.replace
('$$', '$') if have_dollar_replacement
else x
479 def __init__
(self
, events
):
482 def on_left_scope
(self
):
485 def on_variable
(self
, key
, value
):
489 class BuildScope
(Scope
):
490 def __init__
(self
, events
, out
, iout
, rule
, in_
, iin
, orderdep
, rule_vars
):
491 super
().__init__
(events
)
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_
))
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
)
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
()
550 def variable
(self
, name
, value
):
552 self.scope.on_variable
(name
, value
)
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
])
572 self.scope.on_left_scope
()
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
):
588 def end_rule
(self
, scope
, name
):
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
)
601 self.had_vars
= False
603 def dollar_token
(self
, word
, in_path
=False
):
606 def print
(self
, *args
, **kwargs
):
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
))
615 def begin_scope
(self
):
617 self.had_vars
= False
623 self.had_vars
= False
625 def begin_pool
(self
, name
):
626 self.print
('pool %s' % name
)
629 def begin_rule
(self
, name
):
630 self.print
('rule %s' % name
)
633 def begin_build
(self
, outputs
, implicit_outputs
, rule
, inputs
, implicit
, order_only
):
634 all_outputs
= list
(outputs
)
635 all_inputs
= list
(inputs
)
638 all_inputs.append
('|')
639 all_inputs.extend
(implicit
)
641 all_inputs.append
('||')
642 all_inputs.extend
(order_only
)
644 all_outputs.append
('|')
645 all_outputs.extend
(implicit_outputs
)
647 self.print
('build %s: %s' % (' '.
join(all_outputs
),
648 ' '.
join([rule
] + all_inputs
)))
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
)
665 self.rules
= args.rules
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
))
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
)
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])
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
)
708 print
('%d files' % self.cnt
)
710 def do_clean
(self
, *files
):
713 if os.path.exists
(f
):
715 print
('Would remove ' + f
)
725 print
('Removed ' + f
)
726 except FileNotFoundError
:
729 def end_build
(self
, scope
, out
, iout
, rule
, in_
, iin
, orderdep
):
733 rspfile
= scope.
expand('${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}')
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
)
758 self.emit_clean
= args.emit_clean
759 self.doublecolon
= args.doublecolon
760 self.omit
= set(args.omit
)
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()
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
('$', '$$')
792 self.print
(targets
+ '::' + (' ' if deps
else '') + deps
+ ';@:')
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')
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'])
809 self.print
(r
'NULL :=')
810 self.print
(r
'SPACE := ${NULL} #')
811 self.print
(r
'MAKEFLAGS += -rR')
812 self.print
(r
'define NEWLINE')
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")
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
)]
825 self.print
('ninja-outputdirs :=')
826 for rule
in self.rule_vars
:
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
)
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}')
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
('$', '$$')))
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
)
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
882 # Phony rules treat order-only dependencies as normal deps
883 self.print_phony
(out
+ iout
, in_
+ iin
+ orderdep
)
886 inputs
= ' '.
join(in_
+ iin
).replace
('$', '$$')
887 orderonly
= ' '.
join(orderdep
).replace
('$', '$$')
889 rspfile
= scope.
expand('${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}')
900 'command': scope.
expand('${command}'),
901 'description': scope.
expand('${description}'),
902 'out': scope.
expand('${out}')
905 if restat and not depfile
:
907 stamp
= out
[0] + '.stamp'
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
916 self.print
('%s: %s | %s; ${ninja-command}' % (targets
, inputs
, orderonly
))
917 self.build_vars
[targets
] = build_vars
919 self.depfiles.append
(depfile
)
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
))
929 self.print
('ninja-%s:: ; $(if $V,,@)rm -rf ${ninja-targets-%s}' % (target
, name
))
931 def default
(self
, targets
):
932 self.print
("ninja-default: " + ' '.
join(targets
))
933 self.seen_default
= True
936 # ---- command line parsing ----
938 # we cannot use subparsers because tools are chosen through the "-t"
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
)
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
)
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
)
960 def __call__
(self
, parser
, namespace
, values
, option_string
=None
):
962 namespace.tool.ARGS.print_help
()
970 'ninja2make': Ninja2Make
,
975 parser
= argparse.ArgumentParser
(description
='Process and transform build.ninja files.',
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')
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')
991 if len
(sys.argv
) >= 2 and sys.argv
[1] == '--version':
995 args
, tool_args
= parser.parse_known_args
()
996 args.tool.ARGS.parse_args
(tool_args
, args
)
999 with open
(args.
file, 'r') as f
:
1000 parser
= NinjaParser
(args.
file, f
)
1002 events
= args.tool
(sys.stdout
, parser
, args
)
1003 except InvalidArgumentError as e
:
1004 parser.error
(str
(e
))
1005 parser.parse
(events
)