1#!/usr/bin/python 2 3# Copyright 2011 Google Inc. All Rights Reserved. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17"""Python module for generating .ninja files. 18 19Note that this is emphatically not a required piece of Ninja; it's 20just a helpful utility for build-file-generation systems that already 21use Python. 22""" 23 24import re 25import textwrap 26from io import TextIOWrapper 27from typing import Dict, List, Match, Optional, Tuple, Union 28 29def escape_path(word: str) -> str: 30 return word.replace('$ ', '$$ ').replace(' ', '$ ').replace(':', '$:') 31 32class Writer(object): 33 def __init__(self, output: TextIOWrapper, width: int = 78) -> None: 34 self.output = output 35 self.width = width 36 37 def newline(self) -> None: 38 self.output.write('\n') 39 40 def comment(self, text: str) -> None: 41 for line in textwrap.wrap(text, self.width - 2, break_long_words=False, 42 break_on_hyphens=False): 43 self.output.write('# ' + line + '\n') 44 45 def variable( 46 self, 47 key: str, 48 value: Optional[Union[bool, int, float, str, List[str]]], 49 indent: int = 0, 50 ) -> None: 51 if value is None: 52 return 53 if isinstance(value, list): 54 value = ' '.join(filter(None, value)) # Filter out empty strings. 55 self._line('%s = %s' % (key, value), indent) 56 57 def pool(self, name: str, depth: int) -> None: 58 self._line('pool %s' % name) 59 self.variable('depth', depth, indent=1) 60 61 def rule( 62 self, 63 name: str, 64 command: str, 65 description: Optional[str] = None, 66 depfile: Optional[str] = None, 67 generator: bool = False, 68 pool: Optional[str] = None, 69 restat: bool = False, 70 rspfile: Optional[str] = None, 71 rspfile_content: Optional[str] = None, 72 deps: Optional[Union[str, List[str]]] = None, 73 ) -> None: 74 self._line('rule %s' % name) 75 self.variable('command', command, indent=1) 76 if description: 77 self.variable('description', description, indent=1) 78 if depfile: 79 self.variable('depfile', depfile, indent=1) 80 if generator: 81 self.variable('generator', '1', indent=1) 82 if pool: 83 self.variable('pool', pool, indent=1) 84 if restat: 85 self.variable('restat', '1', indent=1) 86 if rspfile: 87 self.variable('rspfile', rspfile, indent=1) 88 if rspfile_content: 89 self.variable('rspfile_content', rspfile_content, indent=1) 90 if deps: 91 self.variable('deps', deps, indent=1) 92 93 def build( 94 self, 95 outputs: Union[str, List[str]], 96 rule: str, 97 inputs: Optional[Union[str, List[str]]] = None, 98 implicit: Optional[Union[str, List[str]]] = None, 99 order_only: Optional[Union[str, List[str]]] = None, 100 variables: Optional[ 101 Union[ 102 List[Tuple[str, Optional[Union[str, List[str]]]]], 103 Dict[str, Optional[Union[str, List[str]]]], 104 ] 105 ] = None, 106 implicit_outputs: Optional[Union[str, List[str]]] = None, 107 pool: Optional[str] = None, 108 dyndep: Optional[str] = None, 109 ) -> List[str]: 110 outputs = as_list(outputs) 111 out_outputs = [escape_path(x) for x in outputs] 112 all_inputs = [escape_path(x) for x in as_list(inputs)] 113 114 if implicit: 115 implicit = [escape_path(x) for x in as_list(implicit)] 116 all_inputs.append('|') 117 all_inputs.extend(implicit) 118 if order_only: 119 order_only = [escape_path(x) for x in as_list(order_only)] 120 all_inputs.append('||') 121 all_inputs.extend(order_only) 122 if implicit_outputs: 123 implicit_outputs = [escape_path(x) 124 for x in as_list(implicit_outputs)] 125 out_outputs.append('|') 126 out_outputs.extend(implicit_outputs) 127 128 self._line('build %s: %s' % (' '.join(out_outputs), 129 ' '.join([rule] + all_inputs))) 130 if pool is not None: 131 self._line(' pool = %s' % pool) 132 if dyndep is not None: 133 self._line(' dyndep = %s' % dyndep) 134 135 if variables: 136 if isinstance(variables, dict): 137 iterator = iter(variables.items()) 138 else: 139 iterator = iter(variables) 140 141 for key, val in iterator: 142 self.variable(key, val, indent=1) 143 144 return outputs 145 146 def include(self, path: str) -> None: 147 self._line('include %s' % path) 148 149 def subninja(self, path: str) -> None: 150 self._line('subninja %s' % path) 151 152 def default(self, paths: Union[str, List[str]]) -> None: 153 self._line('default %s' % ' '.join(as_list(paths))) 154 155 def _count_dollars_before_index(self, s: str, i: int) -> int: 156 """Returns the number of '$' characters right in front of s[i].""" 157 dollar_count = 0 158 dollar_index = i - 1 159 while dollar_index > 0 and s[dollar_index] == '$': 160 dollar_count += 1 161 dollar_index -= 1 162 return dollar_count 163 164 def _line(self, text: str, indent: int = 0) -> None: 165 """Write 'text' word-wrapped at self.width characters.""" 166 leading_space = ' ' * indent 167 while len(leading_space) + len(text) > self.width: 168 # The text is too wide; wrap if possible. 169 170 # Find the rightmost space that would obey our width constraint and 171 # that's not an escaped space. 172 available_space = self.width - len(leading_space) - len(' $') 173 space = available_space 174 while True: 175 space = text.rfind(' ', 0, space) 176 if (space < 0 or 177 self._count_dollars_before_index(text, space) % 2 == 0): 178 break 179 180 if space < 0: 181 # No such space; just use the first unescaped space we can find. 182 space = available_space - 1 183 while True: 184 space = text.find(' ', space + 1) 185 if (space < 0 or 186 self._count_dollars_before_index(text, space) % 2 == 0): 187 break 188 if space < 0: 189 # Give up on breaking. 190 break 191 192 self.output.write(leading_space + text[0:space] + ' $\n') 193 text = text[space+1:] 194 195 # Subsequent lines are continuations, so indent them. 196 leading_space = ' ' * (indent+2) 197 198 self.output.write(leading_space + text + '\n') 199 200 def close(self) -> None: 201 self.output.close() 202 203 204def as_list(input: Optional[Union[str, List[str]]]) -> List[str]: 205 if input is None: 206 return [] 207 if isinstance(input, list): 208 return input 209 return [input] 210 211 212def escape(string: str) -> str: 213 """Escape a string such that it can be embedded into a Ninja file without 214 further interpretation.""" 215 assert '\n' not in string, 'Ninja syntax does not allow newlines' 216 # We only have one special metacharacter: '$'. 217 return string.replace('$', '$$') 218 219 220def expand(string: str, vars: Dict[str, str], local_vars: Dict[str, str] = {}) -> str: 221 """Expand a string containing $vars as Ninja would. 222 223 Note: doesn't handle the full Ninja variable syntax, but it's enough 224 to make configure.py's use of it work. 225 """ 226 def exp(m: Match[str]) -> str: 227 var = m.group(1) 228 if var == '$': 229 return '$' 230 return local_vars.get(var, vars.get(var, '')) 231 return re.sub(r'\$(\$|\w*)', exp, string) 232