1 // SemiTwist D Tools 2 // STManage: STBuild 3 // Written in the D programming language. 4 5 module semitwist.apps.stmanage.stbuild.conf; 6 7 import std.conv; 8 import std.file; 9 import std.string; 10 import std.uni; 11 12 import semitwist.cmd.all; 13 14 enum BuildTool 15 { 16 rdmd, 17 rebuild, 18 xfbuild, 19 } 20 21 string buildToolExecName(BuildTool tool) 22 { 23 switch(tool) 24 { 25 case BuildTool.rdmd: 26 return "rdmd"; 27 case BuildTool.rebuild: 28 return "rebuild"; 29 case BuildTool.xfbuild: 30 return "xfbuild"; 31 default: 32 throw new Exception("Internal Error: Unexpected Build Tool #%s".format(tool)); 33 } 34 } 35 36 class STBuildConfException : Exception 37 { 38 this(string msg) 39 { 40 super(msg); 41 } 42 } 43 44 string quoteArg(string str) 45 { 46 version(Windows) 47 return '"' ~ str ~ '"'; 48 else 49 return '\'' ~ str ~ '\''; 50 } 51 52 class Conf 53 { 54 string[] targets; 55 private Switch[][string][string] flags; 56 57 string[] errors; 58 59 enum string targetAll = "all"; 60 enum string modeRelease = "release"; 61 enum string modeDebug = "debug"; 62 enum string modeAll = "all"; 63 enum string[] predefTargets = [targetAll]; 64 enum string[] modes = [modeRelease, modeDebug, modeAll]; 65 66 // GDC workaround 67 string[] getPredefTargets() 68 { 69 return predefTargets; 70 } 71 72 // GDC workaround 73 string[] getModes() 74 { 75 return modes; 76 } 77 78 string[] targetAllElems; 79 string[] modeAllElems; 80 81 this(string filename) 82 { 83 auto parser = new ConfParser(); 84 parser.doParse(this, filename); 85 86 if(parser.errors.length > 0) 87 { 88 foreach(string error; parser.errors) 89 cmd.echo(error); 90 91 throw new STBuildConfException( 92 "%s error(s) in conf file '%s'" 93 .format(parser.errors.length, filename) 94 ); 95 } 96 97 version(GNU) 98 { 99 foreach(e; std.algorithm.filter!((a) {return a != targetAll; } )(targets)) 100 targetAllElems ~= e; 101 foreach(e; std.algorithm.filter!((a) {return a != modeAll; } )(modes)) 102 modeAllElems ~= e; 103 } 104 else 105 { 106 targetAllElems = array(std.algorithm.filter!( 107 (a) { return a != targetAll; })(targets) 108 );//targets.allExcept(targetAll); 109 modeAllElems = array(std.algorithm.filter!( 110 (a) { return a != modeAll; })(modes) 111 );//modes.allExcept(modeAll); 112 } 113 } 114 115 private Switch[] getFlagsSafe(string target, string mode) 116 { 117 if(target in flags && mode in flags[target]) 118 { 119 Switch[] ret = flags[target][mode].dup; 120 //foreach(int i, Switch sw; ret) 121 // ret[i].data = ret[i].data.dup; 122 123 return ret; 124 } 125 126 return []; 127 } 128 129 private static int convertSwitch(ref Switch[] switches, string fromStr, string toStr) 130 { 131 int numConverted = 0; 132 foreach(ref Switch sw; switches) 133 { 134 if(sw.data == fromStr) 135 { 136 sw.data = toStr; 137 numConverted++; 138 } 139 } 140 return numConverted; 141 } 142 143 private static int convertPrefix(ref Switch[] switches, string fromPrefix, string toPrefix) 144 { 145 int numConverted = 0; 146 foreach(ref Switch sw; switches) 147 { 148 if(sw.data.length >= fromPrefix.length) 149 if(sw.data[0..fromPrefix.length] == fromPrefix) 150 { 151 sw.data = toPrefix~sw.data[fromPrefix.length..$]; 152 numConverted++; 153 } 154 } 155 return numConverted; 156 } 157 158 private static size_t removePrefix(ref Switch[] switches, string prefix) 159 { 160 int[] switchIndicies = []; 161 foreach(int index, Switch sw; switches) 162 { 163 if(sw.data.length >= prefix.length) 164 if(sw.data[0..prefix.length] == prefix) 165 switchIndicies ~= index; 166 } 167 if(switchIndicies.length > 0) 168 { 169 foreach_reverse(int index; switchIndicies) 170 switches = switches[0..index] ~ switches[index+1..$]; 171 } 172 return switchIndicies.length; 173 } 174 175 private static int combinePrefix(ref Switch[] switches, string fromPrefix, string fromSwitch, string toPrefix) 176 { 177 auto numRemoved = removePrefix(switches, fromSwitch); 178 if(numRemoved > 0) 179 return convertPrefix(switches, fromPrefix, toPrefix); 180 return 0; 181 } 182 183 private static int splitPrefix(ref Switch[] switches, string fromPrefix, string toPrefix, string toSwitch) 184 { 185 auto numConverted = convertPrefix(switches, fromPrefix, toPrefix); 186 if(numConverted > 0) 187 switches ~= Switch(toSwitch, false); 188 return numConverted; 189 } 190 191 private static void moveSourceFileToEnd(ref Switch[] switches) 192 { 193 size_t sourceIndex = switches.length; 194 195 foreach(int i, Switch sw; switches) 196 if( !sw.data.startsWith("-") && !sw.data.startsWith("+") ) 197 { 198 sourceIndex = i; 199 break; 200 } 201 202 if(sourceIndex < switches.length-1) 203 { 204 switches = 205 switches[0..sourceIndex] ~ 206 switches[sourceIndex+1..$] ~ 207 switches[sourceIndex]; 208 } 209 } 210 211 private static void convert(ref Switch[] switches, BuildTool tool) 212 { 213 switch(tool) 214 { 215 case BuildTool.rdmd: 216 convertPrefix(switches, "-oq", "-od"); 217 convertPrefix(switches, "+o", "-of"); 218 convertPrefix(switches, "+O", "-od"); 219 convertPrefix(switches, "-C", "" ); 220 convertSwitch(switches, "-v", "--chatty"); 221 convertSwitch(switches, "+v", "--chatty"); 222 convertPrefix(switches, "+nolink", "-c"); 223 removePrefix(switches, "+"); 224 225 // Source file must come last 226 moveSourceFileToEnd(switches); 227 228 break; 229 230 case BuildTool.rebuild: 231 combinePrefix(switches, "+O", "+q", "-oq"); 232 convertPrefix(switches, "+o", "-of"); 233 convertPrefix(switches, "+O", "-od"); 234 convertPrefix(switches, "--chatty", "-v"); 235 convertPrefix(switches, "+nolink", "-c"); 236 removePrefix(switches, "+"); 237 removePrefix(switches, "--"); 238 break; 239 240 case BuildTool.xfbuild: 241 //switches.splitPrefix("-oq", "+O", "+q"); //Doesn't work for DMD 242 convertPrefix(switches, "-oq", "+O"); 243 244 convertPrefix(switches, "-C", "" ); 245 convertPrefix(switches, "-of", "+o"); 246 convertPrefix(switches, "-od", "+O"); 247 convertPrefix(switches, "--chatty", "+v"); 248 convertPrefix(switches, "-c", "+nolink"); 249 removePrefix(switches, "--"); 250 break; 251 252 default: 253 throw new Exception("Internal Error: Unexpected Build Tool #%s".format(tool)); 254 } 255 } 256 257 private static string switchesToString(Switch[] switches) 258 { 259 return 260 std..string.join( 261 switches.map( (Switch sw) { return sw.toString(); } ), 262 " " 263 ); 264 } 265 266 unittest 267 { 268 Switch[] switches; 269 auto start = `+foo "-od od" -foo +q +q +o_o -of_of -C_C -oq_oq +O_O +foo`; 270 auto re = `"-od od" -foo -of_o -of_of -C_C -oq_oq -oq_O`; 271 //auto xf = `+foo "+O od" -foo +q +q +o_o +o_of _C +O_oq +O_O +foo +q`; // See "Doesn't work for DMD" above 272 auto xf = `+foo "+O od" -foo +q +q +o_o +o_of _C +O_oq +O_O +foo`; 273 auto start2 = `+foo -od_od -foo +o_o -of_of -C_C -oq_oq +O_O +foo`; 274 auto re2 = `-od_od -foo -of_o -of_of -C_C -oq_oq -od_O`; 275 276 version(Posix) 277 { 278 re = re.replace(`"`, `'`); 279 xf = xf.replace(`"`, `'`); 280 } 281 282 switches = ConfParser.splitSwitches(start); 283 convert(switches, BuildTool.rebuild); 284 mixin(deferEnsure!(`switchesToString(switches)`, `_ == re`)); 285 286 switches = ConfParser.splitSwitches(start); 287 convert(switches, BuildTool.xfbuild); 288 mixin(deferEnsure!(`switchesToString(switches)`, `_ == xf`)); 289 290 switches = ConfParser.splitSwitches(start2); 291 convert(switches, BuildTool.rebuild); 292 mixin(deferEnsure!(`switchesToString(switches)`, `_ == re2`)); 293 } 294 295 private static bool addDefault(ref Switch[] switches, string prefix, string defaultVal) 296 { 297 bool prefixFound=false; 298 foreach(Switch sw; switches) 299 if(sw.data.startsWith(prefix)) 300 { 301 prefixFound = true; 302 break; 303 } 304 if(!prefixFound) 305 switches ~= Switch(prefix~defaultVal, false); 306 return !prefixFound; 307 } 308 309 private static void addDefaults(ref Switch[] switches) 310 { 311 // Keep object and deps files from each target/mode 312 // separate so things don't get screwed up. 313 addDefault(switches, "-oq", "obj/$(TARGET)/$(MODE)/"); 314 addDefault(switches, "+D", "obj/$(TARGET)/$(MODE)/deps"); 315 addDefault(switches, "--build-only", ""); 316 } 317 318 private static string fixSlashes(string str) 319 { 320 version(Windows) 321 return std.array.replace(str, "/", "\\"); 322 else 323 return std.array.replace(str, "\\", "/"); 324 //return str; 325 } 326 327 string getFlags(string target, string mode, BuildTool tool) 328 { 329 auto isTargetAll = (target == targetAll); 330 auto isModeAll = (mode == modeAll ); 331 332 Switch[] switches = getFlagsSafe(target, mode); 333 if(!isTargetAll) switches ~= getFlagsSafe(targetAll, mode ); 334 if(!isModeAll ) switches ~= getFlagsSafe(target, modeAll); 335 if(!isTargetAll && !isModeAll) switches ~= getFlagsSafe(targetAll, modeAll); 336 337 addDefaults(switches); 338 convert(switches, tool); 339 auto flags = fixSlashes(switchesToString(switches)); 340 //mixin(traceVal!("flags")); 341 flags = std.array.replace(flags, "$(TARGET)", target); 342 flags = std.array.replace(flags, "$(MODE)", mode); 343 flags = std.array.replace(flags, "$(OS)", enumOSToString(os)); 344 flags = std.array.replace(flags, "$()", ""); 345 return flags; 346 /+ return 347 //fixSlashes(switchesToString(switches)) 348 x 349 .replace("$(TARGET)", target) 350 .replace("$(MODE)", mode) 351 .replace("$(OS)", enumOSToString(os)) 352 .replace("$()", ""); 353 //.format(target, mode, enumOSToString(os), "");+/ 354 } 355 356 struct Switch 357 { 358 string data; 359 bool quoted; 360 string toString() 361 { 362 return quoted? quoteArg(data) : data; 363 } 364 } 365 366 private class ConfParser 367 { 368 private Conf conf; 369 private string filename; 370 371 private uint stmtLineNo; 372 private string partialStmt=null; 373 374 string[] currTargets; 375 string[] currModes; 376 377 string[] targets=null; 378 string[] modes=null; 379 380 string[] errors; 381 382 static Switch[] splitSwitches(string str) 383 { 384 Switch[] ret = []; 385 bool inPlainSwitch=false; 386 bool inQuotedSwitch=false; 387 foreach(dchar c; str) 388 { 389 if(inPlainSwitch) 390 { 391 if(isWhite(c)) 392 inPlainSwitch = false; 393 else 394 ret[$-1].data ~= to!(string)(c); 395 } 396 else if(inQuotedSwitch) 397 { 398 if(c == `"`d[0]) 399 inQuotedSwitch = false; 400 else 401 ret[$-1].data ~= to!(string)(c); 402 } 403 else 404 { 405 if(c == `"`d[0]) 406 { 407 ret ~= Switch("", true); 408 inQuotedSwitch = true; 409 } 410 else if(!isWhite(c)) 411 { 412 ret ~= Switch(to!(string)(c), false); 413 inPlainSwitch = true; 414 } 415 } 416 } 417 return ret; 418 } 419 420 private void doParse(Conf conf, string filename) 421 { 422 mixin(initMember("conf", "filename")); 423 424 if(!exists(filename) || !isFile(filename)) 425 throw new STBuildConfException( 426 "Can't find configuration file '%s'".format(filename) 427 ); 428 429 auto input = cast(string)read(filename); 430 uint lineno = 1; 431 foreach(string line; input.splitLines()) 432 { 433 parseLine(line, lineno); 434 lineno++; 435 } 436 437 if(targets is null) 438 error(`No targets defined (Forgot "target targetname1, targetname2"?)`); 439 440 conf.targets = targets ~ predefTargets; 441 //conf.modes = modes; 442 conf.errors = errors; 443 } 444 445 private void parseLine(string line, uint lineno) 446 { 447 auto commentStart = line.locate('#'); 448 //if(commentStart == -1) commentStart = line.length; 449 auto stmt = line[0..commentStart].strip(); 450 451 version(verbose) 452 { 453 writef("%s: ", lineno); 454 455 if(stmt == "") 456 writef("BlankLine "); 457 else 458 writef("Statement[%s] ", stmt); 459 460 if(commentStart < line.length) 461 writef("Comment[%s] ", line[commentStart..$]); 462 463 scope(exit) Stdout.newline; 464 } 465 466 if(partialStmt is null) 467 stmtLineNo = lineno; 468 else 469 stmt = partialStmt ~ " " ~ stmt; 470 471 if(stmt != "") 472 { 473 if(stmt[$-1] == '_') 474 { 475 version(verbose) Stdout("TBC "); 476 477 partialStmt = stmt[0..$-1]; 478 return; 479 } 480 version(verbose) if(partialStmt !is null) writefln("\nFullStmt[%s] ", stmt); 481 partialStmt = null; 482 483 if(stmt[0] == '[' && stmt[$-1] == ']') 484 { 485 stmt = stmt[1..$-1]; 486 auto delimIndex = stmt.indexOf(':'); 487 if(delimIndex == -1) 488 error("Rule definition header must be of the form [target(s):mode(s)]"); 489 else 490 { 491 currTargets = parseCSV(stmt[0..delimIndex]); 492 currModes = parseCSV(stmt[delimIndex+1..$]); 493 } 494 } 495 else 496 { 497 auto stmtParts = stmt.split(); 498 auto stmtCmd = stmtParts[0]; 499 auto stmtPred = stmt[stmtCmd.length..$].strip(); 500 switch(stmtCmd) 501 { 502 case "target": 503 setList(targets, stmtCmd, stmtPred, conf.getPredefTargets); 504 break; 505 case "flags": 506 if(currTargets is null) 507 error("'%s' must be in a target definition".format(stmtCmd)); 508 else 509 { 510 foreach(string target; currTargets) 511 foreach(string mode; currModes) 512 conf.flags[target][mode] ~= splitSwitches(stmtPred); 513 } 514 break; 515 default: 516 error("Unsupported command '%s'".format(stmtCmd)); 517 break; 518 } 519 } 520 } 521 } 522 523 private void error(string msg) 524 { 525 errors ~= "%s(%s): %s".format(filename, stmtLineNo, msg); 526 } 527 528 private string[] parseCSV(string str) 529 { 530 string[] ret; 531 foreach(string name; str.split(",")) 532 if(name.strip() != "") 533 ret ~= name.strip(); 534 return ret; 535 } 536 537 private void setList(ref string[] set, string command, string listStr, string[] predefined) 538 { 539 if(currTargets !is null) 540 { 541 error("Statement '%s' must come before the rule definitions".format(command)); 542 return; 543 } 544 545 if(set !is null) 546 { 547 error("List '%s' has already been set".format(command)); 548 return; 549 } 550 551 set ~= parseCSV(listStr); 552 foreach(int i, string elem; set) 553 { 554 if(std.algorithm.find(predefined, elem) != []) 555 error("'%s' is a reserved value for '%s'".format(elem, command)); 556 else 557 { 558 if(std.algorithm.find(set[0..i], elem) != []) 559 error("'%s' is defined more than once in list '%s'".format(elem, command)); 560 } 561 } 562 } 563 } 564 }