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 }