1 /** 2 * Copyright: Copyright Jason White, 2016 3 * License: MIT 4 * Authors: Jason White 5 * 6 * Description: 7 * Handles running gcc processes. 8 * 9 * Inputs and outputs are detected by adding the -MMD option and parsing the 10 * deps file that gcc produces. 11 */ 12 module button.handlers.gcc; 13 14 import button.log; 15 import button.resource; 16 import button.context; 17 18 import std.range.primitives : isInputRange, ElementEncodingType, 19 front, empty, popFront; 20 21 import std.traits : isSomeChar; 22 23 /** 24 * Exception that is thrown on invalid GCC deps syntax. 25 */ 26 class MakeParserError : Exception 27 { 28 this(string msg) 29 { 30 // TODO: Include line information? 31 super(msg); 32 } 33 } 34 35 /** 36 * Helper function to escape a character. 37 */ 38 private C escapeChar(C)(C c) 39 if (isSomeChar!C) 40 { 41 switch (c) 42 { 43 case 't': return '\t'; 44 case 'v': return '\v'; 45 case 'r': return '\r'; 46 case 'n': return '\n'; 47 case 'b': return '\b'; 48 case 'f': return '\f'; 49 case '0': return '\0'; 50 default: return c; 51 } 52 } 53 54 /** 55 * A single Make rule. 56 */ 57 struct MakeRule 58 { 59 string target; 60 string[] deps; 61 } 62 63 /** 64 * An input range of Make rules. 65 * 66 * This parses a deps file that gcc produces. The file consists of simple Make 67 * rules. Rules are separated by lines (discounting line continuations). Each 68 * rule consists of a target file and its dependencies. 69 */ 70 struct MakeRules(Source) 71 if (isInputRange!Source && isSomeChar!(ElementEncodingType!Source)) 72 { 73 private 74 { 75 import std.array : Appender; 76 import std.traits : Unqual; 77 78 alias C = Unqual!(ElementEncodingType!Source); 79 80 Source source; 81 bool _empty; 82 MakeRule current; 83 84 Appender!(C[]) buf; 85 } 86 87 this(Source source) 88 { 89 this.source = source; 90 popFront(); 91 } 92 93 @property 94 bool empty() const pure nothrow 95 { 96 return _empty; 97 } 98 99 @property 100 const(MakeRule) front() const pure nothrow 101 { 102 return current; 103 } 104 105 /** 106 * Parses a single file name. 107 */ 108 private string parseFileName() 109 { 110 import std.uni : isWhite; 111 112 buf.clear(); 113 114 while (!source.empty) 115 { 116 immutable c = source.front; 117 118 if (c == ':' || c.isWhite) 119 { 120 // ':' delimits a target. 121 break; 122 } 123 else if (c == '\\') 124 { 125 // Skip past the '\\' 126 source.popFront(); 127 if (source.empty) 128 break; 129 130 immutable e = source.front; 131 if (e == '\n') 132 { 133 // Line continuation 134 source.popFront(); 135 } 136 else 137 { 138 buf.put(escapeChar(e)); 139 source.popFront(); 140 } 141 142 continue; 143 } 144 145 // Regular character 146 buf.put(c); 147 source.popFront(); 148 } 149 150 return buf.data.idup; 151 } 152 153 /** 154 * Skips spaces. 155 */ 156 private void skipSpace() 157 { 158 import std.uni : isSpace; 159 while (!source.empty && isSpace(source.front)) 160 source.popFront(); 161 } 162 163 /** 164 * Skips whitespace 165 */ 166 private void skipWhite() 167 { 168 import std.uni : isWhite; 169 while (!source.empty && isWhite(source.front)) 170 source.popFront(); 171 } 172 173 /** 174 * Parses the list of dependencies after the target. 175 * 176 * Returns: The list of dependencies. 177 */ 178 private string[] parseDeps() 179 { 180 Appender!(string[]) deps; 181 182 skipSpace(); 183 184 while (!source.empty) 185 { 186 // A new line delimits the dependency list 187 if (source.front == '\n') 188 { 189 source.popFront(); 190 break; 191 } 192 193 auto dep = parseFileName(); 194 if (dep.length) 195 deps.put(dep); 196 197 if (!source.empty && source.front == ':') 198 throw new MakeParserError("Unexpected ':'"); 199 200 skipSpace(); 201 } 202 203 return deps.data; 204 } 205 206 /** 207 * Parses a rule. 208 */ 209 private MakeRule parseRule() 210 { 211 string target = parseFileName(); 212 if (target.empty) 213 throw new MakeParserError("Empty target name"); 214 215 skipSpace(); 216 217 if (source.empty) 218 throw new MakeParserError("Unexpected end of file"); 219 220 if (source.front != ':') 221 throw new MakeParserError("Expected ':' after target name"); 222 223 source.popFront(); 224 225 // Parse dependency names 226 auto deps = parseDeps(); 227 228 skipWhite(); 229 230 return MakeRule(target, deps); 231 } 232 233 void popFront() 234 { 235 skipWhite(); 236 237 if (source.empty) 238 { 239 _empty = true; 240 return; 241 } 242 243 current = parseRule(); 244 } 245 } 246 247 /** 248 * Convenience function for constructing a MakeRules range. 249 */ 250 MakeRules!Source makeRules(Source)(Source source) 251 if (isInputRange!Source && isSomeChar!(ElementEncodingType!Source)) 252 { 253 return MakeRules!Source(source); 254 } 255 256 unittest 257 { 258 import std.array : array; 259 import std.exception : collectException; 260 import std.algorithm.comparison : equal; 261 262 static assert(isInputRange!(MakeRules!string)); 263 264 { 265 auto rules = makeRules( 266 "\n\nfoo.c : foo.h \\\n bar.h \\\n" 267 ); 268 269 assert(rules.equal([ 270 MakeRule("foo.c", ["foo.h", "bar.h"]), 271 ])); 272 } 273 274 { 275 auto rules = makeRules( 276 "foo.c : foo.h \\\n bar.h\n"~ 277 " \nbar.c : bar.h\n"~ 278 "\n \nbaz.c:\n"~ 279 "ba\\\nz.c: blah.h\n"~ 280 `foo\ bar: bing\ bang` 281 ); 282 283 assert(rules.equal([ 284 MakeRule("foo.c", ["foo.h", "bar.h"]), 285 MakeRule("bar.c", ["bar.h"]), 286 MakeRule("baz.c", []), 287 MakeRule("baz.c", ["blah.h"]), 288 MakeRule("foo bar", ["bing bang"]), 289 ])); 290 } 291 292 assert(collectException!MakeParserError(makeRules( 293 "foo.c: foo.h: bar.h" 294 ).array)); 295 } 296 297 void execute( 298 ref BuildContext ctx, 299 const(string)[] args, 300 string workDir, 301 ref Resources inputs, 302 ref Resources outputs, 303 TaskLogger logger 304 ) 305 { 306 import std.file : remove; 307 308 import io.file : File, tempFile, AutoDelete; 309 import io.range : byBlock; 310 311 import button.handlers.base : base = execute; 312 313 // Create the temporary file for the dependencies. 314 auto depsPath = tempFile(AutoDelete.no).path; 315 scope (exit) remove(depsPath); 316 317 // Tell gcc to write dependencies to our temporary file. 318 args ~= ["-MMD", "-MF", depsPath]; 319 320 base(ctx, args, workDir, inputs, outputs, logger); 321 322 // TODO: Parse the command line arguments for -I and -o options. 323 324 // Parse the dependencies 325 auto deps = File(depsPath).byBlock!char; 326 foreach (rule; makeRules(&deps)) 327 { 328 outputs.put(rule.target); 329 inputs.put(rule.deps); 330 } 331 }