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