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
)