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(const scope 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 	if ("path" in attrs) {
186 		if ("version" in attrs)
187 			logDiagnostic("Ignoring version specification (%s) for path based dependency %s", attrs["version"][0].value.get!string, attrs["path"][0].value.get!string);
188 		dep.versionSpec = "*";
189 		dep.path = NativePath(attrs["path"][0].value.get!string);
190 	} else if ("repository" in attrs) {
191 		enforceSDL("version" in attrs, "Missing version specification.", t);
192 
193 		dep.repository = Repository(attrs["repository"][0].value.get!string);
194 		dep.versionSpec = attrs["version"][0].value.get!string;
195 	} else {
196 		enforceSDL("version" in attrs, "Missing version specification.", t);
197 		dep.versionSpec = attrs["version"][0].value.get!string;
198 	}
199 
200 	if ("optional" in attrs)
201 		dep.optional = attrs["optional"][0].value.get!bool;
202 
203 	if ("default" in attrs)
204 		dep.default_ = attrs["default"][0].value.get!bool;
205 
206 	bs.dependencies[pkg] = dep;
207 
208 	BuildSettingsTemplate dbs;
209 	parseBuildSettings(t, dbs, package_name);
210 	bs.dependencyBuildSettings[pkg] = dbs;
211 }
212 
213 private void parseConfiguration(Tag t, ref ConfigurationInfo ret, string package_name)
214 {
215 	ret.name = t.stringTagValue(true);
216 	foreach (f; t.tags) {
217 		switch (f.fullName) {
218 			default: parseBuildSetting(f, ret.buildSettings, package_name); break;
219 			case "platforms": ret.platforms ~= f.stringArrayTagValue; break;
220 		}
221 	}
222 }
223 
224 private Tag toSDL(const scope ref ConfigurationInfo config)
225 {
226 	auto ret = new Tag(null, "configuration", [Value(config.name)]);
227 	if (config.platforms.length) ret.add(new Tag(null, "platforms", config.platforms[].map!(p => Value(p)).array));
228 	ret.add(config.buildSettings.toSDL());
229 	return ret;
230 }
231 
232 private Tag[] toSDL(const scope ref BuildSettingsTemplate bs)
233 {
234 	Tag[] ret;
235 	void add(string name, string value, string namespace = null) { ret ~= new Tag(namespace, name, [Value(value)]); }
236 	void adda(string name, string suffix, in string[] values, string namespace = null) {
237 		ret ~= new Tag(namespace, name, values[].map!(v => Value(v)).array,
238 			suffix.length ? [new Attribute(null, "platform", Value(suffix[1 .. $]))] : null);
239 	}
240 
241 	string[] toNameArray(T, U)(U bits) if(is(T == enum)) {
242 		string[] ret;
243 		foreach (m; __traits(allMembers, T))
244 			if (bits & __traits(getMember, T, m))
245 				ret ~= m;
246 		return ret;
247 	}
248 
249 	foreach (pack, d; bs.dependencies) {
250 		Attribute[] attribs;
251 		if (!d.repository.empty) attribs ~= new Attribute(null, "repository", Value(d.repository.toString()));
252 		if (!d.path.empty) attribs ~= new Attribute(null, "path", Value(d.path.toString()));
253 		else attribs ~= new Attribute(null, "version", Value(d.versionSpec));
254 		if (d.optional) attribs ~= new Attribute(null, "optional", Value(true));
255 		auto t = new Tag(null, "dependency", [Value(pack)], attribs);
256 		if (pack in bs.dependencyBuildSettings)
257 			t.add(bs.dependencyBuildSettings[pack].toSDL());
258 		ret ~= t;
259 	}
260 	if (bs.systemDependencies !is null) add("systemDependencies", bs.systemDependencies);
261 	if (bs.targetType != TargetType.autodetect) add("targetType", bs.targetType.to!string());
262 	if (bs.targetPath.length) add("targetPath", bs.targetPath);
263 	if (bs.targetName.length) add("targetName", bs.targetName);
264 	if (bs.workingDirectory.length) add("workingDirectory", bs.workingDirectory);
265 	if (bs.mainSourceFile.length) add("mainSourceFile", bs.mainSourceFile);
266 	foreach (pack, conf; bs.subConfigurations) ret ~= new Tag(null, "subConfiguration", [Value(pack), Value(conf)]);
267 	foreach (suffix, arr; bs.dflags) adda("dflags", suffix, arr);
268 	foreach (suffix, arr; bs.lflags) adda("lflags", suffix, arr);
269 	foreach (suffix, arr; bs.libs) adda("libs", suffix, arr);
270 	foreach (suffix, arr; bs.sourceFiles) adda("sourceFiles", suffix, arr);
271 	foreach (suffix, arr; bs.sourcePaths) adda("sourcePaths", suffix, arr);
272 	foreach (suffix, arr; bs.excludedSourceFiles) adda("excludedSourceFiles", suffix, arr);
273 	foreach (suffix, arr; bs.copyFiles) adda("copyFiles", suffix, arr);
274 	foreach (suffix, arr; bs.extraDependencyFiles) adda("extraDependencyFiles", suffix, arr);
275 	foreach (suffix, arr; bs.versions) adda("versions", suffix, arr);
276 	foreach (suffix, arr; bs.debugVersions) adda("debugVersions", suffix, arr);
277 	foreach (suffix, arr; bs.versionFilters) adda("versionFilters", suffix, arr, "x");
278 	foreach (suffix, arr; bs.debugVersionFilters) adda("debugVersionFilters", suffix, arr, "x");
279 	foreach (suffix, arr; bs.importPaths) adda("importPaths", suffix, arr);
280 	foreach (suffix, arr; bs.stringImportPaths) adda("stringImportPaths", suffix, arr);
281 	foreach (suffix, arr; bs.preGenerateCommands) adda("preGenerateCommands", suffix, arr);
282 	foreach (suffix, arr; bs.postGenerateCommands) adda("postGenerateCommands", suffix, arr);
283 	foreach (suffix, arr; bs.preBuildCommands) adda("preBuildCommands", suffix, arr);
284 	foreach (suffix, arr; bs.postBuildCommands) adda("postBuildCommands", suffix, arr);
285 	foreach (suffix, arr; bs.preRunCommands) adda("preRunCommands", suffix, arr);
286 	foreach (suffix, arr; bs.postRunCommands) adda("postRunCommands", suffix, arr);
287 	foreach (suffix, bits; bs.buildRequirements) adda("buildRequirements", suffix, toNameArray!BuildRequirement(bits));
288 	foreach (suffix, bits; bs.buildOptions) adda("buildOptions", suffix, toNameArray!BuildOption(bits));
289 	return ret;
290 }
291 
292 private void parseToolchainRequirements(ref ToolchainRequirements tr, Tag tag)
293 {
294 	foreach (attr; tag.attributes)
295 		tr.addRequirement(attr.name, attr.value.get!string);
296 }
297 
298 private Tag toSDL(const ref ToolchainRequirements tr)
299 {
300 	Attribute[] attrs;
301 	if (tr.dub != Dependency.any) attrs ~= new Attribute("dub", Value(tr.dub.toString()));
302 	if (tr.frontend != Dependency.any) attrs ~= new Attribute("frontend", Value(tr.frontend.toString()));
303 	if (tr.dmd != Dependency.any) attrs ~= new Attribute("dmd", Value(tr.dmd.toString()));
304 	if (tr.ldc != Dependency.any) attrs ~= new Attribute("ldc", Value(tr.ldc.toString()));
305 	if (tr.gdc != Dependency.any) attrs ~= new Attribute("gdc", Value(tr.gdc.toString()));
306 	return new Tag(null, "toolchainRequirements", null, attrs);
307 }
308 
309 private string expandPackageName(string name, string parent_name, Tag tag)
310 {
311 	import std.algorithm : canFind;
312 	import std.string : format;
313 	if (name.startsWith(":")) {
314 		enforceSDL(!parent_name.canFind(':'), format("Short-hand packages syntax not allowed within sub packages: %s -> %s", parent_name, name), tag);
315 		return parent_name ~ name;
316 	} else return name;
317 }
318 
319 private string stringTagValue(Tag t, bool allow_child_tags = false)
320 {
321 	import std.string : format;
322 	enforceSDL(t.values.length > 0, format("Missing string value for '%s'.", t.fullName), t);
323 	enforceSDL(t.values.length == 1, format("Expected only one value for '%s'.", t.fullName), t);
324 	enforceSDL(t.values[0].peek!string !is null, format("Expected value of type string for '%s'.", t.fullName), t);
325 	enforceSDL(allow_child_tags || t.tags.length == 0, format("No child tags allowed for '%s'.", t.fullName), t);
326 	// Q: should attributes be disallowed, or just ignored for forward compatibility reasons?
327 	//enforceSDL(t.attributes.length == 0, format("No attributes allowed for '%s'.", t.fullName), t);
328 	return t.values[0].get!string;
329 }
330 
331 private string[] stringArrayTagValue(Tag t, bool allow_child_tags = false)
332 {
333 	import std.string : format;
334 	enforceSDL(allow_child_tags || t.tags.length == 0, format("No child tags allowed for '%s'.", t.fullName), t);
335 	// Q: should attributes be disallowed, or just ignored for forward compatibility reasons?
336 	//enforceSDL(t.attributes.length == 0, format("No attributes allowed for '%s'.", t.fullName), t);
337 
338 	string[] ret;
339 	foreach (v; t.values) {
340 		enforceSDL(t.values[0].peek!string !is null, format("Values for '%s' must be strings.", t.fullName), t);
341 		ret ~= v.get!string;
342 	}
343 	return ret;
344 }
345 
346 private void parsePlatformStringArray(Tag t, ref string[][string] dst)
347 {
348 	string platform;
349 	if ("platform" in t.attributes)
350 		platform = "-" ~ t.attributes["platform"][0].value.get!string;
351 	dst[platform] ~= t.values.map!(v => v.get!string).array;
352 }
353 
354 private void parsePlatformEnumArray(E, Es)(Tag t, ref Es[string] dst)
355 {
356 	string platform;
357 	if ("platform" in t.attributes)
358 		platform = "-" ~ t.attributes["platform"][0].value.get!string;
359 	foreach (v; t.values) {
360 		if (platform !in dst) dst[platform] = Es.init;
361 		dst[platform] |= v.get!string.to!E;
362 	}
363 }
364 
365 private void enforceSDL(bool condition, lazy string message, Tag tag, string file = __FILE__, int line = __LINE__)
366 {
367 	import std.string : format;
368 	if (!condition) {
369 		throw new Exception(format("%s(%s): Error: %s", tag.location.file, tag.location.line + 1, message), file, line);
370 	}
371 }
372 
373 
374 unittest { // test all possible fields
375 	auto sdl =
376 `name "projectname";
377 description "project description";
378 homepage "http://example.com"
379 authors "author 1" "author 2"
380 authors "author 3"
381 copyright "copyright string"
382 license "license string"
383 version "1.0.0"
384 subPackage {
385 	name "subpackage1"
386 }
387 subPackage {
388 	name "subpackage2"
389 	dependency "projectname:subpackage1" version="*"
390 }
391 subPackage "pathsp3"
392 configuration "config1" {
393 	platforms "windows" "linux"
394 	targetType "library"
395 }
396 configuration "config2" {
397 	platforms "windows-x86"
398 	targetType "executable"
399 }
400 buildType "debug" {
401 	dflags "-g" "-debug"
402 }
403 buildType "release" {
404 	dflags "-release" "-O"
405 }
406 toolchainRequirements dub="~>1.11.0" dmd="~>2.082"
407 x:ddoxFilterArgs "-arg1" "-arg2"
408 x:ddoxFilterArgs "-arg3"
409 x:ddoxTool "ddoxtool"
410 
411 dependency ":subpackage1" optional=false path="." {
412 	dflags "-g" "-debug"
413 }
414 dependency "somedep" version="1.0.0" optional=true
415 systemDependencies "system dependencies"
416 targetType "executable"
417 targetName "target name"
418 targetPath "target path"
419 workingDirectory "working directory"
420 subConfiguration ":subpackage2" "library"
421 buildRequirements "allowWarnings" "silenceDeprecations"
422 buildOptions "verbose" "ignoreUnknownPragmas"
423 libs "lib1" "lib2"
424 libs "lib3"
425 sourceFiles "source1" "source2"
426 sourceFiles "source3"
427 sourcePaths "sourcepath1" "sourcepath2"
428 sourcePaths "sourcepath3"
429 excludedSourceFiles "excluded1" "excluded2"
430 excludedSourceFiles "excluded3"
431 mainSourceFile "main source"
432 copyFiles "copy1" "copy2"
433 copyFiles "copy3"
434 extraDependencyFiles "extradepfile1" "extradepfile2"
435 extraDependencyFiles "extradepfile3"
436 versions "version1" "version2"
437 versions "version3"
438 debugVersions "debug1" "debug2"
439 debugVersions "debug3"
440 x:versionFilters "version1" "version2"
441 x:versionFilters "version3"
442 x:versionFilters
443 x:debugVersionFilters "debug1" "debug2"
444 x:debugVersionFilters "debug3"
445 x:debugVersionFilters
446 importPaths "import1" "import2"
447 importPaths "import3"
448 stringImportPaths "string1" "string2"
449 stringImportPaths "string3"
450 preGenerateCommands "preg1" "preg2"
451 preGenerateCommands "preg3"
452 postGenerateCommands "postg1" "postg2"
453 postGenerateCommands "postg3"
454 preBuildCommands "preb1" "preb2"
455 preBuildCommands "preb3"
456 postBuildCommands "postb1" "postb2"
457 postBuildCommands "postb3"
458 preRunCommands "prer1" "prer2"
459 preRunCommands "prer3"
460 postRunCommands "postr1" "postr2"
461 postRunCommands "postr3"
462 dflags "df1" "df2"
463 dflags "df3"
464 lflags "lf1" "lf2"
465 lflags "lf3"
466 `;
467 	PackageRecipe rec1;
468 	parseSDL(rec1, sdl, null, "testfile");
469 	PackageRecipe rec;
470 	parseSDL(rec, rec1.toSDL(), null); // verify that all fields are serialized properly
471 
472 	assert(rec.name == "projectname");
473 	assert(rec.description == "project description");
474 	assert(rec.homepage == "http://example.com");
475 	assert(rec.authors == ["author 1", "author 2", "author 3"]);
476 	assert(rec.copyright == "copyright string");
477 	assert(rec.license == "license string");
478 	assert(rec.version_ == "1.0.0");
479 	assert(rec.subPackages.length == 3);
480 	assert(rec.subPackages[0].path == "");
481 	assert(rec.subPackages[0].recipe.name == "subpackage1");
482 	assert(rec.subPackages[1].path == "");
483 	assert(rec.subPackages[1].recipe.name == "subpackage2");
484 	assert(rec.subPackages[1].recipe.buildSettings.dependencies.length == 1);
485 	assert("projectname:subpackage1" in rec.subPackages[1].recipe.buildSettings.dependencies);
486 	assert(rec.subPackages[2].path == "pathsp3");
487 	assert(rec.configurations.length == 2);
488 	assert(rec.configurations[0].name == "config1");
489 	assert(rec.configurations[0].platforms == ["windows", "linux"]);
490 	assert(rec.configurations[0].buildSettings.targetType == TargetType.library);
491 	assert(rec.configurations[1].name == "config2");
492 	assert(rec.configurations[1].platforms == ["windows-x86"]);
493 	assert(rec.configurations[1].buildSettings.targetType == TargetType.executable);
494 	assert(rec.buildTypes.length == 2);
495 	assert(rec.buildTypes["debug"].dflags == ["": ["-g", "-debug"]]);
496 	assert(rec.buildTypes["release"].dflags == ["": ["-release", "-O"]]);
497 	assert(rec.toolchainRequirements.dub == Dependency("~>1.11.0"));
498 	assert(rec.toolchainRequirements.frontend == Dependency.any);
499 	assert(rec.toolchainRequirements.dmd == Dependency("~>2.82.0"));
500 	assert(rec.toolchainRequirements.ldc == Dependency.any);
501 	assert(rec.toolchainRequirements.gdc == Dependency.any);
502 	assert(rec.ddoxFilterArgs == ["-arg1", "-arg2", "-arg3"], rec.ddoxFilterArgs.to!string);
503 	assert(rec.ddoxTool == "ddoxtool");
504 	assert(rec.buildSettings.dependencies.length == 2);
505 	assert(rec.buildSettings.dependencies["projectname:subpackage1"].optional == false);
506 	assert(rec.buildSettings.dependencies["projectname:subpackage1"].path == NativePath("."));
507 	assert(rec.buildSettings.dependencyBuildSettings["projectname:subpackage1"].dflags == ["":["-g", "-debug"]]);
508 	assert(rec.buildSettings.dependencies["somedep"].versionSpec == "1.0.0");
509 	assert(rec.buildSettings.dependencies["somedep"].optional == true);
510 	assert(rec.buildSettings.dependencies["somedep"].path.empty);
511 	assert(rec.buildSettings.systemDependencies == "system dependencies");
512 	assert(rec.buildSettings.targetType == TargetType.executable);
513 	assert(rec.buildSettings.targetName == "target name");
514 	assert(rec.buildSettings.targetPath == "target path");
515 	assert(rec.buildSettings.workingDirectory == "working directory");
516 	assert(rec.buildSettings.subConfigurations.length == 1);
517 	assert(rec.buildSettings.subConfigurations["projectname:subpackage2"] == "library");
518 	assert(rec.buildSettings.buildRequirements == ["": cast(BuildRequirements)(BuildRequirement.allowWarnings | BuildRequirement.silenceDeprecations)]);
519 	assert(rec.buildSettings.buildOptions == ["": cast(BuildOptions)(BuildOption.verbose | BuildOption.ignoreUnknownPragmas)]);
520 	assert(rec.buildSettings.libs == ["": ["lib1", "lib2", "lib3"]]);
521 	assert(rec.buildSettings.sourceFiles == ["": ["source1", "source2", "source3"]]);
522 	assert(rec.buildSettings.sourcePaths == ["": ["sourcepath1", "sourcepath2", "sourcepath3"]]);
523 	assert(rec.buildSettings.excludedSourceFiles == ["": ["excluded1", "excluded2", "excluded3"]]);
524 	assert(rec.buildSettings.mainSourceFile == "main source");
525 	assert(rec.buildSettings.copyFiles == ["": ["copy1", "copy2", "copy3"]]);
526 	assert(rec.buildSettings.extraDependencyFiles == ["": ["extradepfile1", "extradepfile2", "extradepfile3"]]);
527 	assert(rec.buildSettings.versions == ["": ["version1", "version2", "version3"]]);
528 	assert(rec.buildSettings.debugVersions == ["": ["debug1", "debug2", "debug3"]]);
529 	assert(rec.buildSettings.versionFilters == ["": ["version1", "version2", "version3"]]);
530 	assert(rec.buildSettings.debugVersionFilters == ["": ["debug1", "debug2", "debug3"]]);
531 	assert(rec.buildSettings.importPaths == ["": ["import1", "import2", "import3"]]);
532 	assert(rec.buildSettings.stringImportPaths == ["": ["string1", "string2", "string3"]]);
533 	assert(rec.buildSettings.preGenerateCommands == ["": ["preg1", "preg2", "preg3"]]);
534 	assert(rec.buildSettings.postGenerateCommands == ["": ["postg1", "postg2", "postg3"]]);
535 	assert(rec.buildSettings.preBuildCommands == ["": ["preb1", "preb2", "preb3"]]);
536 	assert(rec.buildSettings.postBuildCommands == ["": ["postb1", "postb2", "postb3"]]);
537 	assert(rec.buildSettings.preRunCommands == ["": ["prer1", "prer2", "prer3"]]);
538 	assert(rec.buildSettings.postRunCommands == ["": ["postr1", "postr2", "postr3"]]);
539 	assert(rec.buildSettings.dflags == ["": ["df1", "df2", "df3"]]);
540 	assert(rec.buildSettings.lflags == ["": ["lf1", "lf2", "lf3"]]);
541 }
542 
543 unittest { // test platform identifiers
544 	auto sdl =
545 `name "testproject"
546 dflags "-a" "-b" platform="windows-x86"
547 dflags "-c" platform="windows-x86"
548 dflags "-e" "-f"
549 dflags "-g"
550 dflags "-h" "-i" platform="linux"
551 dflags "-j" platform="linux"
552 `;
553 	PackageRecipe rec;
554 	parseSDL(rec, sdl, null, "testfile");
555 	assert(rec.buildSettings.dflags.length == 3);
556 	assert(rec.buildSettings.dflags["-windows-x86"] == ["-a", "-b", "-c"]);
557 	assert(rec.buildSettings.dflags[""] == ["-e", "-f", "-g"]);
558 	assert(rec.buildSettings.dflags["-linux"] == ["-h", "-i", "-j"]);
559 }
560 
561 unittest { // test for missing name field
562 	import std.exception;
563 	auto sdl = `description "missing name"`;
564 	PackageRecipe rec;
565 	assertThrown(parseSDL(rec, sdl, null, "testfile"));
566 }
567 
568 unittest { // test single value fields
569 	import std.exception;
570 	PackageRecipe rec;
571 	assertThrown!Exception(parseSDL(rec, `name "hello" "world"`, null, "testfile"));
572 	assertThrown!Exception(parseSDL(rec, `name`, null, "testfile"));
573 	assertThrown!Exception(parseSDL(rec, `name 10`, null, "testfile"));
574 	assertThrown!Exception(parseSDL(rec,
575 		`name "hello" {
576 			world
577 		}`, null, "testfile"));
578 	assertThrown!Exception(parseSDL(rec,
579 		`name ""
580 		versions "hello" 10`
581 		, null, "testfile"));
582 }
583 
584 unittest { // test basic serialization
585 	PackageRecipe p;
586 	p.name = "test";
587 	p.authors = ["foo", "bar"];
588 	p.buildSettings.dflags["-windows"] = ["-a"];
589 	p.buildSettings.lflags[""] = ["-b", "-c"];
590 	auto sdl = toSDL(p).toSDLDocument();
591 	assert(sdl ==
592 `name "test"
593 authors "foo" "bar"
594 dflags "-a" platform="windows"
595 lflags "-b" "-c"
596 `);
597 }
598 
599 unittest {
600 	auto sdl = "name \"test\"\nsourcePaths";
601 	PackageRecipe rec;
602 	parseSDL(rec, sdl, null, "testfile");
603 	assert("" in rec.buildSettings.sourcePaths);
604 }
605 
606 unittest {
607 	auto sdl =
608 `name "test"
609 dependency "package" repository="git+https://some.url" version="12345678"
610 `;
611 	PackageRecipe rec;
612 	parseSDL(rec, sdl, null, "testfile");
613 	auto dependency = rec.buildSettings.dependencies["package"];
614 	assert(!dependency.repository.empty);
615 	assert(dependency.versionSpec == "12345678");
616 }
617 
618 unittest {
619 	PackageRecipe p;
620 	p.name = "test";
621 
622 	auto repository = Repository("git+https://some.url");
623 	p.buildSettings.dependencies["package"] = Dependency(repository, "12345678");
624 	auto sdl = toSDL(p).toSDLDocument();
625 	assert(sdl ==
626 `name "test"
627 dependency "package" repository="git+https://some.url" version="12345678"
628 `);
629 }