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
40 class InvalidArgumentError(Exception):
43 # faster version of os.path.normpath: do nothing unless there is a double
44 # slash or a "." or ".." component. The filter does not have to be super
45 # precise, but it has to be fast. os.path.normpath is the hottest function
46 # for ninja2make without this optimization!
47 if os.path.sep == '/':
48 def normpath(path, _slow_re=re.compile('/[.
/]')):
49 return os.path.normpath(path) if _slow_re.search(path) or path[0] == '.
' else path
51 normpath = os.path.normpath
54 # ---- lexer and parser ----
56 PATH_RE = r"[^$\s:|]+|\$[$ :]|\$[a-zA-Z0-9_-]+|\$\{[a-zA-Z0-9_.-]+\}"
58 SIMPLE_PATH_RE = re.compile(r"[^$\s:|]+")
59 IDENT_RE = re.compile(r"[a-zA-Z0-9_.-]+$")
60 STRING_RE = re.compile(r"(" + PATH_RE + r"|[\s:|])(?:\r?\n)?|.")
61 TOPLEVEL_RE = re.compile(r"([=:#]|\|\|?|^ +|(?:" + PATH_RE + r")+)\s*|.")
62 VAR_RE=re.compile(r'\$\$|\$\
{([^
}]*)\
}')
78 class LexerError(Exception):
82 class ParseError(Exception):
86 class NinjaParserEvents(object):
87 def __init__(self, parser):
90 def dollar_token(self, word, in_path=False):
91 return '$$
' if word == '$
' else word
93 def variable_expansion_token(self, varname):
94 return '${%s}' % varname
96 def variable(self, name, arg):
108 def begin_pool(self, name):
111 def begin_rule(self, name):
114 def begin_build(self, out, iout, rule, in_, iin, orderdep):
117 def default(self, targets):
121 class NinjaParser(object):
123 InputFile = namedtuple('InputFile
', 'filename iter lineno
')
125 def __init__(self, filename, input):
130 self.match_keyword = False
131 self.push(filename, input)
133 def file_changed(self):
134 self.iter = self.top.iter
135 self.lineno = self.top.lineno
136 if self.top.filename is not None:
137 os.chdir(os.path.dirname(self.top.filename) or '.
')
139 def push(self, filename, input):
141 self.top.lineno = self.lineno
142 self.top.iter = self.iter
143 self.stack.append(self.top)
144 self.top = self.InputFile(filename=filename or 'stdin
',
145 iter=self._tokens(input), lineno=0)
150 self.top = self.stack[-1]
154 self.top = self.iter = None
156 def next_line(self, input):
157 line = next(input).rstrip()
159 while len(line) and line[-1] == '$
':
160 line = line[0:-1] + next(input).strip()
164 def print_token(self, tok):
189 def error(self, msg):
190 raise LexerError("%s:%d: %s" % (self.stack[-1].filename, self.lineno, msg))
192 def parse_error(self, msg):
193 raise ParseError("%s:%d: %s" % (self.stack[-1].filename, self.lineno, msg))
195 def expected(self, expected, tok):
196 msg = "found %s, expected " % (self.print_token(tok), )
197 for i, exp_tok in enumerate(expected):
199 msg = msg + (' or
' if i == len(expected) - 1 else ', ')
200 msg = msg + self.print_token(exp_tok)
201 self.parse_error(msg)
203 def _variable_tokens(self, value):
204 for m in STRING_RE.finditer(value):
207 self.error("unexpected '%s
'" % (m.group(0), ))
210 def _tokens(self, input):
213 line = self.next_line(input)
214 except StopIteration:
216 for m in TOPLEVEL_RE.finditer(line):
219 self.error("unexpected '%s
'" % (m.group(0), ))
234 value = line[m.start() + 1:].lstrip()
235 yield from self._variable_tokens(value)
241 if self.match_keyword
:
251 if match
== 'default':
254 if match
== 'include':
255 filename
= line
[m.start
() + 8:].strip
()
256 self.push
(filename
, open
(filename
, 'r'))
258 if match
== 'subninja':
259 self.error
('subninja is not supported')
263 def parse
(self
, events
):
266 def look_for
(*expected
):
267 # The last token in the token stream is always EOL. This
268 # is exploited to avoid catching StopIteration everywhere.
269 tok
= next
(self.iter
)
270 if tok not
in expected
:
271 self.expected
(expected
, tok
)
274 def look_for_ident
(*expected
):
275 tok
= next
(self.iter
)
276 if isinstance
(tok
, str
):
277 if not IDENT_RE.match
(tok
):
278 self.parse_error
('variable expansion not allowed')
279 elif tok not
in expected
:
280 self.expected
(expected
+ (IDENT
,), tok
)
283 def parse_assignment_rhs
(gen
, expected
, in_path
):
286 if not isinstance
(tok
, str
):
289 self.expected
(expected
+ (IDENT
,), tok
)
292 elif tok
== '$ ' or tok
== '$$' or tok
== '$:':
293 tokens.append
(events.dollar_token
(tok
[1], in_path
))
295 var
= tok
[2:-1] if tok
[1] == '{' else tok
[1:]
296 tokens.append
(events.variable_expansion_token
(var
))
298 # gen must have raised StopIteration
302 # Fast path avoiding str.join()
303 value
= tokens
[0] if len
(tokens
) == 1 else ''.
join(tokens
)
308 def look_for_path
(*expected
):
309 # paths in build rules are parsed one space-separated token
310 # at a time and expanded
311 token
= next
(self.iter
)
312 if not isinstance
(token
, str
):
314 # Fast path if there are no dollar and variable expansion
315 if SIMPLE_PATH_RE.match
(token
):
317 gen
= self._variable_tokens
(token
)
318 return parse_assignment_rhs
(gen
, expected
, True
)
320 def parse_assignment
(tok
):
322 assert isinstance
(name
, str
)
324 value
, tok
= parse_assignment_rhs
(self.iter
, (EOL
,), False
)
326 events.variable
(name
, value
)
333 value
, tok
= look_for_path
(COLON
, PIPE
)
339 value
, tok
= look_for_path
(COLON
)
346 rule
= look_for_ident
()
348 # parse inputs and dependencies
353 value
, tok
= look_for_path
(PIPE
, PIPE2
, EOL
)
359 value
, tok
= look_for_path
(PIPE2
, EOL
)
365 value
, tok
= look_for_path
(EOL
)
368 orderdep.append
(value
)
370 events.begin_build
(out
, iout
, rule
, in_
, iin
, orderdep
)
375 # pool declarations are ignored. Just gobble all the variables
376 ident
= look_for_ident
()
378 events.begin_pool
(ident
)
383 ident
= look_for_ident
()
385 events.begin_rule
(ident
)
392 ident
= look_for_ident
(EOL
)
396 events.default
(idents
)
398 def parse_declaration
(tok
):
405 self.parse_error
('indented line outside rule or edge')
406 tok
= look_for_ident
(EOL
)
409 parse_assignment
(tok
)
423 elif isinstance
(tok
, str
):
424 parse_assignment
(tok
)
426 self.expected
((POOL
, BUILD
, RULE
, INCLUDE
, DEFAULT
, IDENT
), tok
)
431 self.match_keyword
= True
432 token
= next
(self.iter
)
433 self.match_keyword
= False
434 parse_declaration
(token
)
435 except StopIteration
:
440 # ---- variable handling ----
442 def
expand(x
, rule_vars
=None
, build_vars
=None
, global_vars
=None
):
446 have_dollar_replacement
= False
449 matches
= list
(VAR_RE.finditer
(x
))
453 # Reverse the match so that expanding later matches does not
454 # invalidate m.start()/m.end() for earlier ones. Do not reduce $$ to $
455 # until all variables are dealt with.
456 for m
in reversed
(matches
):
459 have_dollar_replacement
= True
462 if build_vars and name
in build_vars
:
463 value
= build_vars
[name
]
464 elif rule_vars and name
in rule_vars
:
465 value
= rule_vars
[name
]
466 elif name
in global_vars
:
467 value
= global_vars
[name
]
470 x
= x
[:m.start
()] + value
+ x
[m.end
():]
471 return x.replace
('$$', '$') if have_dollar_replacement
else x
475 def __init__
(self
, events
):
478 def on_left_scope
(self
):
481 def on_variable
(self
, key
, value
):
485 class BuildScope
(Scope
):
486 def __init__
(self
, events
, out
, iout
, rule
, in_
, iin
, orderdep
, rule_vars
):
487 super
().__init__
(events
)
489 self.out
= [events.expand_and_normalize
(x
) for x
in out
]
490 self.in_
= [events.expand_and_normalize
(x
) for x
in in_
]
491 self.iin
= [events.expand_and_normalize
(x
) for x
in iin
]
492 self.orderdep
= [events.expand_and_normalize
(x
) for x
in orderdep
]
493 self.iout
= [events.expand_and_normalize
(x
) for x
in iout
]
494 self.rule_vars
= rule_vars
495 self.build_vars
= dict
()
496 self._define_variable
('out', ' '.
join(self.out
))
497 self._define_variable
('in', ' '.
join(self.in_
))
500 return self.events.
expand(x
, self.rule_vars
, self.build_vars
)
502 def on_left_scope
(self
):
503 self.events.variable
('out', self.build_vars
['out'])
504 self.events.variable
('in', self.build_vars
['in'])
505 self.events.end_build
(self
, self.out
, self.iout
, self.rule
, self.in_
,
506 self.iin
, self.orderdep
)
508 def _define_variable
(self
, key
, value
):
509 # The value has been expanded already, quote it for further
510 # expansion from rule variables
511 value
= value.replace
('$', '$$')
512 self.build_vars
[key
] = value
514 def on_variable
(self
, key
, value
):
515 # in and out are at the top of the lookup order and cannot
516 # be overridden. Also, unlike what the manual says, build
517 # variables only lookup global variables. They never lookup
518 # rule variables, earlier build variables, or in/out.
519 if key not
in ('in', 'in_newline', 'out'):
520 self._define_variable
(key
, self.events.
expand(value
))
523 class RuleScope
(Scope
):
524 def __init__
(self
, events
, name
, vars_dict
):
525 super
().__init__
(events
)
527 self.vars_dict
= vars_dict
528 self.generator
= False
530 def on_left_scope
(self
):
531 self.events.end_rule
(self
, self.name
)
533 def on_variable
(self
, key
, value
):
534 self.vars_dict
[key
] = value
535 if key
== 'generator':
536 self.generator
= True
539 class NinjaParserEventsWithVars
(NinjaParserEvents
):
540 def __init__
(self
, parser
):
541 super
().__init__
(parser
)
542 self.rule_vars
= defaultdict
(lambda
: dict
())
543 self.global_vars
= dict
()
546 def variable
(self
, name
, value
):
548 self.scope.on_variable
(name
, value
)
550 self.global_vars
[name
] = self.
expand(value
)
552 def begin_build
(self
, out
, iout
, rule
, in_
, iin
, orderdep
):
553 if rule
!= 'phony' and rule not
in self.rule_vars
:
554 self.parser.parse_error
("undefined rule '%s'" % rule
)
556 self.scope
= BuildScope
(self
, out
, iout
, rule
, in_
, iin
, orderdep
, self.rule_vars
[rule
])
558 def begin_pool
(self
, name
):
559 # pool declarations are ignored. Just gobble all the variables
560 self.scope
= Scope
(self
)
562 def begin_rule
(self
, name
):
563 if name
in self.rule_vars
:
564 self.parser.parse_error
("duplicate rule '%s'" % name
)
565 self.scope
= RuleScope
(self
, name
, self.rule_vars
[name
])
568 self.scope.on_left_scope
()
573 def
expand(self
, x
, rule_vars
=None
, build_vars
=None
):
574 return expand(x
, rule_vars
, build_vars
, self.global_vars
)
576 def expand_and_normalize
(self
, x
):
577 return normpath
(self.
expand(x
))
579 # extra events not present in the superclass:
581 def end_build
(self
, scope
, out
, iout
, rule
, in_
, iin
, orderdep
):
584 def end_rule
(self
, scope
, name
):
588 # ---- test client that just prints back whatever it parsed ----
590 class Writer
(NinjaParserEvents
):
591 ARGS
= argparse.ArgumentParser
(description
='Rewrite input build.ninja to stdout.')
593 def __init__
(self
, output
, parser
, args
):
594 super
().__init__
(parser
)
597 self.had_vars
= False
599 def dollar_token
(self
, word
, in_path
=False
):
602 def print
(self
, *args
, **kwargs
):
604 self.output.
write(self.indent
)
605 print
(*args
, **kwargs
, file=self.output
)
607 def variable
(self
, name
, value
):
608 self.print
('%s = %s' % (name
, value
))
611 def begin_scope
(self
):
613 self.had_vars
= False
619 self.had_vars
= False
621 def begin_pool
(self
, name
):
622 self.print
('pool %s' % name
)
625 def begin_rule
(self
, name
):
626 self.print
('rule %s' % name
)
629 def begin_build
(self
, outputs
, implicit_outputs
, rule
, inputs
, implicit
, order_only
):
630 all_outputs
= list
(outputs
)
631 all_inputs
= list
(inputs
)
634 all_inputs.append
('|')
635 all_inputs.extend
(implicit
)
637 all_inputs.append
('||')
638 all_inputs.extend
(order_only
)
640 all_outputs.append
('|')
641 all_outputs.extend
(implicit_outputs
)
643 self.print
('build %s: %s' % (' '.
join(all_outputs
),
644 ' '.
join([rule
] + all_inputs
)))
647 def default
(self
, targets
):
648 self.print
('default %s' % ' '.
join(targets
))
651 # ---- emit compile_commands.json ----
653 class Compdb
(NinjaParserEventsWithVars
):
654 ARGS
= argparse.ArgumentParser
(description
='Emit compile_commands.json.')
655 ARGS.add_argument
('rules', nargs
='*',
656 help='The ninja rules to emit compilation commands for.')
658 def __init__
(self
, output
, parser
, args
):
659 super
().__init__
(parser
)
661 self.rules
= args.rules
664 def begin_file
(self
):
665 self.output.
write('[')
666 self.directory
= os.getcwd
()
668 def print_entry
(self
, **entry
):
669 entry
['directory'] = self.directory
670 self.output.
write(self.sep
+ json.dumps
(entry
))
673 def begin_build
(self
, out
, iout
, rule
, in_
, iin
, orderdep
):
674 if in_ and rule
in self.rules
:
675 super
().begin_build
(out
, iout
, rule
, in_
, iin
, orderdep
)
677 self.scope
= Scope
(self
)
679 def end_build
(self
, scope
, out
, iout
, rule
, in_
, iin
, orderdep
):
680 self.print_entry
(command=scope.
expand('${command}'), file=in_
[0])
683 self.output.
write(']\n')
686 # ---- clean output files ----
688 class Clean
(NinjaParserEventsWithVars
):
689 ARGS
= argparse.ArgumentParser
(description
='Remove output build files.')
690 ARGS.add_argument
('-g', dest
='generator', action
='store_true',
691 help='clean generated files too')
693 def __init__
(self
, output
, parser
, args
):
694 super
().__init__
(parser
)
695 self.dry_run
= args.dry_run
696 self.verbose
= args.verbose or args.dry_run
697 self.generator
= args.generator
699 def begin_file
(self
):
700 print
('Cleaning... ', end
=(None
if self.verbose
else ''), flush
=True
)
704 print
('%d files' % self.cnt
)
706 def do_clean
(self
, *files
):
709 if os.path.exists
(f
):
711 print
('Would remove ' + f
)
721 print
('Removed ' + f
)
722 except FileNotFoundError
:
725 def end_build
(self
, scope
, out
, iout
, rule
, in_
, iin
, orderdep
):
729 rspfile
= scope.
expand('${rspfile}')
731 self.do_clean
(rspfile
)
732 if self.generator or not scope.
expand('${generator}'):
733 self.do_clean
(*out
, *iout
)
734 depfile
= scope.
expand('${depfile}')
736 self.do_clean
(depfile
)
739 # ---- convert build.ninja to makefile ----
741 class Ninja2Make
(NinjaParserEventsWithVars
):
742 ARGS
= argparse.ArgumentParser
(description
='Convert build.ninja to a Makefile.')
743 ARGS.add_argument
('--clean', dest
='emit_clean', action
='store_true',
744 help='Emit clean/distclean rules.')
745 ARGS.add_argument
('--doublecolon', action
='store_true',
746 help='Emit double-colon rules for phony targets.')
747 ARGS.add_argument
('--omit', metavar
='TARGET', nargs
='+',
748 help='Targets to omit.')
750 def __init__
(self
, output
, parser
, args
):
751 super
().__init__
(parser
)
754 self.emit_clean
= args.emit_clean
755 self.doublecolon
= args.doublecolon
756 self.omit
= set(args.omit
)
759 self.omit.update
(['clean', 'distclean'])
761 # Lists of targets are kept in memory and emitted only at the
762 # end because appending is really inefficient in GNU make.
763 # We only do it when it's O(#rules) or O(#variables), but
764 # never when it could be O(#targets).
765 self.depfiles
= list
()
766 self.rspfiles
= list
()
767 self.build_vars
= defaultdict
(lambda
: dict
())
768 self.rule_targets
= defaultdict
(lambda
: list
())
769 self.stamp_targets
= defaultdict
(lambda
: list
())
770 self.num_stamp
= defaultdict
(lambda
: 0)
771 self.all_outs
= set()
773 self.all_phony
= set()
774 self.seen_default
= False
776 def print
(self
, *args
, **kwargs
):
777 print
(*args
, **kwargs
, file=self.output
)
779 def dollar_token
(self
, word
, in_path
=False
):
780 if in_path and word
== ' ':
781 self.parser.parse_error
('Make does not support spaces in filenames')
782 return '$$' if word
== '$' else word
784 def print_phony
(self
, outs
, ins
):
785 targets
= ' '.
join(outs
).replace
('$', '$$')
786 deps
= ' '.
join(ins
).replace
('$', '$$')
789 self.print
(targets
+ '::' + (' ' if deps
else '') + deps
+ ';@:')
791 self.print
(targets
+ ':' + (' ' if deps
else '') + deps
)
792 self.all_phony.update
(outs
)
794 def begin_file
(self
):
795 self.print
(r
'# This is an automatically generated file, and it shows.')
796 self.print
(r
'ninja-default:')
797 self.print
(r
'.PHONY: ninja-default ninja-clean ninja-distclean')
799 self.print
(r
'ninja-clean:: ninja-clean-start; $(if $V,,@)rm -f ${ninja-depfiles}')
800 self.print
(r
'ninja-clean-start:; $(if $V,,@echo Cleaning...)')
801 self.print
(r
'ninja-distclean:: clean; $(if $V,,@)rm -f ${ninja-rspfiles}')
802 self.print
(r
'.PHONY: ninja-clean-start')
803 self.print_phony
(['clean'], ['ninja-clean'])
804 self.print_phony
(['distclean'], ['ninja-distclean'])
806 self.print
(r
'NULL :=')
807 self.print
(r
'SPACE := ${NULL} #')
808 self.print
(r
'MAKEFLAGS += -rR')
809 self.print
(r
'define NEWLINE')
812 self.print
(r
'.var.in_newline = $(subst $(SPACE),$(NEWLINE),${.var.in})')
813 self.print
(r
"ninja-command = $(if $V,,$(if ${.var.description},@printf '%s\n' '$(subst ','\'',${.var.description})' && ))${.var.command}")
814 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")
817 def natural_sort_key
(s
, _nsre
=re.compile
('([0-9]+)')):
818 return [int
(text
) if text.isdigit
() else text.lower
()
819 for text
in _nsre.
split(s
)]
822 self.print
('ninja-outputdirs :=')
823 for rule
in self.rule_vars
:
826 self.print
('ninja-targets-%s := %s' % (rule
, ' '.
join(self.rule_targets
[rule
])))
827 self.print
('ninja-stamp-%s := %s' % (rule
, ' '.
join(self.stamp_targets
[rule
])))
828 self.print
('ninja-outputdirs += $(sort $(dir ${ninja-targets-%s}))' % rule
)
830 self.print
('dummy := $(shell mkdir -p . $(sort $(ninja-outputdirs)))')
831 self.print
('ninja-depfiles :=' + ' '.
join(self.depfiles
))
832 self.print
('ninja-rspfiles :=' + ' '.
join(self.rspfiles
))
833 self.print
('-include ${ninja-depfiles}')
835 for targets
in self.build_vars
:
836 for name
, value
in self.build_vars
[targets
].items
():
837 self.print
('%s: private .var.%s := %s' % (targets
, name
, value
))
839 if not self.seen_default
:
840 default_targets
= sorted
(self.all_outs
- self.all_ins
, key
=natural_sort_key
)
841 self.print
('ninja-default: ' + ' '.
join(default_targets
))
843 # This is a hack... Meson declares input meson.build files as
844 # phony, because Ninja does not have an equivalent of Make's
845 # "path/to/file:" declaration that ignores "path/to/file" even
846 # if it is absent. However, Makefile.ninja wants to depend on
847 # build.ninja, which in turn depends on these phony targets which
848 # would cause Makefile.ninja to be rebuilt in a loop.
849 phony_targets
= sorted
(self.all_phony
- self.all_ins
, key
=natural_sort_key
)
850 self.print
('.PHONY: ' + ' '.
join(phony_targets
))
852 def variable
(self
, name
, value
):
853 super
().variable
(name
, value
)
854 if self.scope is None
:
855 self.global_vars
[name
] = self.
expand(value
)
856 self.print
('.var.%s := %s' % (name
, self.global_vars
[name
]))
858 def begin_build
(self
, out
, iout
, rule
, in_
, iin
, orderdep
):
859 if any
(x
in self.omit
for x
in out
):
860 self.scope
= Scope
(self
)
863 super
().begin_build
(out
, iout
, rule
, in_
, iin
, orderdep
)
864 self.current_targets
= ' '.
join(self.scope.out
+ self.scope.iout
).replace
('$', '$$')
866 def end_build
(self
, scope
, out
, iout
, rule
, in_
, iin
, orderdep
):
867 self.rule_targets
[rule
] += self.scope.out
868 self.rule_targets
[rule
] += self.scope.iout
870 self.all_outs.update
(self.scope.iout
)
871 self.all_outs.update
(self.scope.out
)
872 self.all_ins.update
(self.scope.in_
)
873 self.all_ins.update
(self.scope.iin
)
875 targets
= self.current_targets
876 self.current_targets
= None
878 # Phony rules treat order-only dependencies as normal deps
879 self.print_phony
(out
+ iout
, in_
+ iin
+ orderdep
)
882 inputs
= ' '.
join(in_
+ iin
).replace
('$', '$$')
883 orderonly
= ' '.
join(orderdep
).replace
('$', '$$')
885 rspfile
= scope.
expand('${rspfile}')
887 rspfile_content
= scope.
expand('${rspfile_content}')
888 with open
(rspfile
, 'w') as f
:
889 f.
write(rspfile_content
)
890 inputs
+= ' ' + rspfile
891 self.rspfiles.append
(rspfile
)
893 restat
= 'restat' in self.scope.build_vars or
'restat' in self.rule_vars
[rule
]
894 depfile
= scope.
expand('${depfile}')
896 'command': scope.
expand('${command}'),
897 'description': scope.
expand('${description}'),
898 'out': scope.
expand('${out}')
901 if restat and not depfile
:
903 stamp
= out
[0] + '.stamp'
905 stamp
= '%s%d.stamp' %(rule
, self.num_stamp
[rule
])
906 self.num_stamp
[rule
] += 1
907 self.print
('%s: %s; @:' % (targets
, stamp
))
908 self.print
('%s: %s | %s; ${ninja-command-restat}' % (stamp
, inputs
, orderonly
))
909 self.rule_targets
[rule
].append
(stamp
)
910 self.stamp_targets
[rule
].append
(stamp
)
911 self.build_vars
[stamp
] = build_vars
913 self.print
('%s: %s | %s; ${ninja-command}' % (targets
, inputs
, orderonly
))
914 self.build_vars
[targets
] = build_vars
916 self.depfiles.append
(depfile
)
918 def end_rule
(self
, scope
, name
):
919 # Note that the generator pseudo-variable could also be attached
920 # to a build block rather than a rule. This is not handled here
921 # in order to reduce the number of "rm" invocations. However,
922 # "ninjatool.py -t clean" does that correctly.
923 target
= 'distclean' if scope.generator
else 'clean'
924 self.print
('ninja-%s:: ; $(if $V,,@)rm -f ${ninja-stamp-%s}' % (target
, name
))
926 self.print
('ninja-%s:: ; $(if $V,,@)rm -rf ${ninja-targets-%s}' % (target
, name
))
928 def default
(self
, targets
):
929 self.print
("ninja-default: " + ' '.
join(targets
))
930 self.seen_default
= True
933 # ---- command line parsing ----
935 # we cannot use subparsers because tools are chosen through the "-t"
938 class ToolAction
(argparse.Action
):
939 def __init__
(self
, option_strings
, dest
, choices
, metavar
='TOOL', nargs
=None
, **kwargs
):
940 if nargs is not None
:
941 raise ValueError
("nargs not allowed")
942 super
().__init__
(option_strings
, dest
, required
=True
, choices
=choices
,
943 metavar
=metavar
, **kwargs
)
945 def __call__
(self
, parser
, namespace
, value
, option_string
):
946 tool
= self.choices
[value
]
947 setattr
(namespace
, self.dest
, tool
)
948 tool.ARGS.prog
= '%s %s %s' % (parser.prog
, option_string
, value
)
951 class ToolHelpAction
(argparse.Action
):
952 def __init__
(self
, option_strings
, dest
, nargs
=None
, **kwargs
):
953 if nargs is not None
:
954 raise ValueError
("nargs not allowed")
955 super
().__init__
(option_strings
, dest
, nargs
=0, **kwargs
)
957 def __call__
(self
, parser
, namespace
, values
, option_string
=None
):
959 namespace.tool.ARGS.print_help
()
967 'ninja2make': Ninja2Make
,
972 parser
= argparse.ArgumentParser
(description
='Process and transform build.ninja files.',
974 parser.add_argument
('-C', metavar
='DIR', dest
='dir', default
='.',
975 help='change to DIR before doing anything else')
976 parser.add_argument
('-f', metavar
='FILE', dest
='file', default
='build.ninja',
977 help='specify input build file [default=build.ninja]')
978 parser.add_argument
('-n', dest
='dry_run', action
='store_true',
979 help='do not actually do anything')
980 parser.add_argument
('-v', dest
='verbose', action
='store_true',
981 help='be more verbose')
983 parser.add_argument
('-t', dest
='tool', choices
=tools
, action
=ToolAction
,
984 help='choose the tool to run')
985 parser.add_argument
('-h', '--help', action
=ToolHelpAction
,
986 help='show this help message and exit')
988 if len
(sys.argv
) >= 2 and sys.argv
[1] == '--version':
992 args
, tool_args
= parser.parse_known_args
()
993 args.tool.ARGS.parse_args
(tool_args
, args
)
996 with open
(args.
file, 'r') as f
:
997 parser
= NinjaParser
(args.
file, f
)
999 events
= args.tool
(sys.stdout
, parser
, args
)
1000 except InvalidArgumentError as e
:
1001 parser.error
(str
(e
))
1002 parser.parse
(events
)