blob: cc77d51aa84ace3d121b4e18569de2ffc8173d50 [file] [log] [blame]
#! /bin/sh
# Python module for parsing and processing .ninja files.
#
# Author: Paolo Bonzini
#
# Copyright (C) 2019 Red Hat, Inc.
# We don't want to put "#! @PYTHON@" as the shebang and
# make the file executable, so instead we make this a
# Python/shell polyglot. The first line below starts a
# multiline string literal for Python, while it is just
# ":" for bash. The closing of the multiline string literal
# is never parsed by bash since it exits before.
'''':
case "$0" in
/*) me=$0 ;;
*) me=$(command -v "$0") ;;
esac
python="@PYTHON@"
case $python in
@*) python=python3 ;;
esac
exec $python "$me" "$@"
exit 1
'''
from collections import namedtuple, defaultdict
import sys
import os
import re
import json
import argparse
import shutil
class InvalidArgumentError(Exception):
pass
# faster version of os.path.normpath: do nothing unless there is a double
# slash or a "." or ".." component. The filter does not have to be super
# precise, but it has to be fast. os.path.normpath is the hottest function
# for ninja2make without this optimization!
if os.path.sep == '/':
def normpath(path, _slow_re=re.compile('/[./]')):
return os.path.normpath(path) if _slow_re.search(path) or path[0] == '.' else path
else:
normpath = os.path.normpath
# ---- lexer and parser ----
PATH_RE = r"[^$\s:|]+|\$[$ :]|\$[a-zA-Z0-9_-]+|\$\{[a-zA-Z0-9_.-]+\}"
SIMPLE_PATH_RE = re.compile(r"[^$\s:|]+")
IDENT_RE = re.compile(r"[a-zA-Z0-9_.-]+$")
STRING_RE = re.compile(r"(" + PATH_RE + r"|[\s:|])(?:\r?\n)?|.")
TOPLEVEL_RE = re.compile(r"([=:#]|\|\|?|^ +|(?:" + PATH_RE + r")+)\s*|.")
VAR_RE=re.compile(r'\$\$|\$\{([^}]*)\}')
BUILD = 1
POOL = 2
RULE = 3
DEFAULT = 4
EQUALS = 5
COLON = 6
PIPE = 7
PIPE2 = 8
IDENT = 9
INCLUDE = 10
INDENT = 11
EOL = 12
class LexerError(Exception):
pass
class ParseError(Exception):
pass
class NinjaParserEvents(object):
def __init__(self, parser):
self.parser = parser
def dollar_token(self, word, in_path=False):
return '$$' if word == '$' else word
def variable_expansion_token(self, varname):
return '${%s}' % varname
def variable(self, name, arg):
pass
def begin_file(self):
pass
def end_file(self):
pass
def end_scope(self):
pass
def begin_pool(self, name):
pass
def begin_rule(self, name):
pass
def begin_build(self, out, iout, rule, in_, iin, orderdep):
pass
def default(self, targets):
pass
class NinjaParser(object):
InputFile = namedtuple('InputFile', 'filename iter lineno')
def __init__(self, filename, input):
self.stack = []
self.top = None
self.iter = None
self.lineno = None
self.match_keyword = False
self.push(filename, input)
def file_changed(self):
self.iter = self.top.iter
self.lineno = self.top.lineno
if self.top.filename is not None:
os.chdir(os.path.dirname(self.top.filename) or '.')
def push(self, filename, input):
if self.top:
self.top.lineno = self.lineno
self.top.iter = self.iter
self.stack.append(self.top)
self.top = self.InputFile(filename=filename or 'stdin',
iter=self._tokens(input), lineno=0)
self.file_changed()
def pop(self):
if len(self.stack):
self.top = self.stack[-1]
self.stack.pop()
self.file_changed()
else:
self.top = self.iter = None
def next_line(self, input):
line = next(input).rstrip()
self.lineno += 1
while len(line) and line[-1] == '$':
line = line[0:-1] + next(input).strip()
self.lineno += 1
return line
def print_token(self, tok):
if tok == EOL:
return "end of line"
if tok == BUILD:
return '"build"'
if tok == POOL:
return '"pool"'
if tok == RULE:
return '"rule"'
if tok == DEFAULT:
return '"default"'
if tok == EQUALS:
return '"="'
if tok == COLON:
return '":"'
if tok == PIPE:
return '"|"'
if tok == PIPE2:
return '"||"'
if tok == INCLUDE:
return '"include"'
if tok == IDENT:
return 'identifier'
return '"%s"' % tok
def error(self, msg):
raise LexerError("%s:%d: %s" % (self.stack[-1].filename, self.lineno, msg))
def parse_error(self, msg):
raise ParseError("%s:%d: %s" % (self.stack[-1].filename, self.lineno, msg))
def expected(self, expected, tok):
msg = "found %s, expected " % (self.print_token(tok), )
for i, exp_tok in enumerate(expected):
if i > 0:
msg = msg + (' or ' if i == len(expected) - 1 else ', ')
msg = msg + self.print_token(exp_tok)
self.parse_error(msg)
def _variable_tokens(self, value):
for m in STRING_RE.finditer(value):
match = m.group(1)
if not match:
self.error("unexpected '%s'" % (m.group(0), ))
yield match
def _tokens(self, input):
while True:
try:
line = self.next_line(input)
except StopIteration:
return
for m in TOPLEVEL_RE.finditer(line):
match = m.group(1)
if not match:
self.error("unexpected '%s'" % (m.group(0), ))
if match == ':':
yield COLON
continue
if match == '|':
yield PIPE
continue
if match == '||':
yield PIPE2
continue
if match[0] == ' ':
yield INDENT
continue
if match[0] == '=':
yield EQUALS
value = line[m.start() + 1:].lstrip()
yield from self._variable_tokens(value)
break
if match[0] == '#':
break
# identifier
if self.match_keyword:
if match == 'build':
yield BUILD
continue
if match == 'pool':
yield POOL
continue
if match == 'rule':
yield RULE
continue
if match == 'default':
yield DEFAULT
continue
if match == 'include':
filename = line[m.start() + 8:].strip()
self.push(filename, open(filename, 'r'))
break
if match == 'subninja':
self.error('subninja is not supported')
yield match
yield EOL
def parse(self, events):
global_var = True
def look_for(*expected):
# The last token in the token stream is always EOL. This
# is exploited to avoid catching StopIteration everywhere.
tok = next(self.iter)
if tok not in expected:
self.expected(expected, tok)
return tok
def look_for_ident(*expected):
tok = next(self.iter)
if isinstance(tok, str):
if not IDENT_RE.match(tok):
self.parse_error('variable expansion not allowed')
elif tok not in expected:
self.expected(expected + (IDENT,), tok)
return tok
def parse_assignment_rhs(gen, expected, in_path):
tokens = []
for tok in gen:
if not isinstance(tok, str):
if tok in expected:
break
self.expected(expected + (IDENT,), tok)
if tok[0] != '$':
tokens.append(tok)
elif tok == '$ ' or tok == '$$' or tok == '$:':
tokens.append(events.dollar_token(tok[1], in_path))
else:
var = tok[2:-1] if tok[1] == '{' else tok[1:]
tokens.append(events.variable_expansion_token(var))
else:
# gen must have raised StopIteration
tok = None
if tokens:
# Fast path avoiding str.join()
value = tokens[0] if len(tokens) == 1 else ''.join(tokens)
else:
value = None
return value, tok
def look_for_path(*expected):
# paths in build rules are parsed one space-separated token
# at a time and expanded
token = next(self.iter)
if not isinstance(token, str):
return None, token
# Fast path if there are no dollar and variable expansion
if SIMPLE_PATH_RE.match(token):
return token, None
gen = self._variable_tokens(token)
return parse_assignment_rhs(gen, expected, True)
def parse_assignment(tok):
name = tok
assert isinstance(name, str)
look_for(EQUALS)
value, tok = parse_assignment_rhs(self.iter, (EOL,), False)
assert tok == EOL
events.variable(name, value)
def parse_build():
# parse outputs
out = []
iout = []
while True:
value, tok = look_for_path(COLON, PIPE)
if value is None:
break
out.append(value)
if tok == PIPE:
while True:
value, tok = look_for_path(COLON)
if value is None:
break
iout.append(value)
# parse rule
assert tok == COLON
rule = look_for_ident()
# parse inputs and dependencies
in_ = []
iin = []
orderdep = []
while True:
value, tok = look_for_path(PIPE, PIPE2, EOL)
if value is None:
break
in_.append(value)
if tok == PIPE:
while True:
value, tok = look_for_path(PIPE2, EOL)
if value is None:
break
iin.append(value)
if tok == PIPE2:
while True:
value, tok = look_for_path(EOL)
if value is None:
break
orderdep.append(value)
assert tok == EOL
events.begin_build(out, iout, rule, in_, iin, orderdep)
nonlocal global_var
global_var = False
def parse_pool():
# pool declarations are ignored. Just gobble all the variables
ident = look_for_ident()
look_for(EOL)
events.begin_pool(ident)
nonlocal global_var
global_var = False
def parse_rule():
ident = look_for_ident()
look_for(EOL)
events.begin_rule(ident)
nonlocal global_var
global_var = False
def parse_default():
idents = []
while True:
ident = look_for_ident(EOL)
if ident == EOL:
break
idents.append(ident)
events.default(idents)
def parse_declaration(tok):
if tok == EOL:
return
nonlocal global_var
if tok == INDENT:
if global_var:
self.parse_error('indented line outside rule or edge')
tok = look_for_ident(EOL)
if tok == EOL:
return
parse_assignment(tok)
return
if not global_var:
events.end_scope()
global_var = True
if tok == POOL:
parse_pool()
elif tok == BUILD:
parse_build()
elif tok == RULE:
parse_rule()
elif tok == DEFAULT:
parse_default()
elif isinstance(tok, str):
parse_assignment(tok)
else:
self.expected((POOL, BUILD, RULE, INCLUDE, DEFAULT, IDENT), tok)
events.begin_file()
while self.iter:
try:
self.match_keyword = True
token = next(self.iter)
self.match_keyword = False
parse_declaration(token)
except StopIteration:
self.pop()
events.end_file()
# ---- variable handling ----
def expand(x, rule_vars=None, build_vars=None, global_vars=None):
if x is None:
return None
changed = True
have_dollar_replacement = False
while changed:
changed = False
matches = list(VAR_RE.finditer(x))
if not matches:
break
# Reverse the match so that expanding later matches does not
# invalidate m.start()/m.end() for earlier ones. Do not reduce $$ to $
# until all variables are dealt with.
for m in reversed(matches):
name = m.group(1)
if not name:
have_dollar_replacement = True
continue
changed = True
if build_vars and name in build_vars:
value = build_vars[name]
elif rule_vars and name in rule_vars:
value = rule_vars[name]
elif name in global_vars:
value = global_vars[name]
else:
value = ''
x = x[:m.start()] + value + x[m.end():]
return x.replace('$$', '$') if have_dollar_replacement else x
class Scope(object):
def __init__(self, events):
self.events = events
def on_left_scope(self):
pass
def on_variable(self, key, value):
pass
class BuildScope(Scope):
def __init__(self, events, out, iout, rule, in_, iin, orderdep, rule_vars):
super().__init__(events)
self.rule = rule
self.out = [events.expand_and_normalize(x) for x in out]
self.in_ = [events.expand_and_normalize(x) for x in in_]
self.iin = [events.expand_and_normalize(x) for x in iin]
self.orderdep = [events.expand_and_normalize(x) for x in orderdep]
self.iout = [events.expand_and_normalize(x) for x in iout]
self.rule_vars = rule_vars
self.build_vars = dict()
self._define_variable('out', ' '.join(self.out))
self._define_variable('in', ' '.join(self.in_))
def expand(self, x):
return self.events.expand(x, self.rule_vars, self.build_vars)
def on_left_scope(self):
self.events.variable('out', self.build_vars['out'])
self.events.variable('in', self.build_vars['in'])
self.events.end_build(self, self.out, self.iout, self.rule, self.in_,
self.iin, self.orderdep)
def _define_variable(self, key, value):
# The value has been expanded already, quote it for further
# expansion from rule variables
value = value.replace('$', '$$')
self.build_vars[key] = value
def on_variable(self, key, value):
# in and out are at the top of the lookup order and cannot
# be overridden. Also, unlike what the manual says, build
# variables only lookup global variables. They never lookup
# rule variables, earlier build variables, or in/out.
if key not in ('in', 'in_newline', 'out'):
self._define_variable(key, self.events.expand(value))
class RuleScope(Scope):
def __init__(self, events, name, vars_dict):
super().__init__(events)
self.name = name
self.vars_dict = vars_dict
self.generator = False
def on_left_scope(self):
self.events.end_rule(self, self.name)
def on_variable(self, key, value):
self.vars_dict[key] = value
if key == 'generator':
self.generator = True
class NinjaParserEventsWithVars(NinjaParserEvents):
def __init__(self, parser):
super().__init__(parser)
self.rule_vars = defaultdict(lambda: dict())
self.global_vars = dict()
self.scope = None
def variable(self, name, value):
if self.scope:
self.scope.on_variable(name, value)
else:
self.global_vars[name] = self.expand(value)
def begin_build(self, out, iout, rule, in_, iin, orderdep):
if rule != 'phony' and rule not in self.rule_vars:
self.parser.parse_error("undefined rule '%s'" % rule)
self.scope = BuildScope(self, out, iout, rule, in_, iin, orderdep, self.rule_vars[rule])
def begin_pool(self, name):
# pool declarations are ignored. Just gobble all the variables
self.scope = Scope(self)
def begin_rule(self, name):
if name in self.rule_vars:
self.parser.parse_error("duplicate rule '%s'" % name)
self.scope = RuleScope(self, name, self.rule_vars[name])
def end_scope(self):
self.scope.on_left_scope()
self.scope = None
# utility functions:
def expand(self, x, rule_vars=None, build_vars=None):
return expand(x, rule_vars, build_vars, self.global_vars)
def expand_and_normalize(self, x):
return normpath(self.expand(x))
# extra events not present in the superclass:
def end_build(self, scope, out, iout, rule, in_, iin, orderdep):
pass
def end_rule(self, scope, name):
pass
# ---- test client that just prints back whatever it parsed ----
class Writer(NinjaParserEvents):
ARGS = argparse.ArgumentParser(description='Rewrite input build.ninja to stdout.')
def __init__(self, output, parser, args):
super().__init__(parser)
self.output = output
self.indent = ''
self.had_vars = False
def dollar_token(self, word, in_path=False):
return '$' + word
def print(self, *args, **kwargs):
if len(args):
self.output.write(self.indent)
print(*args, **kwargs, file=self.output)
def variable(self, name, value):
self.print('%s = %s' % (name, value))
self.had_vars = True
def begin_scope(self):
self.indent = ' '
self.had_vars = False
def end_scope(self):
if self.had_vars:
self.print()
self.indent = ''
self.had_vars = False
def begin_pool(self, name):
self.print('pool %s' % name)
self.begin_scope()
def begin_rule(self, name):
self.print('rule %s' % name)
self.begin_scope()
def begin_build(self, outputs, implicit_outputs, rule, inputs, implicit, order_only):
all_outputs = list(outputs)
all_inputs = list(inputs)
if implicit:
all_inputs.append('|')
all_inputs.extend(implicit)
if order_only:
all_inputs.append('||')
all_inputs.extend(order_only)
if implicit_outputs:
all_outputs.append('|')
all_outputs.extend(implicit_outputs)
self.print('build %s: %s' % (' '.join(all_outputs),
' '.join([rule] + all_inputs)))
self.begin_scope()
def default(self, targets):
self.print('default %s' % ' '.join(targets))
# ---- emit compile_commands.json ----
class Compdb(NinjaParserEventsWithVars):
ARGS = argparse.ArgumentParser(description='Emit compile_commands.json.')
ARGS.add_argument('rules', nargs='*',
help='The ninja rules to emit compilation commands for.')
def __init__(self, output, parser, args):
super().__init__(parser)
self.output = output
self.rules = args.rules
self.sep = ''
def begin_file(self):
self.output.write('[')
self.directory = os.getcwd()
def print_entry(self, **entry):
entry['directory'] = self.directory
self.output.write(self.sep + json.dumps(entry))
self.sep = ',\n'
def begin_build(self, out, iout, rule, in_, iin, orderdep):
if in_ and rule in self.rules:
super().begin_build(out, iout, rule, in_, iin, orderdep)
else:
self.scope = Scope(self)
def end_build(self, scope, out, iout, rule, in_, iin, orderdep):
self.print_entry(command=scope.expand('${command}'), file=in_[0])
def end_file(self):
self.output.write(']\n')
# ---- clean output files ----
class Clean(NinjaParserEventsWithVars):
ARGS = argparse.ArgumentParser(description='Remove output build files.')
ARGS.add_argument('-g', dest='generator', action='store_true',
help='clean generated files too')
def __init__(self, output, parser, args):
super().__init__(parser)
self.dry_run = args.dry_run
self.verbose = args.verbose or args.dry_run
self.generator = args.generator
def begin_file(self):
print('Cleaning... ', end=(None if self.verbose else ''), flush=True)
self.cnt = 0
def end_file(self):
print('%d files' % self.cnt)
def do_clean(self, *files):
for f in files:
if self.dry_run:
if os.path.exists(f):
self.cnt += 1
print('Would remove ' + f)
continue
else:
try:
if os.path.isdir(f):
shutil.rmtree(f)
else:
os.unlink(f)
self.cnt += 1
if self.verbose:
print('Removed ' + f)
except FileNotFoundError:
pass
def end_build(self, scope, out, iout, rule, in_, iin, orderdep):
if rule == 'phony':
return
if self.generator:
rspfile = scope.expand('${rspfile}')
if rspfile:
self.do_clean(rspfile)
if self.generator or not scope.expand('${generator}'):
self.do_clean(*out, *iout)
depfile = scope.expand('${depfile}')
if depfile:
self.do_clean(depfile)
# ---- convert build.ninja to makefile ----
class Ninja2Make(NinjaParserEventsWithVars):
ARGS = argparse.ArgumentParser(description='Convert build.ninja to a Makefile.')
ARGS.add_argument('--clean', dest='emit_clean', action='store_true',
help='Emit clean/distclean rules.')
ARGS.add_argument('--doublecolon', action='store_true',
help='Emit double-colon rules for phony targets.')
ARGS.add_argument('--omit', metavar='TARGET', nargs='+',
help='Targets to omit.')
def __init__(self, output, parser, args):
super().__init__(parser)
self.output = output
self.emit_clean = args.emit_clean
self.doublecolon = args.doublecolon
self.omit = set(args.omit)
if self.emit_clean:
self.omit.update(['clean', 'distclean'])
# Lists of targets are kept in memory and emitted only at the
# end because appending is really inefficient in GNU make.
# We only do it when it's O(#rules) or O(#variables), but
# never when it could be O(#targets).
self.depfiles = list()
self.rspfiles = list()
self.build_vars = defaultdict(lambda: dict())
self.rule_targets = defaultdict(lambda: list())
self.stamp_targets = defaultdict(lambda: list())
self.num_stamp = defaultdict(lambda: 0)
self.all_outs = set()
self.all_ins = set()
self.all_phony = set()
self.seen_default = False
def print(self, *args, **kwargs):
print(*args, **kwargs, file=self.output)
def dollar_token(self, word, in_path=False):
if in_path and word == ' ':
self.parser.parse_error('Make does not support spaces in filenames')
return '$$' if word == '$' else word
def print_phony(self, outs, ins):
targets = ' '.join(outs).replace('$', '$$')
deps = ' '.join(ins).replace('$', '$$')
deps = deps.strip()
if self.doublecolon:
self.print(targets + '::' + (' ' if deps else '') + deps + ';@:')
else:
self.print(targets + ':' + (' ' if deps else '') + deps)
self.all_phony.update(outs)
def begin_file(self):
self.print(r'# This is an automatically generated file, and it shows.')
self.print(r'ninja-default:')
self.print(r'.PHONY: ninja-default ninja-clean ninja-distclean')
if self.emit_clean:
self.print(r'ninja-clean:: ninja-clean-start; $(if $V,,@)rm -f ${ninja-depfiles}')
self.print(r'ninja-clean-start:; $(if $V,,@echo Cleaning...)')
self.print(r'ninja-distclean:: clean; $(if $V,,@)rm -f ${ninja-rspfiles}')
self.print(r'.PHONY: ninja-clean-start')
self.print_phony(['clean'], ['ninja-clean'])
self.print_phony(['distclean'], ['ninja-distclean'])
self.print(r'vpath')
self.print(r'NULL :=')
self.print(r'SPACE := ${NULL} #')
self.print(r'MAKEFLAGS += -rR')
self.print(r'define NEWLINE')
self.print(r'')
self.print(r'endef')
self.print(r'.var.in_newline = $(subst $(SPACE),$(NEWLINE),${.var.in})')
self.print(r"ninja-command = $(if $V,,$(if ${.var.description},@printf '%s\n' '$(subst ','\'',${.var.description})' && ))${.var.command}")
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")
def end_file(self):
def natural_sort_key(s, _nsre=re.compile('([0-9]+)')):
return [int(text) if text.isdigit() else text.lower()
for text in _nsre.split(s)]
self.print()
self.print('ninja-outputdirs :=')
for rule in self.rule_vars:
if rule == 'phony':
continue
self.print('ninja-targets-%s := %s' % (rule, ' '.join(self.rule_targets[rule])))
self.print('ninja-stamp-%s := %s' % (rule, ' '.join(self.stamp_targets[rule])))
self.print('ninja-outputdirs += $(sort $(dir ${ninja-targets-%s}))' % rule)
self.print()
self.print('dummy := $(shell mkdir -p . $(sort $(ninja-outputdirs)))')
self.print('ninja-depfiles :=' + ' '.join(self.depfiles))
self.print('ninja-rspfiles :=' + ' '.join(self.rspfiles))
self.print('-include ${ninja-depfiles}')
self.print()
for targets in self.build_vars:
for name, value in self.build_vars[targets].items():
self.print('%s: private .var.%s := %s' % (targets, name, value))
self.print()
if not self.seen_default:
default_targets = sorted(self.all_outs - self.all_ins, key=natural_sort_key)
self.print('ninja-default: ' + ' '.join(default_targets))
# This is a hack... Meson declares input meson.build files as
# phony, because Ninja does not have an equivalent of Make's
# "path/to/file:" declaration that ignores "path/to/file" even
# if it is absent. However, Makefile.ninja wants to depend on
# build.ninja, which in turn depends on these phony targets which
# would cause Makefile.ninja to be rebuilt in a loop.
phony_targets = sorted(self.all_phony - self.all_ins, key=natural_sort_key)
self.print('.PHONY: ' + ' '.join(phony_targets))
def variable(self, name, value):
super().variable(name, value)
if self.scope is None:
self.global_vars[name] = self.expand(value)
self.print('.var.%s := %s' % (name, self.global_vars[name]))
def begin_build(self, out, iout, rule, in_, iin, orderdep):
if any(x in self.omit for x in out):
self.scope = Scope(self)
return
super().begin_build(out, iout, rule, in_, iin, orderdep)
self.current_targets = ' '.join(self.scope.out + self.scope.iout).replace('$', '$$')
def end_build(self, scope, out, iout, rule, in_, iin, orderdep):
self.rule_targets[rule] += self.scope.out
self.rule_targets[rule] += self.scope.iout
self.all_outs.update(self.scope.iout)
self.all_outs.update(self.scope.out)
self.all_ins.update(self.scope.in_)
self.all_ins.update(self.scope.iin)
targets = self.current_targets
self.current_targets = None
if rule == 'phony':
# Phony rules treat order-only dependencies as normal deps
self.print_phony(out + iout, in_ + iin + orderdep)
return
inputs = ' '.join(in_ + iin).replace('$', '$$')
orderonly = ' '.join(orderdep).replace('$', '$$')
rspfile = scope.expand('${rspfile}')
if rspfile:
rspfile_content = scope.expand('${rspfile_content}')
with open(rspfile, 'w') as f:
f.write(rspfile_content)
inputs += ' ' + rspfile
self.rspfiles.append(rspfile)
restat = 'restat' in self.scope.build_vars or 'restat' in self.rule_vars[rule]
depfile = scope.expand('${depfile}')
build_vars = {
'command': scope.expand('${command}'),
'description': scope.expand('${description}'),
'out': scope.expand('${out}')
}
if restat and not depfile:
if len(out) == 1:
stamp = out[0] + '.stamp'
else:
stamp = '%s%d.stamp' %(rule, self.num_stamp[rule])
self.num_stamp[rule] += 1
self.print('%s: %s; @:' % (targets, stamp))
self.print('%s: %s | %s; ${ninja-command-restat}' % (stamp, inputs, orderonly))
self.rule_targets[rule].append(stamp)
self.stamp_targets[rule].append(stamp)
self.build_vars[stamp] = build_vars
else:
self.print('%s: %s | %s; ${ninja-command}' % (targets, inputs, orderonly))
self.build_vars[targets] = build_vars
if depfile:
self.depfiles.append(depfile)
def end_rule(self, scope, name):
# Note that the generator pseudo-variable could also be attached
# to a build block rather than a rule. This is not handled here
# in order to reduce the number of "rm" invocations. However,
# "ninjatool.py -t clean" does that correctly.
target = 'distclean' if scope.generator else 'clean'
self.print('ninja-%s:: ; $(if $V,,@)rm -f ${ninja-stamp-%s}' % (target, name))
if self.emit_clean:
self.print('ninja-%s:: ; $(if $V,,@)rm -rf ${ninja-targets-%s}' % (target, name))
def default(self, targets):
self.print("ninja-default: " + ' '.join(targets))
self.seen_default = True
# ---- command line parsing ----
# we cannot use subparsers because tools are chosen through the "-t"
# option.
class ToolAction(argparse.Action):
def __init__(self, option_strings, dest, choices, metavar='TOOL', nargs=None, **kwargs):
if nargs is not None:
raise ValueError("nargs not allowed")
super().__init__(option_strings, dest, required=True, choices=choices,
metavar=metavar, **kwargs)
def __call__(self, parser, namespace, value, option_string):
tool = self.choices[value]
setattr(namespace, self.dest, tool)
tool.ARGS.prog = '%s %s %s' % (parser.prog, option_string, value)
class ToolHelpAction(argparse.Action):
def __init__(self, option_strings, dest, nargs=None, **kwargs):
if nargs is not None:
raise ValueError("nargs not allowed")
super().__init__(option_strings, dest, nargs=0, **kwargs)
def __call__(self, parser, namespace, values, option_string=None):
if namespace.tool:
namespace.tool.ARGS.print_help()
else:
parser.print_help()
parser.exit()
tools = {
'test': Writer,
'ninja2make': Ninja2Make,
'compdb': Compdb,
'clean': Clean,
}
parser = argparse.ArgumentParser(description='Process and transform build.ninja files.',
add_help=False)
parser.add_argument('-C', metavar='DIR', dest='dir', default='.',
help='change to DIR before doing anything else')
parser.add_argument('-f', metavar='FILE', dest='file', default='build.ninja',
help='specify input build file [default=build.ninja]')
parser.add_argument('-n', dest='dry_run', action='store_true',
help='do not actually do anything')
parser.add_argument('-v', dest='verbose', action='store_true',
help='be more verbose')
parser.add_argument('-t', dest='tool', choices=tools, action=ToolAction,
help='choose the tool to run')
parser.add_argument('-h', '--help', action=ToolHelpAction,
help='show this help message and exit')
if len(sys.argv) >= 2 and sys.argv[1] == '--version':
print('1.8')
sys.exit(0)
args, tool_args = parser.parse_known_args()
args.tool.ARGS.parse_args(tool_args, args)
os.chdir(args.dir)
with open(args.file, 'r') as f:
parser = NinjaParser(args.file, f)
try:
events = args.tool(sys.stdout, parser, args)
except InvalidArgumentError as e:
parser.error(str(e))
parser.parse(events)