2
0

ninjatool.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008
  1. #! /bin/sh
  2. # Python module for parsing and processing .ninja files.
  3. #
  4. # Author: Paolo Bonzini
  5. #
  6. # Copyright (C) 2019 Red Hat, Inc.
  7. # We don't want to put "#! @PYTHON@" as the shebang and
  8. # make the file executable, so instead we make this a
  9. # Python/shell polyglot. The first line below starts a
  10. # multiline string literal for Python, while it is just
  11. # ":" for bash. The closing of the multiline string literal
  12. # is never parsed by bash since it exits before.
  13. '''':
  14. case "$0" in
  15. /*) me=$0 ;;
  16. *) me=$(command -v "$0") ;;
  17. esac
  18. python="@PYTHON@"
  19. case $python in
  20. @*) python=python3 ;;
  21. esac
  22. exec $python "$me" "$@"
  23. exit 1
  24. '''
  25. from collections import namedtuple, defaultdict
  26. import sys
  27. import os
  28. import re
  29. import json
  30. import argparse
  31. import hashlib
  32. import shutil
  33. class InvalidArgumentError(Exception):
  34. pass
  35. # faster version of os.path.normpath: do nothing unless there is a double
  36. # slash or a "." or ".." component. The filter does not have to be super
  37. # precise, but it has to be fast. os.path.normpath is the hottest function
  38. # for ninja2make without this optimization!
  39. if os.path.sep == '/':
  40. def normpath(path, _slow_re=re.compile('/[./]')):
  41. return os.path.normpath(path) if _slow_re.search(path) or path[0] == '.' else path
  42. else:
  43. normpath = os.path.normpath
  44. def sha1_text(text):
  45. return hashlib.sha1(text.encode()).hexdigest()
  46. # ---- lexer and parser ----
  47. PATH_RE = r"[^$\s:|]+|\$[$ :]|\$[a-zA-Z0-9_-]+|\$\{[a-zA-Z0-9_.-]+\}"
  48. SIMPLE_PATH_RE = re.compile(r"^[^$\s:|]+$")
  49. IDENT_RE = re.compile(r"[a-zA-Z0-9_.-]+$")
  50. STRING_RE = re.compile(r"(" + PATH_RE + r"|[\s:|])(?:\r?\n)?|.")
  51. TOPLEVEL_RE = re.compile(r"([=:#]|\|\|?|^ +|(?:" + PATH_RE + r")+)\s*|.")
  52. VAR_RE=re.compile(r'\$\$|\$\{([^}]*)\}')
  53. BUILD = 1
  54. POOL = 2
  55. RULE = 3
  56. DEFAULT = 4
  57. EQUALS = 5
  58. COLON = 6
  59. PIPE = 7
  60. PIPE2 = 8
  61. IDENT = 9
  62. INCLUDE = 10
  63. INDENT = 11
  64. EOL = 12
  65. class LexerError(Exception):
  66. pass
  67. class ParseError(Exception):
  68. pass
  69. class NinjaParserEvents(object):
  70. def __init__(self, parser):
  71. self.parser = parser
  72. def dollar_token(self, word, in_path=False):
  73. return '$$' if word == '$' else word
  74. def variable_expansion_token(self, varname):
  75. return '${%s}' % varname
  76. def variable(self, name, arg):
  77. pass
  78. def begin_file(self):
  79. pass
  80. def end_file(self):
  81. pass
  82. def end_scope(self):
  83. pass
  84. def begin_pool(self, name):
  85. pass
  86. def begin_rule(self, name):
  87. pass
  88. def begin_build(self, out, iout, rule, in_, iin, orderdep):
  89. pass
  90. def default(self, targets):
  91. pass
  92. class NinjaParser(object):
  93. InputFile = namedtuple('InputFile', 'filename iter lineno')
  94. def __init__(self, filename, input):
  95. self.stack = []
  96. self.top = None
  97. self.iter = None
  98. self.lineno = None
  99. self.match_keyword = False
  100. self.push(filename, input)
  101. def file_changed(self):
  102. self.iter = self.top.iter
  103. self.lineno = self.top.lineno
  104. if self.top.filename is not None:
  105. os.chdir(os.path.dirname(self.top.filename) or '.')
  106. def push(self, filename, input):
  107. if self.top:
  108. self.top.lineno = self.lineno
  109. self.top.iter = self.iter
  110. self.stack.append(self.top)
  111. self.top = self.InputFile(filename=filename or 'stdin',
  112. iter=self._tokens(input), lineno=0)
  113. self.file_changed()
  114. def pop(self):
  115. if len(self.stack):
  116. self.top = self.stack[-1]
  117. self.stack.pop()
  118. self.file_changed()
  119. else:
  120. self.top = self.iter = None
  121. def next_line(self, input):
  122. line = next(input).rstrip()
  123. self.lineno += 1
  124. while len(line) and line[-1] == '$':
  125. line = line[0:-1] + next(input).strip()
  126. self.lineno += 1
  127. return line
  128. def print_token(self, tok):
  129. if tok == EOL:
  130. return "end of line"
  131. if tok == BUILD:
  132. return '"build"'
  133. if tok == POOL:
  134. return '"pool"'
  135. if tok == RULE:
  136. return '"rule"'
  137. if tok == DEFAULT:
  138. return '"default"'
  139. if tok == EQUALS:
  140. return '"="'
  141. if tok == COLON:
  142. return '":"'
  143. if tok == PIPE:
  144. return '"|"'
  145. if tok == PIPE2:
  146. return '"||"'
  147. if tok == INCLUDE:
  148. return '"include"'
  149. if tok == IDENT:
  150. return 'identifier'
  151. return '"%s"' % tok
  152. def error(self, msg):
  153. raise LexerError("%s:%d: %s" % (self.stack[-1].filename, self.lineno, msg))
  154. def parse_error(self, msg):
  155. raise ParseError("%s:%d: %s" % (self.stack[-1].filename, self.lineno, msg))
  156. def expected(self, expected, tok):
  157. msg = "found %s, expected " % (self.print_token(tok), )
  158. for i, exp_tok in enumerate(expected):
  159. if i > 0:
  160. msg = msg + (' or ' if i == len(expected) - 1 else ', ')
  161. msg = msg + self.print_token(exp_tok)
  162. self.parse_error(msg)
  163. def _variable_tokens(self, value):
  164. for m in STRING_RE.finditer(value):
  165. match = m.group(1)
  166. if not match:
  167. self.error("unexpected '%s'" % (m.group(0), ))
  168. yield match
  169. def _tokens(self, input):
  170. while True:
  171. try:
  172. line = self.next_line(input)
  173. except StopIteration:
  174. return
  175. for m in TOPLEVEL_RE.finditer(line):
  176. match = m.group(1)
  177. if not match:
  178. self.error("unexpected '%s'" % (m.group(0), ))
  179. if match == ':':
  180. yield COLON
  181. continue
  182. if match == '|':
  183. yield PIPE
  184. continue
  185. if match == '||':
  186. yield PIPE2
  187. continue
  188. if match[0] == ' ':
  189. yield INDENT
  190. continue
  191. if match[0] == '=':
  192. yield EQUALS
  193. value = line[m.start() + 1:].lstrip()
  194. yield from self._variable_tokens(value)
  195. break
  196. if match[0] == '#':
  197. break
  198. # identifier
  199. if self.match_keyword:
  200. if match == 'build':
  201. yield BUILD
  202. continue
  203. if match == 'pool':
  204. yield POOL
  205. continue
  206. if match == 'rule':
  207. yield RULE
  208. continue
  209. if match == 'default':
  210. yield DEFAULT
  211. continue
  212. if match == 'include':
  213. filename = line[m.start() + 8:].strip()
  214. self.push(filename, open(filename, 'r'))
  215. break
  216. if match == 'subninja':
  217. self.error('subninja is not supported')
  218. yield match
  219. yield EOL
  220. def parse(self, events):
  221. global_var = True
  222. def look_for(*expected):
  223. # The last token in the token stream is always EOL. This
  224. # is exploited to avoid catching StopIteration everywhere.
  225. tok = next(self.iter)
  226. if tok not in expected:
  227. self.expected(expected, tok)
  228. return tok
  229. def look_for_ident(*expected):
  230. tok = next(self.iter)
  231. if isinstance(tok, str):
  232. if not IDENT_RE.match(tok):
  233. self.parse_error('variable expansion not allowed')
  234. elif tok not in expected:
  235. self.expected(expected + (IDENT,), tok)
  236. return tok
  237. def parse_assignment_rhs(gen, expected, in_path):
  238. tokens = []
  239. for tok in gen:
  240. if not isinstance(tok, str):
  241. if tok in expected:
  242. break
  243. self.expected(expected + (IDENT,), tok)
  244. if tok[0] != '$':
  245. tokens.append(tok)
  246. elif tok == '$ ' or tok == '$$' or tok == '$:':
  247. tokens.append(events.dollar_token(tok[1], in_path))
  248. else:
  249. var = tok[2:-1] if tok[1] == '{' else tok[1:]
  250. tokens.append(events.variable_expansion_token(var))
  251. else:
  252. # gen must have raised StopIteration
  253. tok = None
  254. if tokens:
  255. # Fast path avoiding str.join()
  256. value = tokens[0] if len(tokens) == 1 else ''.join(tokens)
  257. else:
  258. value = None
  259. return value, tok
  260. def look_for_path(*expected):
  261. # paths in build rules are parsed one space-separated token
  262. # at a time and expanded
  263. token = next(self.iter)
  264. if not isinstance(token, str):
  265. return None, token
  266. # Fast path if there are no dollar and variable expansion
  267. if SIMPLE_PATH_RE.match(token):
  268. return token, None
  269. gen = self._variable_tokens(token)
  270. return parse_assignment_rhs(gen, expected, True)
  271. def parse_assignment(tok):
  272. name = tok
  273. assert isinstance(name, str)
  274. look_for(EQUALS)
  275. value, tok = parse_assignment_rhs(self.iter, (EOL,), False)
  276. assert tok == EOL
  277. events.variable(name, value)
  278. def parse_build():
  279. # parse outputs
  280. out = []
  281. iout = []
  282. while True:
  283. value, tok = look_for_path(COLON, PIPE)
  284. if value is None:
  285. break
  286. out.append(value)
  287. if tok == PIPE:
  288. while True:
  289. value, tok = look_for_path(COLON)
  290. if value is None:
  291. break
  292. iout.append(value)
  293. # parse rule
  294. assert tok == COLON
  295. rule = look_for_ident()
  296. # parse inputs and dependencies
  297. in_ = []
  298. iin = []
  299. orderdep = []
  300. while True:
  301. value, tok = look_for_path(PIPE, PIPE2, EOL)
  302. if value is None:
  303. break
  304. in_.append(value)
  305. if tok == PIPE:
  306. while True:
  307. value, tok = look_for_path(PIPE2, EOL)
  308. if value is None:
  309. break
  310. iin.append(value)
  311. if tok == PIPE2:
  312. while True:
  313. value, tok = look_for_path(EOL)
  314. if value is None:
  315. break
  316. orderdep.append(value)
  317. assert tok == EOL
  318. events.begin_build(out, iout, rule, in_, iin, orderdep)
  319. nonlocal global_var
  320. global_var = False
  321. def parse_pool():
  322. # pool declarations are ignored. Just gobble all the variables
  323. ident = look_for_ident()
  324. look_for(EOL)
  325. events.begin_pool(ident)
  326. nonlocal global_var
  327. global_var = False
  328. def parse_rule():
  329. ident = look_for_ident()
  330. look_for(EOL)
  331. events.begin_rule(ident)
  332. nonlocal global_var
  333. global_var = False
  334. def parse_default():
  335. idents = []
  336. while True:
  337. ident = look_for_ident(EOL)
  338. if ident == EOL:
  339. break
  340. idents.append(ident)
  341. events.default(idents)
  342. def parse_declaration(tok):
  343. if tok == EOL:
  344. return
  345. nonlocal global_var
  346. if tok == INDENT:
  347. if global_var:
  348. self.parse_error('indented line outside rule or edge')
  349. tok = look_for_ident(EOL)
  350. if tok == EOL:
  351. return
  352. parse_assignment(tok)
  353. return
  354. if not global_var:
  355. events.end_scope()
  356. global_var = True
  357. if tok == POOL:
  358. parse_pool()
  359. elif tok == BUILD:
  360. parse_build()
  361. elif tok == RULE:
  362. parse_rule()
  363. elif tok == DEFAULT:
  364. parse_default()
  365. elif isinstance(tok, str):
  366. parse_assignment(tok)
  367. else:
  368. self.expected((POOL, BUILD, RULE, INCLUDE, DEFAULT, IDENT), tok)
  369. events.begin_file()
  370. while self.iter:
  371. try:
  372. self.match_keyword = True
  373. token = next(self.iter)
  374. self.match_keyword = False
  375. parse_declaration(token)
  376. except StopIteration:
  377. self.pop()
  378. events.end_file()
  379. # ---- variable handling ----
  380. def expand(x, rule_vars=None, build_vars=None, global_vars=None):
  381. if x is None:
  382. return None
  383. changed = True
  384. have_dollar_replacement = False
  385. while changed:
  386. changed = False
  387. matches = list(VAR_RE.finditer(x))
  388. if not matches:
  389. break
  390. # Reverse the match so that expanding later matches does not
  391. # invalidate m.start()/m.end() for earlier ones. Do not reduce $$ to $
  392. # until all variables are dealt with.
  393. for m in reversed(matches):
  394. name = m.group(1)
  395. if not name:
  396. have_dollar_replacement = True
  397. continue
  398. changed = True
  399. if build_vars and name in build_vars:
  400. value = build_vars[name]
  401. elif rule_vars and name in rule_vars:
  402. value = rule_vars[name]
  403. elif name in global_vars:
  404. value = global_vars[name]
  405. else:
  406. value = ''
  407. x = x[:m.start()] + value + x[m.end():]
  408. return x.replace('$$', '$') if have_dollar_replacement else x
  409. class Scope(object):
  410. def __init__(self, events):
  411. self.events = events
  412. def on_left_scope(self):
  413. pass
  414. def on_variable(self, key, value):
  415. pass
  416. class BuildScope(Scope):
  417. def __init__(self, events, out, iout, rule, in_, iin, orderdep, rule_vars):
  418. super().__init__(events)
  419. self.rule = rule
  420. self.out = [events.expand_and_normalize(x) for x in out]
  421. self.in_ = [events.expand_and_normalize(x) for x in in_]
  422. self.iin = [events.expand_and_normalize(x) for x in iin]
  423. self.orderdep = [events.expand_and_normalize(x) for x in orderdep]
  424. self.iout = [events.expand_and_normalize(x) for x in iout]
  425. self.rule_vars = rule_vars
  426. self.build_vars = dict()
  427. self._define_variable('out', ' '.join(self.out))
  428. self._define_variable('in', ' '.join(self.in_))
  429. def expand(self, x):
  430. return self.events.expand(x, self.rule_vars, self.build_vars)
  431. def on_left_scope(self):
  432. self.events.variable('out', self.build_vars['out'])
  433. self.events.variable('in', self.build_vars['in'])
  434. self.events.end_build(self, self.out, self.iout, self.rule, self.in_,
  435. self.iin, self.orderdep)
  436. def _define_variable(self, key, value):
  437. # The value has been expanded already, quote it for further
  438. # expansion from rule variables
  439. value = value.replace('$', '$$')
  440. self.build_vars[key] = value
  441. def on_variable(self, key, value):
  442. # in and out are at the top of the lookup order and cannot
  443. # be overridden. Also, unlike what the manual says, build
  444. # variables only lookup global variables. They never lookup
  445. # rule variables, earlier build variables, or in/out.
  446. if key not in ('in', 'in_newline', 'out'):
  447. self._define_variable(key, self.events.expand(value))
  448. class RuleScope(Scope):
  449. def __init__(self, events, name, vars_dict):
  450. super().__init__(events)
  451. self.name = name
  452. self.vars_dict = vars_dict
  453. self.generator = False
  454. def on_left_scope(self):
  455. self.events.end_rule(self, self.name)
  456. def on_variable(self, key, value):
  457. self.vars_dict[key] = value
  458. if key == 'generator':
  459. self.generator = True
  460. class NinjaParserEventsWithVars(NinjaParserEvents):
  461. def __init__(self, parser):
  462. super().__init__(parser)
  463. self.rule_vars = defaultdict(lambda: dict())
  464. self.global_vars = dict()
  465. self.scope = None
  466. def variable(self, name, value):
  467. if self.scope:
  468. self.scope.on_variable(name, value)
  469. else:
  470. self.global_vars[name] = self.expand(value)
  471. def begin_build(self, out, iout, rule, in_, iin, orderdep):
  472. if rule != 'phony' and rule not in self.rule_vars:
  473. self.parser.parse_error("undefined rule '%s'" % rule)
  474. self.scope = BuildScope(self, out, iout, rule, in_, iin, orderdep, self.rule_vars[rule])
  475. def begin_pool(self, name):
  476. # pool declarations are ignored. Just gobble all the variables
  477. self.scope = Scope(self)
  478. def begin_rule(self, name):
  479. if name in self.rule_vars:
  480. self.parser.parse_error("duplicate rule '%s'" % name)
  481. self.scope = RuleScope(self, name, self.rule_vars[name])
  482. def end_scope(self):
  483. self.scope.on_left_scope()
  484. self.scope = None
  485. # utility functions:
  486. def expand(self, x, rule_vars=None, build_vars=None):
  487. return expand(x, rule_vars, build_vars, self.global_vars)
  488. def expand_and_normalize(self, x):
  489. return normpath(self.expand(x))
  490. # extra events not present in the superclass:
  491. def end_build(self, scope, out, iout, rule, in_, iin, orderdep):
  492. pass
  493. def end_rule(self, scope, name):
  494. pass
  495. # ---- test client that just prints back whatever it parsed ----
  496. class Writer(NinjaParserEvents):
  497. ARGS = argparse.ArgumentParser(description='Rewrite input build.ninja to stdout.')
  498. def __init__(self, output, parser, args):
  499. super().__init__(parser)
  500. self.output = output
  501. self.indent = ''
  502. self.had_vars = False
  503. def dollar_token(self, word, in_path=False):
  504. return '$' + word
  505. def print(self, *args, **kwargs):
  506. if len(args):
  507. self.output.write(self.indent)
  508. print(*args, **kwargs, file=self.output)
  509. def variable(self, name, value):
  510. self.print('%s = %s' % (name, value))
  511. self.had_vars = True
  512. def begin_scope(self):
  513. self.indent = ' '
  514. self.had_vars = False
  515. def end_scope(self):
  516. if self.had_vars:
  517. self.print()
  518. self.indent = ''
  519. self.had_vars = False
  520. def begin_pool(self, name):
  521. self.print('pool %s' % name)
  522. self.begin_scope()
  523. def begin_rule(self, name):
  524. self.print('rule %s' % name)
  525. self.begin_scope()
  526. def begin_build(self, outputs, implicit_outputs, rule, inputs, implicit, order_only):
  527. all_outputs = list(outputs)
  528. all_inputs = list(inputs)
  529. if implicit:
  530. all_inputs.append('|')
  531. all_inputs.extend(implicit)
  532. if order_only:
  533. all_inputs.append('||')
  534. all_inputs.extend(order_only)
  535. if implicit_outputs:
  536. all_outputs.append('|')
  537. all_outputs.extend(implicit_outputs)
  538. self.print('build %s: %s' % (' '.join(all_outputs),
  539. ' '.join([rule] + all_inputs)))
  540. self.begin_scope()
  541. def default(self, targets):
  542. self.print('default %s' % ' '.join(targets))
  543. # ---- emit compile_commands.json ----
  544. class Compdb(NinjaParserEventsWithVars):
  545. ARGS = argparse.ArgumentParser(description='Emit compile_commands.json.')
  546. ARGS.add_argument('rules', nargs='*',
  547. help='The ninja rules to emit compilation commands for.')
  548. def __init__(self, output, parser, args):
  549. super().__init__(parser)
  550. self.output = output
  551. self.rules = args.rules
  552. self.sep = ''
  553. def begin_file(self):
  554. self.output.write('[')
  555. self.directory = os.getcwd()
  556. def print_entry(self, **entry):
  557. entry['directory'] = self.directory
  558. self.output.write(self.sep + json.dumps(entry))
  559. self.sep = ',\n'
  560. def begin_build(self, out, iout, rule, in_, iin, orderdep):
  561. if in_ and rule in self.rules:
  562. super().begin_build(out, iout, rule, in_, iin, orderdep)
  563. else:
  564. self.scope = Scope(self)
  565. def end_build(self, scope, out, iout, rule, in_, iin, orderdep):
  566. self.print_entry(command=scope.expand('${command}'), file=in_[0])
  567. def end_file(self):
  568. self.output.write(']\n')
  569. # ---- clean output files ----
  570. class Clean(NinjaParserEventsWithVars):
  571. ARGS = argparse.ArgumentParser(description='Remove output build files.')
  572. ARGS.add_argument('-g', dest='generator', action='store_true',
  573. help='clean generated files too')
  574. def __init__(self, output, parser, args):
  575. super().__init__(parser)
  576. self.dry_run = args.dry_run
  577. self.verbose = args.verbose or args.dry_run
  578. self.generator = args.generator
  579. def begin_file(self):
  580. print('Cleaning... ', end=(None if self.verbose else ''), flush=True)
  581. self.cnt = 0
  582. def end_file(self):
  583. print('%d files' % self.cnt)
  584. def do_clean(self, *files):
  585. for f in files:
  586. if self.dry_run:
  587. if os.path.exists(f):
  588. self.cnt += 1
  589. print('Would remove ' + f)
  590. continue
  591. else:
  592. try:
  593. if os.path.isdir(f):
  594. shutil.rmtree(f)
  595. else:
  596. os.unlink(f)
  597. self.cnt += 1
  598. if self.verbose:
  599. print('Removed ' + f)
  600. except FileNotFoundError:
  601. pass
  602. def end_build(self, scope, out, iout, rule, in_, iin, orderdep):
  603. if rule == 'phony':
  604. return
  605. if self.generator:
  606. rspfile = scope.expand('${rspfile}')
  607. if rspfile:
  608. self.do_clean(rspfile)
  609. if self.generator or not scope.expand('${generator}'):
  610. self.do_clean(*out, *iout)
  611. depfile = scope.expand('${depfile}')
  612. if depfile:
  613. self.do_clean(depfile)
  614. # ---- convert build.ninja to makefile ----
  615. class Ninja2Make(NinjaParserEventsWithVars):
  616. ARGS = argparse.ArgumentParser(description='Convert build.ninja to a Makefile.')
  617. ARGS.add_argument('--clean', dest='emit_clean', action='store_true',
  618. help='Emit clean/distclean rules.')
  619. ARGS.add_argument('--doublecolon', action='store_true',
  620. help='Emit double-colon rules for phony targets.')
  621. ARGS.add_argument('--omit', metavar='TARGET', nargs='+',
  622. help='Targets to omit.')
  623. def __init__(self, output, parser, args):
  624. super().__init__(parser)
  625. self.output = output
  626. self.emit_clean = args.emit_clean
  627. self.doublecolon = args.doublecolon
  628. self.omit = set(args.omit)
  629. if self.emit_clean:
  630. self.omit.update(['clean', 'distclean'])
  631. # Lists of targets are kept in memory and emitted only at the
  632. # end because appending is really inefficient in GNU make.
  633. # We only do it when it's O(#rules) or O(#variables), but
  634. # never when it could be O(#targets).
  635. self.depfiles = list()
  636. self.rspfiles = list()
  637. self.build_vars = defaultdict(lambda: dict())
  638. self.rule_targets = defaultdict(lambda: list())
  639. self.stamp_targets = defaultdict(lambda: list())
  640. self.all_outs = set()
  641. self.all_ins = set()
  642. self.all_phony = set()
  643. self.seen_default = False
  644. def print(self, *args, **kwargs):
  645. print(*args, **kwargs, file=self.output)
  646. def dollar_token(self, word, in_path=False):
  647. if in_path and word == ' ':
  648. self.parser.parse_error('Make does not support spaces in filenames')
  649. return '$$' if word == '$' else word
  650. def print_phony(self, outs, ins):
  651. targets = ' '.join(outs).replace('$', '$$')
  652. deps = ' '.join(ins).replace('$', '$$')
  653. deps = deps.strip()
  654. if self.doublecolon:
  655. self.print(targets + '::' + (' ' if deps else '') + deps + ';@:')
  656. else:
  657. self.print(targets + ':' + (' ' if deps else '') + deps)
  658. self.all_phony.update(outs)
  659. def begin_file(self):
  660. self.print(r'# This is an automatically generated file, and it shows.')
  661. self.print(r'ninja-default:')
  662. self.print(r'.PHONY: ninja-default ninja-clean ninja-distclean')
  663. if self.emit_clean:
  664. self.print(r'ninja-clean:: ninja-clean-start; $(if $V,,@)rm -f ${ninja-depfiles}')
  665. self.print(r'ninja-clean-start:; $(if $V,,@echo Cleaning...)')
  666. self.print(r'ninja-distclean:: clean; $(if $V,,@)rm -f ${ninja-rspfiles}')
  667. self.print(r'.PHONY: ninja-clean-start')
  668. self.print_phony(['clean'], ['ninja-clean'])
  669. self.print_phony(['distclean'], ['ninja-distclean'])
  670. self.print(r'vpath')
  671. self.print(r'NULL :=')
  672. self.print(r'SPACE := ${NULL} #')
  673. self.print(r'MAKEFLAGS += -rR')
  674. self.print(r'define NEWLINE')
  675. self.print(r'')
  676. self.print(r'endef')
  677. self.print(r'.var.in_newline = $(subst $(SPACE),$(NEWLINE),${.var.in})')
  678. self.print(r"ninja-command = $(if $V,,$(if ${.var.description},@printf '%s\n' '$(subst ','\'',${.var.description})' && ))${.var.command}")
  679. 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")
  680. def end_file(self):
  681. def natural_sort_key(s, _nsre=re.compile('([0-9]+)')):
  682. return [int(text) if text.isdigit() else text.lower()
  683. for text in _nsre.split(s)]
  684. self.print()
  685. self.print('ninja-outputdirs :=')
  686. for rule in self.rule_vars:
  687. if rule == 'phony':
  688. continue
  689. self.print('ninja-targets-%s := %s' % (rule, ' '.join(self.rule_targets[rule])))
  690. self.print('ninja-stamp-%s := %s' % (rule, ' '.join(self.stamp_targets[rule])))
  691. self.print('ninja-outputdirs += $(sort $(dir ${ninja-targets-%s}))' % rule)
  692. self.print()
  693. self.print('dummy := $(shell mkdir -p . $(sort $(ninja-outputdirs)))')
  694. self.print('ninja-depfiles :=' + ' '.join(self.depfiles))
  695. self.print('ninja-rspfiles :=' + ' '.join(self.rspfiles))
  696. self.print('-include ${ninja-depfiles}')
  697. self.print()
  698. for targets in self.build_vars:
  699. for name, value in self.build_vars[targets].items():
  700. self.print('%s: private .var.%s := %s' %
  701. (targets, name, value.replace('$', '$$')))
  702. self.print()
  703. if not self.seen_default:
  704. default_targets = sorted(self.all_outs - self.all_ins, key=natural_sort_key)
  705. self.print('ninja-default: ' + ' '.join(default_targets))
  706. # This is a hack... Meson declares input meson.build files as
  707. # phony, because Ninja does not have an equivalent of Make's
  708. # "path/to/file:" declaration that ignores "path/to/file" even
  709. # if it is absent. However, Makefile.ninja wants to depend on
  710. # build.ninja, which in turn depends on these phony targets which
  711. # would cause Makefile.ninja to be rebuilt in a loop.
  712. phony_targets = sorted(self.all_phony - self.all_ins, key=natural_sort_key)
  713. self.print('.PHONY: ' + ' '.join(phony_targets))
  714. def variable(self, name, value):
  715. super().variable(name, value)
  716. if self.scope is None:
  717. self.global_vars[name] = self.expand(value)
  718. self.print('.var.%s := %s' % (name, self.global_vars[name]))
  719. def begin_build(self, out, iout, rule, in_, iin, orderdep):
  720. if any(x in self.omit for x in out):
  721. self.scope = Scope(self)
  722. return
  723. super().begin_build(out, iout, rule, in_, iin, orderdep)
  724. self.current_targets = ' '.join(self.scope.out + self.scope.iout).replace('$', '$$')
  725. def end_build(self, scope, out, iout, rule, in_, iin, orderdep):
  726. self.rule_targets[rule] += self.scope.out
  727. self.rule_targets[rule] += self.scope.iout
  728. self.all_outs.update(self.scope.iout)
  729. self.all_outs.update(self.scope.out)
  730. self.all_ins.update(self.scope.in_)
  731. self.all_ins.update(self.scope.iin)
  732. targets = self.current_targets
  733. self.current_targets = None
  734. if rule == 'phony':
  735. # Phony rules treat order-only dependencies as normal deps
  736. self.print_phony(out + iout, in_ + iin + orderdep)
  737. return
  738. inputs = ' '.join(in_ + iin).replace('$', '$$')
  739. orderonly = ' '.join(orderdep).replace('$', '$$')
  740. rspfile = scope.expand('${rspfile}')
  741. if rspfile:
  742. rspfile_content = scope.expand('${rspfile_content}')
  743. with open(rspfile, 'w') as f:
  744. f.write(rspfile_content)
  745. inputs += ' ' + rspfile
  746. self.rspfiles.append(rspfile)
  747. restat = 'restat' in self.scope.build_vars or 'restat' in self.rule_vars[rule]
  748. depfile = scope.expand('${depfile}')
  749. build_vars = {
  750. 'command': scope.expand('${command}'),
  751. 'description': scope.expand('${description}'),
  752. 'out': scope.expand('${out}')
  753. }
  754. if restat and not depfile:
  755. if len(out) == 1:
  756. stamp = out[0] + '.stamp'
  757. else:
  758. stamp = '%s@%s.stamp' % (rule, sha1_text(targets)[0:11])
  759. self.print('%s: %s; @:' % (targets, stamp))
  760. self.print('ifneq (%s, $(wildcard %s))' % (targets, targets))
  761. self.print('.PHONY: %s' % (stamp, ))
  762. self.print('endif')
  763. self.print('%s: %s | %s; ${ninja-command-restat}' % (stamp, inputs, orderonly))
  764. self.rule_targets[rule].append(stamp)
  765. self.stamp_targets[rule].append(stamp)
  766. self.build_vars[stamp] = build_vars
  767. else:
  768. self.print('%s: %s | %s; ${ninja-command}' % (targets, inputs, orderonly))
  769. self.build_vars[targets] = build_vars
  770. if depfile:
  771. self.depfiles.append(depfile)
  772. def end_rule(self, scope, name):
  773. # Note that the generator pseudo-variable could also be attached
  774. # to a build block rather than a rule. This is not handled here
  775. # in order to reduce the number of "rm" invocations. However,
  776. # "ninjatool.py -t clean" does that correctly.
  777. target = 'distclean' if scope.generator else 'clean'
  778. self.print('ninja-%s:: ; $(if $V,,@)rm -f ${ninja-stamp-%s}' % (target, name))
  779. if self.emit_clean:
  780. self.print('ninja-%s:: ; $(if $V,,@)rm -rf ${ninja-targets-%s}' % (target, name))
  781. def default(self, targets):
  782. self.print("ninja-default: " + ' '.join(targets))
  783. self.seen_default = True
  784. # ---- command line parsing ----
  785. # we cannot use subparsers because tools are chosen through the "-t"
  786. # option.
  787. class ToolAction(argparse.Action):
  788. def __init__(self, option_strings, dest, choices, metavar='TOOL', nargs=None, **kwargs):
  789. if nargs is not None:
  790. raise ValueError("nargs not allowed")
  791. super().__init__(option_strings, dest, required=True, choices=choices,
  792. metavar=metavar, **kwargs)
  793. def __call__(self, parser, namespace, value, option_string):
  794. tool = self.choices[value]
  795. setattr(namespace, self.dest, tool)
  796. tool.ARGS.prog = '%s %s %s' % (parser.prog, option_string, value)
  797. class ToolHelpAction(argparse.Action):
  798. def __init__(self, option_strings, dest, nargs=None, **kwargs):
  799. if nargs is not None:
  800. raise ValueError("nargs not allowed")
  801. super().__init__(option_strings, dest, nargs=0, **kwargs)
  802. def __call__(self, parser, namespace, values, option_string=None):
  803. if namespace.tool:
  804. namespace.tool.ARGS.print_help()
  805. else:
  806. parser.print_help()
  807. parser.exit()
  808. tools = {
  809. 'test': Writer,
  810. 'ninja2make': Ninja2Make,
  811. 'compdb': Compdb,
  812. 'clean': Clean,
  813. }
  814. parser = argparse.ArgumentParser(description='Process and transform build.ninja files.',
  815. add_help=False)
  816. parser.add_argument('-C', metavar='DIR', dest='dir', default='.',
  817. help='change to DIR before doing anything else')
  818. parser.add_argument('-f', metavar='FILE', dest='file', default='build.ninja',
  819. help='specify input build file [default=build.ninja]')
  820. parser.add_argument('-n', dest='dry_run', action='store_true',
  821. help='do not actually do anything')
  822. parser.add_argument('-v', dest='verbose', action='store_true',
  823. help='be more verbose')
  824. parser.add_argument('-t', dest='tool', choices=tools, action=ToolAction,
  825. help='choose the tool to run')
  826. parser.add_argument('-h', '--help', action=ToolHelpAction,
  827. help='show this help message and exit')
  828. if len(sys.argv) >= 2 and sys.argv[1] == '--version':
  829. print('1.8')
  830. sys.exit(0)
  831. args, tool_args = parser.parse_known_args()
  832. args.tool.ARGS.parse_args(tool_args, args)
  833. os.chdir(args.dir)
  834. with open(args.file, 'r') as f:
  835. parser = NinjaParser(args.file, f)
  836. try:
  837. events = args.tool(sys.stdout, parser, args)
  838. except InvalidArgumentError as e:
  839. parser.error(str(e))
  840. parser.parse(events)