1 /**
2 	SDL format support for PackageRecipe
3 
4 	Copyright: © 2014-2015 rejectedsoftware e.K.
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Sönke Ludwig
7 */
8 module dub.recipe.sdl;
9 
10 import dub.compilers.compiler;
11 import dub.dependency;
12 import dub.internal.sdlang;
13 import dub.internal.vibecompat.core.log;
14 import dub.internal.vibecompat.inet.path;
15 import dub.recipe.packagerecipe;
16 
17 import std.algorithm : map;
18 import std.array : array;
19 import std.conv;
20 import std.string : startsWith;
21 
22 
23 void parseSDL(ref PackageRecipe recipe, string sdl, string parent_name, string filename)
24 {
25 	parseSDL(recipe, parseSource(sdl, filename), parent_name);
26 }
27 
28 void parseSDL(ref PackageRecipe recipe, Tag sdl, string parent_name)
29 {
30 	Tag[] subpacks;
31 	Tag[] configs;
32 
33 	// parse top-level fields
34 	foreach (n; sdl.all.tags) {
35 		enforceSDL(n.name.length > 0, "Anonymous tags are not allowed at the root level.", n);
36 		switch (n.fullName) {
37 			default: break;
38 			case "name": recipe.name = n.stringTagValue; break;
39 			case "version": recipe.version_ = n.stringTagValue; break;
40 			case "description": recipe.description = n.stringTagValue; break;
41 			case "homepage": recipe.homepage = n.stringTagValue; break;
42 			case "authors": recipe.authors ~= n.stringArrayTagValue; break;
43 			case "copyright": recipe.copyright = n.stringTagValue; break;
44 			case "license": recipe.license = n.stringTagValue; break;
45 			case "subPackage": subpacks ~= n; break;
46 			case "configuration": configs ~= n; break;
47 			case "buildType":
48 				auto name = n.stringTagValue(true);
49 				BuildSettingsTemplate bt;
50 				parseBuildSettings(n, bt, parent_name);
51 				recipe.buildTypes[name] = bt;
52 				break;
53 			case "x:ddoxFilterArgs": recipe.ddoxFilterArgs ~= n.stringArrayTagValue; break;
54 			case "x:ddoxTool": recipe.ddoxTool = n.stringTagValue; break;
55 		}
56 	}
57 
58 	enforceSDL(recipe.name.length > 0, "The package \"name\" field is missing or empty.", sdl);
59 	string full_name = parent_name.length ? parent_name ~ ":" ~ recipe.name : recipe.name;
60 
61 	// parse general build settings
62 	parseBuildSettings(sdl, recipe.buildSettings, full_name);
63 
64 	// determine default target type for configurations
65 	auto defttype = recipe.buildSettings.targetType;
66 	if (defttype == TargetType.autodetect)
67 		defttype = TargetType.library;
68 
69 	// parse configurations
70 	recipe.configurations.length = configs.length;
71 	foreach (i, n; configs) {
72 		recipe.configurations[i].buildSettings.targetType = defttype;
73 		parseConfiguration(n, recipe.configurations[i], full_name);
74 	}
75 
76 	// finally parse all sub packages
77 	recipe.subPackages.length = subpacks.length;
78 	foreach (i, n; subpacks) {
79 		if (n.values.length) {
80 			recipe.subPackages[i].path = n.stringTagValue;
81 		} else {
82 			enforceSDL(n.attributes.length == 0, "No attributes allowed for inline sub package definitions.", n);
83 			parseSDL(recipe.subPackages[i].recipe, n, full_name);
84 		}
85 	}
86 }
87 
88 Tag toSDL(in ref PackageRecipe recipe)
89 {
90 	Tag ret = new Tag;
91 	void add(T)(string field, T value) { ret.add(new Tag(null, field, [Value(value)])); }
92 	add("name", recipe.name);
93 	if (recipe.version_.length) add("version", recipe.version_);
94 	if (recipe.description.length) add("description", recipe.description);
95 	if (recipe.homepage.length) add("homepage", recipe.homepage);
96 	if (recipe.authors.length) ret.add(new Tag(null, "authors", recipe.authors.map!(a => Value(a)).array));
97 	if (recipe.copyright.length) add("copyright", recipe.copyright);
98 	if (recipe.license.length) add("license", recipe.license);
99 	foreach (name, settings; recipe.buildTypes) {
100 		auto t = new Tag(null, "buildType", [Value(name)]);
101 		t.add(settings.toSDL());
102 		ret.add(t);
103 	}
104 	if (recipe.ddoxFilterArgs.length)
105 		ret.add(new Tag("x", "ddoxFilterArgs", recipe.ddoxFilterArgs.map!(a => Value(a)).array));
106 	if (recipe.ddoxTool.length) ret.add(new Tag("x", "ddoxTool", [Value(recipe.ddoxTool)]));
107 	ret.add(recipe.buildSettings.toSDL());
108 	foreach(config; recipe.configurations)
109 		ret.add(config.toSDL());
110 	foreach (i, subPackage; recipe.subPackages) {
111 		if (subPackage.path !is null) {
112 			add("subPackage", subPackage.path);
113 		} else {
114 			auto t = subPackage.recipe.toSDL();
115 			t.name = "subPackage";
116 			ret.add(t);
117 		}
118 	}
119 	return ret;
120 }
121 
122 private void parseBuildSettings(Tag settings, ref BuildSettingsTemplate bs, string package_name)
123 {
124 	foreach (setting; settings.tags)
125 		parseBuildSetting(setting, bs, package_name);
126 }
127 
128 private void parseBuildSetting(Tag setting, ref BuildSettingsTemplate bs, string package_name)
129 {
130 	switch (setting.fullName) {
131 		default: break;
132 		case "dependency": parseDependency(setting, bs, package_name); break;
133 		case "systemDependencies": bs.systemDependencies = setting.stringTagValue; break;
134 		case "targetType": bs.targetType = setting.stringTagValue.to!TargetType; break;
135 		case "targetName": bs.targetName = setting.stringTagValue; break;
136 		case "targetPath": bs.targetPath = setting.stringTagValue; break;
137 		case "workingDirectory": bs.workingDirectory = setting.stringTagValue; break;
138 		case "subConfiguration":
139 			auto args = setting.stringArrayTagValue;
140 			enforceSDL(args.length == 2, "Expecting package and configuration names as arguments.", setting);
141 			bs.subConfigurations[expandPackageName(args[0], package_name, setting)] = args[1];
142 			break;
143 		case "dflags": setting.parsePlatformStringArray(bs.dflags); break;
144 		case "lflags": setting.parsePlatformStringArray(bs.lflags); break;
145 		case "libs": setting.parsePlatformStringArray(bs.libs); break;
146 		case "sourceFiles": setting.parsePlatformStringArray(bs.sourceFiles); break;
147 		case "sourcePaths": setting.parsePlatformStringArray(bs.sourcePaths); break;
148 		case "excludedSourceFiles": setting.parsePlatformStringArray(bs.excludedSourceFiles); break;
149 		case "mainSourceFile": bs.mainSourceFile = setting.stringTagValue; break;
150 		case "copyFiles": setting.parsePlatformStringArray(bs.copyFiles); break;
151 		case "versions": setting.parsePlatformStringArray(bs.versions); break;
152 		case "debugVersions": setting.parsePlatformStringArray(bs.debugVersions); break;
153 		case "importPaths": setting.parsePlatformStringArray(bs.importPaths); break;
154 		case "stringImportPaths": setting.parsePlatformStringArray(bs.stringImportPaths); break;
155 		case "preGenerateCommands": setting.parsePlatformStringArray(bs.preGenerateCommands); break;
156 		case "postGenerateCommands": setting.parsePlatformStringArray(bs.postGenerateCommands); break;
157 		case "preBuildCommands": setting.parsePlatformStringArray(bs.preBuildCommands); break;
158 		case "postBuildCommands": setting.parsePlatformStringArray(bs.postBuildCommands); break;
159 		case "buildRequirements": setting.parsePlatformEnumArray!BuildRequirement(bs.buildRequirements); break;
160 		case "buildOptions": setting.parsePlatformEnumArray!BuildOption(bs.buildOptions); break;
161 	}
162 }
163 
164 private void parseDependency(Tag t, ref BuildSettingsTemplate bs, string package_name)
165 {
166 	enforceSDL(t.values.length != 0, "Missing dependency name.", t);
167 	enforceSDL(t.values.length == 1, "Multiple dependency names.", t);
168 	auto pkg = expandPackageName(t.values[0].get!string, package_name, t);
169 	enforceSDL(pkg !in bs.dependencies, "The dependency '"~pkg~"' is specified more than once.", t);
170 
171 	Dependency dep = Dependency.any;
172 	auto attrs = t.attributes;
173 
174 	auto pv = "version" in attrs;
175 
176 	if ("path" in attrs) {
177 		if ("version" in attrs)
178 			logDiagnostic("Ignoring version specification (%s) for path based dependency %s", attrs["version"][0].value.get!string, attrs["path"][0].value.get!string);
179 		dep.versionSpec = "*";
180 		dep.path = NativePath(attrs["path"][0].value.get!string);
181 	} else {
182 		enforceSDL("version" in attrs, "Missing version specification.", t);
183 		dep.versionSpec = attrs["version"][0].value.get!string;
184 	}
185 
186 	if ("optional" in attrs)
187 		dep.optional = attrs["optional"][0].value.get!bool;
188 
189 	if ("default" in attrs)
190 		dep.default_ = attrs["default"][0].value.get!bool;
191 
192 	bs.dependencies[pkg] = dep;
193 }
194 
195 private void parseConfiguration(Tag t, ref ConfigurationInfo ret, string package_name)
196 {
197 	ret.name = t.stringTagValue(true);
198 	foreach (f; t.tags) {
199 		switch (f.fullName) {
200 			default: parseBuildSetting(f, ret.buildSettings, package_name); break;
201 			case "platforms": ret.platforms ~= f.stringArrayTagValue; break;
202 		}
203 	}
204 }
205 
206 private Tag toSDL(in ref ConfigurationInfo config)
207 {
208 	auto ret = new Tag(null, "configuration", [Value(config.name)]);
209 	if (config.platforms.length) ret.add(new Tag(null, "platforms", config.platforms[].map!(p => Value(p)).array));
210 	ret.add(config.buildSettings.toSDL());
211 	return ret;
212 }
213 
214 private Tag[] toSDL(in ref BuildSettingsTemplate bs)
215 {
216 	Tag[] ret;
217 	void add(string name, string value) { ret ~= new Tag(null, name, [Value(value)]); }
218 	void adda(string name, string suffix, in string[] values) {
219 		ret ~= new Tag(null, name, values[].map!(v => Value(v)).array,
220 			suffix.length ? [new Attribute(null, "platform", Value(suffix[1 .. $]))] : null);
221 	}
222 
223 	string[] toNameArray(T, U)(U bits) if(is(T == enum)) {
224 		string[] ret;
225 		foreach (m; __traits(allMembers, T))
226 			if (bits & __traits(getMember, T, m))
227 				ret ~= m;
228 		return ret;
229 	}
230 
231 	foreach (pack, d; bs.dependencies) {
232 		Attribute[] attribs;
233 		if (!d.path.empty) attribs ~= new Attribute(null, "path", Value(d.path.toString()));
234 		else attribs ~= new Attribute(null, "version", Value(d.versionSpec));
235 		if (d.optional) attribs ~= new Attribute(null, "optional", Value(true));
236 		ret ~= new Tag(null, "dependency", [Value(pack)], attribs);
237 	}
238 	if (bs.systemDependencies !is null) add("systemDependencies", bs.systemDependencies);
239 	if (bs.targetType != TargetType.autodetect) add("targetType", bs.targetType.to!string());
240 	if (bs.targetPath.length) add("targetPath", bs.targetPath);
241 	if (bs.targetName.length) add("targetName", bs.targetName);
242 	if (bs.workingDirectory.length) add("workingDirectory", bs.workingDirectory);
243 	if (bs.mainSourceFile.length) add("mainSourceFile", bs.mainSourceFile);
244 	foreach (pack, conf; bs.subConfigurations) ret ~= new Tag(null, "subConfiguration", [Value(pack), Value(conf)]);
245 	foreach (suffix, arr; bs.dflags) adda("dflags", suffix, arr);
246 	foreach (suffix, arr; bs.lflags) adda("lflags", suffix, arr);
247 	foreach (suffix, arr; bs.libs) adda("libs", suffix, arr);
248 	foreach (suffix, arr; bs.sourceFiles) adda("sourceFiles", suffix, arr);
249 	foreach (suffix, arr; bs.sourcePaths) adda("sourcePaths", suffix, arr);
250 	foreach (suffix, arr; bs.excludedSourceFiles) adda("excludedSourceFiles", suffix, arr);
251 	foreach (suffix, arr; bs.copyFiles) adda("copyFiles", suffix, arr);
252 	foreach (suffix, arr; bs.versions) adda("versions", suffix, arr);
253 	foreach (suffix, arr; bs.debugVersions) adda("debugVersions", suffix, arr);
254 	foreach (suffix, arr; bs.importPaths) adda("importPaths", suffix, arr);
255 	foreach (suffix, arr; bs.stringImportPaths) adda("stringImportPaths", suffix, arr);
256 	foreach (suffix, arr; bs.preGenerateCommands) adda("preGenerateCommands", suffix, arr);
257 	foreach (suffix, arr; bs.postGenerateCommands) adda("postGenerateCommands", suffix, arr);
258 	foreach (suffix, arr; bs.preBuildCommands) adda("preBuildCommands", suffix, arr);
259 	foreach (suffix, arr; bs.postBuildCommands) adda("postBuildCommands", suffix, arr);
260 	foreach (suffix, bits; bs.buildRequirements) adda("buildRequirements", suffix, toNameArray!BuildRequirement(bits));
261 	foreach (suffix, bits; bs.buildOptions) adda("buildOptions", suffix, toNameArray!BuildOption(bits));
262 	return ret;
263 }
264 
265 private string expandPackageName(string name, string parent_name, Tag tag)
266 {
267 	import std.algorithm : canFind;
268 	import std.string : format;
269 	if (name.startsWith(":")) {
270 		enforceSDL(!parent_name.canFind(':'), format("Short-hand packages syntax not allowed within sub packages: %s -> %s", parent_name, name), tag);
271 		return parent_name ~ name;
272 	} else return name;
273 }
274 
275 private string stringTagValue(Tag t, bool allow_child_tags = false)
276 {
277 	import std.string : format;
278 	enforceSDL(t.values.length > 0, format("Missing string value for '%s'.", t.fullName), t);
279 	enforceSDL(t.values.length == 1, format("Expected only one value for '%s'.", t.fullName), t);
280 	enforceSDL(t.values[0].peek!string !is null, format("Expected value of type string for '%s'.", t.fullName), t);
281 	enforceSDL(allow_child_tags || t.tags.length == 0, format("No child tags allowed for '%s'.", t.fullName), t);
282 	// Q: should attributes be disallowed, or just ignored for forward compatibility reasons?
283 	//enforceSDL(t.attributes.length == 0, format("No attributes allowed for '%s'.", t.fullName), t);
284 	return t.values[0].get!string;
285 }
286 
287 private string[] stringArrayTagValue(Tag t, bool allow_child_tags = false)
288 {
289 	import std.string : format;
290 	enforceSDL(allow_child_tags || t.tags.length == 0, format("No child tags allowed for '%s'.", t.fullName), t);
291 	// Q: should attributes be disallowed, or just ignored for forward compatibility reasons?
292 	//enforceSDL(t.attributes.length == 0, format("No attributes allowed for '%s'.", t.fullName), t);
293 
294 	string[] ret;
295 	foreach (v; t.values) {
296 		enforceSDL(t.values[0].peek!string !is null, format("Values for '%s' must be strings.", t.fullName), t);
297 		ret ~= v.get!string;
298 	}
299 	return ret;
300 }
301 
302 private void parsePlatformStringArray(Tag t, ref string[][string] dst)
303 {
304 	string platform;
305 	if ("platform" in t.attributes)
306 		platform = "-" ~ t.attributes["platform"][0].value.get!string;
307 	dst[platform] ~= t.values.map!(v => v.get!string).array;
308 }
309 
310 private void parsePlatformEnumArray(E, Es)(Tag t, ref Es[string] dst)
311 {
312 	string platform;
313 	if ("platform" in t.attributes)
314 		platform = "-" ~ t.attributes["platform"][0].value.get!string;
315 	foreach (v; t.values) {
316 		if (platform !in dst) dst[platform] = Es.init;
317 		dst[platform] |= v.get!string.to!E;
318 	}
319 }
320 
321 private void enforceSDL(bool condition, lazy string message, Tag tag, string file = __FILE__, int line = __LINE__)
322 {
323 	import std.string : format;
324 	if (!condition) {
325 		throw new Exception(format("%s(%s): Error: %s", tag.location.file, tag.location.line, message), file, line);
326 	}
327 }
328 
329 
330 unittest { // test all possible fields
331 	auto sdl =
332 `name "projectname";
333 description "project description";
334 homepage "http://example.com"
335 authors "author 1" "author 2"
336 authors "author 3"
337 copyright "copyright string"
338 license "license string"
339 version "1.0.0"
340 subPackage {
341 	name "subpackage1"
342 }
343 subPackage {
344 	name "subpackage2"
345 	dependency "projectname:subpackage1" version="*"
346 }
347 subPackage "pathsp3"
348 configuration "config1" {
349 	platforms "windows" "linux"
350 	targetType "library"
351 }
352 configuration "config2" {
353 	platforms "windows-x86"
354 	targetType "executable"
355 }
356 buildType "debug" {
357 	dflags "-g" "-debug"
358 }
359 buildType "release" {
360 	dflags "-release" "-O"
361 }
362 x:ddoxFilterArgs "-arg1" "-arg2"
363 x:ddoxFilterArgs "-arg3"
364 x:ddoxTool "ddoxtool"
365 
366 dependency ":subpackage1" optional=false path="."
367 dependency "somedep" version="1.0.0" optional=true
368 systemDependencies "system dependencies"
369 targetType "executable"
370 targetName "target name"
371 targetPath "target path"
372 workingDirectory "working directory"
373 subConfiguration ":subpackage2" "library"
374 buildRequirements "allowWarnings" "silenceDeprecations"
375 buildOptions "verbose" "ignoreUnknownPragmas"
376 libs "lib1" "lib2"
377 libs "lib3"
378 sourceFiles "source1" "source2"
379 sourceFiles "source3"
380 sourcePaths "sourcepath1" "sourcepath2"
381 sourcePaths "sourcepath3"
382 excludedSourceFiles "excluded1" "excluded2"
383 excludedSourceFiles "excluded3"
384 mainSourceFile "main source"
385 copyFiles "copy1" "copy2"
386 copyFiles "copy3"
387 versions "version1" "version2"
388 versions "version3"
389 debugVersions "debug1" "debug2"
390 debugVersions "debug3"
391 importPaths "import1" "import2"
392 importPaths "import3"
393 stringImportPaths "string1" "string2"
394 stringImportPaths "string3"
395 preGenerateCommands "preg1" "preg2"
396 preGenerateCommands "preg3"
397 postGenerateCommands "postg1" "postg2"
398 postGenerateCommands "postg3"
399 preBuildCommands "preb1" "preb2"
400 preBuildCommands "preb3"
401 postBuildCommands "postb1" "postb2"
402 postBuildCommands "postb3"
403 dflags "df1" "df2"
404 dflags "df3"
405 lflags "lf1" "lf2"
406 lflags "lf3"
407 `;
408 	PackageRecipe rec1;
409 	parseSDL(rec1, sdl, null, "testfile");
410 	PackageRecipe rec;
411 	parseSDL(rec, rec1.toSDL(), null); // verify that all fields are serialized properly
412 
413 	assert(rec.name == "projectname");
414 	assert(rec.description == "project description");
415 	assert(rec.homepage == "http://example.com");
416 	assert(rec.authors == ["author 1", "author 2", "author 3"]);
417 	assert(rec.copyright == "copyright string");
418 	assert(rec.license == "license string");
419 	assert(rec.version_ == "1.0.0");
420 	assert(rec.subPackages.length == 3);
421 	assert(rec.subPackages[0].path == "");
422 	assert(rec.subPackages[0].recipe.name == "subpackage1");
423 	assert(rec.subPackages[1].path == "");
424 	assert(rec.subPackages[1].recipe.name == "subpackage2");
425 	assert(rec.subPackages[1].recipe.buildSettings.dependencies.length == 1);
426 	assert("projectname:subpackage1" in rec.subPackages[1].recipe.buildSettings.dependencies);
427 	assert(rec.subPackages[2].path == "pathsp3");
428 	assert(rec.configurations.length == 2);
429 	assert(rec.configurations[0].name == "config1");
430 	assert(rec.configurations[0].platforms == ["windows", "linux"]);
431 	assert(rec.configurations[0].buildSettings.targetType == TargetType.library);
432 	assert(rec.configurations[1].name == "config2");
433 	assert(rec.configurations[1].platforms == ["windows-x86"]);
434 	assert(rec.configurations[1].buildSettings.targetType == TargetType.executable);
435 	assert(rec.buildTypes.length == 2);
436 	assert(rec.buildTypes["debug"].dflags == ["": ["-g", "-debug"]]);
437 	assert(rec.buildTypes["release"].dflags == ["": ["-release", "-O"]]);
438 	assert(rec.ddoxFilterArgs == ["-arg1", "-arg2", "-arg3"], rec.ddoxFilterArgs.to!string);
439 	assert(rec.ddoxTool == "ddoxtool");
440 	assert(rec.buildSettings.dependencies.length == 2);
441 	assert(rec.buildSettings.dependencies["projectname:subpackage1"].optional == false);
442 	assert(rec.buildSettings.dependencies["projectname:subpackage1"].path == NativePath("."));
443 	assert(rec.buildSettings.dependencies["somedep"].versionSpec == "1.0.0");
444 	assert(rec.buildSettings.dependencies["somedep"].optional == true);
445 	assert(rec.buildSettings.dependencies["somedep"].path.empty);
446 	assert(rec.buildSettings.systemDependencies == "system dependencies");
447 	assert(rec.buildSettings.targetType == TargetType.executable);
448 	assert(rec.buildSettings.targetName == "target name");
449 	assert(rec.buildSettings.targetPath == "target path");
450 	assert(rec.buildSettings.workingDirectory == "working directory");
451 	assert(rec.buildSettings.subConfigurations.length == 1);
452 	assert(rec.buildSettings.subConfigurations["projectname:subpackage2"] == "library");
453 	assert(rec.buildSettings.buildRequirements == ["": cast(BuildRequirements)(BuildRequirement.allowWarnings | BuildRequirement.silenceDeprecations)]);
454 	assert(rec.buildSettings.buildOptions == ["": cast(BuildOptions)(BuildOption.verbose | BuildOption.ignoreUnknownPragmas)]);
455 	assert(rec.buildSettings.libs == ["": ["lib1", "lib2", "lib3"]]);
456 	assert(rec.buildSettings.sourceFiles == ["": ["source1", "source2", "source3"]]);
457 	assert(rec.buildSettings.sourcePaths == ["": ["sourcepath1", "sourcepath2", "sourcepath3"]]);
458 	assert(rec.buildSettings.excludedSourceFiles == ["": ["excluded1", "excluded2", "excluded3"]]);
459 	assert(rec.buildSettings.mainSourceFile == "main source");
460 	assert(rec.buildSettings.copyFiles == ["": ["copy1", "copy2", "copy3"]]);
461 	assert(rec.buildSettings.versions == ["": ["version1", "version2", "version3"]]);
462 	assert(rec.buildSettings.debugVersions == ["": ["debug1", "debug2", "debug3"]]);
463 	assert(rec.buildSettings.importPaths == ["": ["import1", "import2", "import3"]]);
464 	assert(rec.buildSettings.stringImportPaths == ["": ["string1", "string2", "string3"]]);
465 	assert(rec.buildSettings.preGenerateCommands == ["": ["preg1", "preg2", "preg3"]]);
466 	assert(rec.buildSettings.postGenerateCommands == ["": ["postg1", "postg2", "postg3"]]);
467 	assert(rec.buildSettings.preBuildCommands == ["": ["preb1", "preb2", "preb3"]]);
468 	assert(rec.buildSettings.postBuildCommands == ["": ["postb1", "postb2", "postb3"]]);
469 	assert(rec.buildSettings.dflags == ["": ["df1", "df2", "df3"]]);
470 	assert(rec.buildSettings.lflags == ["": ["lf1", "lf2", "lf3"]]);
471 }
472 
473 unittest { // test platform identifiers
474 	auto sdl =
475 `name "testproject"
476 dflags "-a" "-b" platform="windows-x86"
477 dflags "-c" platform="windows-x86"
478 dflags "-e" "-f"
479 dflags "-g"
480 dflags "-h" "-i" platform="linux"
481 dflags "-j" platform="linux"
482 `;
483 	PackageRecipe rec;
484 	parseSDL(rec, sdl, null, "testfile");
485 	assert(rec.buildSettings.dflags.length == 3);
486 	assert(rec.buildSettings.dflags["-windows-x86"] == ["-a", "-b", "-c"]);
487 	assert(rec.buildSettings.dflags[""] == ["-e", "-f", "-g"]);
488 	assert(rec.buildSettings.dflags["-linux"] == ["-h", "-i", "-j"]);
489 }
490 
491 unittest { // test for missing name field
492 	import std.exception;
493 	auto sdl = `description "missing name"`;
494 	PackageRecipe rec;
495 	assertThrown(parseSDL(rec, sdl, null, "testfile"));
496 }
497 
498 unittest { // test single value fields
499 	import std.exception;
500 	PackageRecipe rec;
501 	assertThrown!Exception(parseSDL(rec, `name "hello" "world"`, null, "testfile"));
502 	assertThrown!Exception(parseSDL(rec, `name`, null, "testfile"));
503 	assertThrown!Exception(parseSDL(rec, `name 10`, null, "testfile"));
504 	assertThrown!Exception(parseSDL(rec,
505 		`name "hello" {
506 			world
507 		}`, null, "testfile"));
508 	assertThrown!Exception(parseSDL(rec,
509 		`name ""
510 		versions "hello" 10`
511 		, null, "testfile"));
512 }
513 
514 unittest { // test basic serialization
515 	PackageRecipe p;
516 	p.name = "test";
517 	p.authors = ["foo", "bar"];
518 	p.buildSettings.dflags["-windows"] = ["-a"];
519 	p.buildSettings.lflags[""] = ["-b", "-c"];
520 	auto sdl = toSDL(p).toSDLDocument();
521 	assert(sdl ==
522 `name "test"
523 authors "foo" "bar"
524 dflags "-a" platform="windows"
525 lflags "-b" "-c"
526 `);
527 }
528 
529 unittest {
530 	auto sdl = "name \"test\"\nsourcePaths";
531 	PackageRecipe rec;
532 	parseSDL(rec, sdl, null, "testfile");
533 	assert("" in rec.buildSettings.sourcePaths);
534 }