1 /**
2 	Stuff with dependencies.
3 
4 	Copyright: © 2012-2013 Matthias Dondorff
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Matthias Dondorff
7 */
8 module dub.package_;
9 
10 import dub.compilers.compiler;
11 import dub.dependency;
12 import dub.internal.utils;
13 import dub.internal.vibecompat.core.log;
14 import dub.internal.vibecompat.core.file;
15 import dub.internal.vibecompat.data.json;
16 import dub.internal.vibecompat.inet.url;
17 
18 import std.algorithm;
19 import std.array;
20 import std.conv;
21 import std.exception;
22 import std.file;
23 import std.range;
24 import std..string;
25 import std.traits : EnumMembers;
26 
27 
28 // Supported package descriptions in decreasing order of preference.
29 enum packageInfoFilenames = ["dub.json", /*"dub.sdl",*/ "package.json"];
30 string defaultPackageFilename() {
31 	return packageInfoFilenames[0];
32 }
33 
34 /**
35 	Represents a package, including its sub packages
36 
37 	Documentation of the dub.json can be found at 
38 	http://registry.vibed.org/package-format
39 */
40 class Package {
41 	static struct LocalPackageDef { string name; Version version_; Path path; }
42 
43 	private {
44 		Path m_path;
45 		Path m_infoFile;
46 		PackageInfo m_info;
47 		Package m_parentPackage;
48 		Package[] m_subPackages;
49 		Path[] m_exportedPackages;
50 	}
51 
52 	static bool isPackageAt(Path path)
53 	{
54 		foreach (f; packageInfoFilenames)
55 			if (existsFile(path ~ f))
56 				return true;
57 		return false;
58 	}
59 
60 	this(Path root, Package parent = null)
61 	{
62 		Json info;
63 		try {
64 			foreach (f; packageInfoFilenames) {
65 				auto name = root ~ f;
66 				if (existsFile(name)) {
67 					m_infoFile = name;
68 					info = jsonFromFile(m_infoFile);
69 					break;
70 				}
71 			}
72 		} catch (Exception ex) throw new Exception(format("Failed to load package at %s: %s", root.toNativeString(), ex.msg));
73 
74 		enforce(info != Json.undefined, format("Missing package description for package at %s", root.toNativeString()));
75 
76 		this(info, root, parent);
77 	}
78 
79 	this(Json packageInfo, Path root = Path(), Package parent = null)
80 	{
81 		m_parentPackage = parent;
82 		m_path = root;
83 
84 		// check for default string import folders
85 		foreach(defvf; ["views"]){
86 			auto p = m_path ~ defvf;
87 			if( existsFile(p) )
88 				m_info.buildSettings.stringImportPaths[""] ~= defvf;
89 		}
90 
91 		string app_main_file;
92 		auto pkg_name = packageInfo.name.get!string();
93 
94 		// check for default source folders
95 		foreach(defsf; ["source/", "src/"]){
96 			auto p = m_path ~ defsf;
97 			if( existsFile(p) ){
98 				m_info.buildSettings.sourcePaths[""] ~= defsf;
99 				m_info.buildSettings.importPaths[""] ~= defsf;
100 				foreach (fil; ["app.d", "main.d", pkg_name ~ "/main.d", pkg_name ~ "/" ~ "app.d"])
101 					if (existsFile(p ~ fil)) {
102 						app_main_file = Path(defsf ~ fil).toNativeString();
103 						break;
104 					}
105 			}
106 		}
107 
108 		// parse the JSON description
109 		{
110 			scope(failure) logError("Failed to parse package description in %s", root.toNativeString());
111 			m_info.parseJson(packageInfo);
112 
113 			// try to run git to determine the version of the package if no explicit version was given
114 			if (m_info.version_.length == 0 && !parent) {
115 				import std.process;
116 				try {
117 					auto branch = execute(["git", "--git-dir="~(root~".git").toNativeString(), "rev-parse", "--abbrev-ref", "HEAD"]);
118 					enforce(branch.status == 0, "git rev-parse failed: " ~ branch.output);
119 					if (branch.output.strip() == "HEAD") {
120 						//auto ver = execute("git",)
121 						enforce(false, "oops");
122 					} else {
123 						m_info.version_ = "~" ~ branch.output.strip();
124 					}
125 				} catch (Exception e) {
126 					logDebug("Failed to run git: %s", e.msg);
127 				}
128 
129 				if (m_info.version_.length == 0) {
130 					logDiagnostic("Note: Failed to determine version of package %s at %s. Assuming ~master.", m_info.name, this.path.toNativeString());
131 					m_info.version_ = "~master";
132 				} else logDiagnostic("Determined package version using GIT: %s %s", m_info.name, m_info.version_);
133 			}
134 		}
135 
136 		// generate default configurations if none are defined
137 		if (m_info.configurations.length == 0) {
138 			if (m_info.buildSettings.targetType == TargetType.executable) {
139 				BuildSettingsTemplate app_settings;
140 				app_settings.targetType = TargetType.executable;
141 				if (m_info.buildSettings.mainSourceFile.empty) app_settings.mainSourceFile = app_main_file;
142 				m_info.configurations ~= ConfigurationInfo("application", app_settings);
143 			} else if (m_info.buildSettings.targetType != TargetType.none) {
144 				BuildSettingsTemplate lib_settings;
145 				lib_settings.targetType = m_info.buildSettings.targetType == TargetType.autodetect ? TargetType.library : m_info.buildSettings.targetType;
146 
147 				if (m_info.buildSettings.targetType == TargetType.autodetect) {
148 					if (app_main_file.length) {
149 						lib_settings.excludedSourceFiles[""] ~= app_main_file;
150 
151 						BuildSettingsTemplate app_settings;
152 						app_settings.targetType = TargetType.executable;
153 						app_settings.mainSourceFile = app_main_file;
154 						m_info.configurations ~= ConfigurationInfo("application", app_settings);
155 					}
156 				}
157 
158 				m_info.configurations ~= ConfigurationInfo("library", lib_settings);
159 			}
160 		}
161 
162 		// load all sub packages defined in the package description
163 		foreach (sub; packageInfo.subPackages.opt!(Json[])) {
164 			if (m_parentPackage) {
165 				throw new Exception("'subPackages' found in '" ~ name ~ "'. This is only supported in the main package file for '" ~ m_parentPackage.name ~ "'.");
166 			}
167 			if (sub.type == Json.Type..string)  {
168 				auto p = Path(sub.get!string);
169 				p.normalize();
170 				enforce(!p.absolute, "Sub package paths must not be absolute: " ~ sub.get!string);
171 				enforce(!p.startsWith(Path("..")), "Sub packages must be in a sub directory, not " ~ sub.get!string);
172 				m_exportedPackages ~= p;
173 			} else {
174 				m_subPackages ~= new Package(sub, root, this);
175 			}
176 		}
177 
178 		simpleLint();
179 	}
180 	
181 	@property string name()
182 	const {
183 		if (m_parentPackage) return m_parentPackage.name ~ ":" ~ m_info.name;
184 		else return m_info.name;
185 	}
186 	@property string vers() const { return m_parentPackage ? m_parentPackage.vers : m_info.version_; }
187 	@property Version ver() const { return Version(this.vers); }
188 	@property void ver(Version ver) { assert(m_parentPackage is null); m_info.version_ = ver.toString(); }
189 	@property ref inout(PackageInfo) info() inout { return m_info; }
190 	@property Path path() const { return m_path; }
191 	@property Path packageInfoFile() const { return m_infoFile; }
192 	@property const(Dependency[string]) dependencies() const { return m_info.dependencies; }
193 	@property inout(Package) basePackage() inout { return m_parentPackage ? m_parentPackage.basePackage : this; }
194 	@property inout(Package) parentPackage() inout { return m_parentPackage; }
195 	@property inout(Package)[] subPackages() inout { return m_subPackages; }
196 	@property inout(Path[]) exportedPackages() inout { return m_exportedPackages; }
197 
198 	@property string[] configurations()
199 	const {
200 		auto ret = appender!(string[])();
201 		foreach( ref config; m_info.configurations )
202 			ret.put(config.name);
203 		return ret.data;
204 	}
205 
206 	/** Overwrites the packge description file using the default filename with the current information.
207 	*/
208 	void storeInfo()
209 	{
210 		auto filename = m_path ~ defaultPackageFilename();
211 		auto dstFile = openFile(filename.toNativeString(), FileMode.CreateTrunc);
212 		scope(exit) dstFile.close();
213 		dstFile.writePrettyJsonString(m_info.toJson());
214 		m_infoFile = filename;
215 	}
216 
217 	inout(Package) getSubPackage(string name) inout {
218 		foreach (p; m_subPackages)
219 			if (p.name == this.name ~ ":" ~ name)
220 				return p;
221 		throw new Exception(format("Unknown sub package: %s:%s", this.name, name));
222 	}
223 
224 	void warnOnSpecialCompilerFlags()
225 	{
226 		// warn about use of special flags
227 		m_info.buildSettings.warnOnSpecialCompilerFlags(m_info.name, null);
228 		foreach (ref config; m_info.configurations)
229 			config.buildSettings.warnOnSpecialCompilerFlags(m_info.name, config.name);
230 	}
231 
232 	/// Returns all BuildSettings for the given platform and config.
233 	BuildSettings getBuildSettings(in BuildPlatform platform, string config)
234 	const {
235 		BuildSettings ret;
236 		m_info.buildSettings.getPlatformSettings(ret, platform, this.path);
237 		bool found = false;
238 		foreach(ref conf; m_info.configurations){
239 			if( conf.name != config ) continue;
240 			conf.buildSettings.getPlatformSettings(ret, platform, this.path);
241 			found = true;
242 			break;
243 		}
244 		assert(found || config is null, "Unknown configuration for "~m_info.name~": "~config);
245 
246 		// construct default target name based on package name
247 		if( ret.targetName.empty ) ret.targetName = this.name.replace(":", "_");
248 
249 		// special support for DMD style flags
250 		getCompiler("dmd").extractBuildOptions(ret);
251 
252 		return ret;
253 	}
254 
255 	void addBuildTypeSettings(ref BuildSettings settings, in BuildPlatform platform, string build_type)
256 	const {
257 		if (build_type == "$DFLAGS") {
258 			import std.process;
259 			string dflags = environment.get("DFLAGS");
260 			settings.addDFlags(dflags.split());
261 			return;
262 		}
263 
264 		if (auto pbt = build_type in m_info.buildTypes) {
265 			logDiagnostic("Using custom build type '%s'.", build_type);
266 			pbt.getPlatformSettings(settings, platform, this.path);
267 		} else {
268 			with(BuildOptions) switch (build_type) {
269 				default: throw new Exception(format("Unknown build type for %s: '%s'", this.name, build_type));
270 				case "plain": break;
271 				case "debug": settings.addOptions(debugMode, debugInfo); break;
272 				case "release": settings.addOptions(releaseMode, optimize, inline); break;
273 				case "release-nobounds": settings.addOptions(releaseMode, optimize, inline, noBoundsCheck); break;
274 				case "unittest": settings.addOptions(unittests, debugMode, debugInfo); break;
275 				case "docs": settings.addOptions(syntaxOnly); settings.addDFlags("-c", "-Dddocs"); break;
276 				case "ddox": settings.addOptions(syntaxOnly); settings.addDFlags("-c", "-Df__dummy.html", "-Xfdocs.json"); break;
277 				case "profile": settings.addOptions(profile, optimize, inline, debugInfo); break;
278 				case "cov": settings.addOptions(coverage, debugInfo); break;
279 				case "unittest-cov": settings.addOptions(unittests, coverage, debugMode, debugInfo); break;
280 			}
281 		}
282 	}
283 
284 	string getSubConfiguration(string config, in Package dependency, in BuildPlatform platform)
285 	const {
286 		bool found = false;
287 		foreach(ref c; m_info.configurations){
288 			if( c.name == config ){
289 				if( auto pv = dependency.name in c.buildSettings.subConfigurations ) return *pv;
290 				found = true;
291 				break;
292 			}
293 		}
294 		assert(found || config is null, "Invalid configuration \""~config~"\" for "~this.name);
295 		if( auto pv = dependency.name in m_info.buildSettings.subConfigurations ) return *pv;
296 		return null;
297 	}
298 
299 	/// Returns the default configuration to build for the given platform
300 	string getDefaultConfiguration(in BuildPlatform platform, bool allow_non_library = false)
301 	const {
302 		foreach (ref conf; m_info.configurations) {
303 			if (!conf.matchesPlatform(platform)) continue;
304 			if (!allow_non_library && conf.buildSettings.targetType == TargetType.executable) continue;
305 			return conf.name;
306 		}
307 		return null;
308 	}
309 
310 	/// Returns a list of configurations suitable for the given platform
311 	string[] getPlatformConfigurations(in BuildPlatform platform, bool is_main_package = false)
312 	const {
313 		auto ret = appender!(string[]);
314 		foreach(ref conf; m_info.configurations){
315 			if (!conf.matchesPlatform(platform)) continue;
316 			if (!is_main_package && conf.buildSettings.targetType == TargetType.executable) continue;
317 			ret ~= conf.name;
318 		}
319 		if (ret.data.length == 0) ret.put(null);
320 		return ret.data;
321 	}
322 
323 	/// Human readable information of this package and its dependencies.
324 	string generateInfoString() const {
325 		string s;
326 		s ~= m_info.name ~ ", version '" ~ m_info.version_ ~ "'";
327 		s ~= "\n  Dependencies:";
328 		foreach(string p, ref const Dependency v; m_info.dependencies)
329 			s ~= "\n    " ~ p ~ ", version '" ~ to!string(v) ~ "'";
330 		return s;
331 	}
332 	
333 	bool hasDependency(string depname, string config)
334 	const {
335 		if (depname in m_info.buildSettings.dependencies) return true;
336 		foreach (ref c; m_info.configurations)
337 			if ((config.empty || c.name == config) && depname in c.buildSettings.dependencies)
338 				return true;
339 		return false;
340 	}
341 
342 	void describe(ref Json dst, BuildPlatform platform, string config)
343 	{
344 		dst.path = m_path.toNativeString();
345 		dst.name = this.name;
346 		dst["version"] = this.vers;
347 		dst.description = m_info.description;
348 		dst.homepage = m_info.homepage;
349 		dst.authors = m_info.authors.serializeToJson();
350 		dst.copyright = m_info.copyright;
351 		dst.license = m_info.license;
352 		dst.dependencies = m_info.dependencies.keys.serializeToJson();
353 
354 		// save build settings
355 		BuildSettings bs = getBuildSettings(platform, config);
356 
357 		foreach (string k, v; bs.serializeToJson()) dst[k] = v;
358 		dst.remove("requirements");
359 		dst.remove("sourceFiles");
360 		dst.remove("importFiles");
361 		dst.remove("stringImportFiles");
362 		dst.targetType = bs.targetType.to!string();
363 		if (dst.targetType != TargetType.none)
364 			dst.targetFileName = getTargetFileName(bs, platform);
365 
366 		// prettify build requirements output
367 		Json[] breqs;
368 		for (int i = 1; i <= BuildRequirements.max; i <<= 1)
369 			if (bs.requirements & i)
370 				breqs ~= Json(to!string(cast(BuildRequirements)i));
371 		dst.buildRequirements = breqs;
372 
373 		// prettify options output
374 		Json[] bopts;
375 		for (int i = 1; i <= BuildOptions.max; i <<= 1)
376 			if (bs.options & i)
377 				bopts ~= Json(to!string(cast(BuildOptions)i));
378 		dst.options = bopts;
379 
380 		// prettify files output
381 		Json[] files;
382 		foreach (f; bs.sourceFiles) {
383 			auto jf = Json.emptyObject;
384 			jf.path = f;
385 			jf["type"] = "source";
386 			files ~= jf;
387 		}
388 		foreach (f; bs.importFiles) {
389 			auto jf = Json.emptyObject;
390 			jf.path = f;
391 			jf["type"] = "import";
392 			files ~= jf;
393 		}
394 		foreach (f; bs.stringImportFiles) {
395 			auto jf = Json.emptyObject;
396 			jf.path = f;
397 			jf["type"] = "stringImport";
398 			files ~= jf;
399 		}
400 		dst.files = Json(files);
401 	}
402 
403 	private void simpleLint() const {
404 		if (m_parentPackage) {
405 			if (m_parentPackage.path != path) {
406 				if (info.license != m_parentPackage.info.license) logWarn("License in subpackage %s is different than it's parent package, this is discouraged.", name);
407 			}
408 		}
409 		if (name.empty()) logWarn("The package in %s has no name.", path);
410 	}
411 }
412 
413 /// Specifying package information without any connection to a certain 
414 /// retrived package, like Package class is doing.
415 struct PackageInfo {
416 	string name;
417 	string version_;
418 	string description;
419 	string homepage;
420 	string[] authors;
421 	string copyright;
422 	string license;
423 	string[] ddoxFilterArgs;
424 	BuildSettingsTemplate buildSettings;
425 	ConfigurationInfo[] configurations;
426 	BuildSettingsTemplate[string] buildTypes;
427 	Json subPackages;
428 
429 	@property const(Dependency)[string] dependencies()
430 	const {
431 		const(Dependency)[string] ret;
432 		foreach (n, d; this.buildSettings.dependencies)
433 			ret[n] = d;
434 		foreach (ref c; configurations)
435 			foreach (n, d; c.buildSettings.dependencies)
436 				ret[n] = d;
437 		return ret;
438 	}
439 
440 	inout(ConfigurationInfo) getConfiguration(string name)
441 	inout {
442 		foreach (c; configurations)
443 			if (c.name == name)
444 				return c;
445 		throw new Exception("Unknown configuration: "~name);
446 	}
447 
448 	void parseJson(Json json)
449 	{
450 		foreach( string field, value; json ){
451 			switch(field){
452 				default: break;
453 				case "name": this.name = value.get!string; break;
454 				case "version": this.version_ = value.get!string; break;
455 				case "description": this.description = value.get!string; break;
456 				case "homepage": this.homepage = value.get!string; break;
457 				case "authors": this.authors = deserializeJson!(string[])(value); break;
458 				case "copyright": this.copyright = value.get!string; break;
459 				case "license": this.license = value.get!string; break;
460 				case "subPackages": subPackages = value;
461 				case "configurations": break; // handled below, after the global settings have been parsed
462 				case "buildTypes":
463 					foreach (string name, settings; value) {
464 						BuildSettingsTemplate bs;
465 						bs.parseJson(settings);
466 						buildTypes[name] = bs;
467 					}
468 					break;
469 				case "-ddoxFilterArgs": this.ddoxFilterArgs = deserializeJson!(string[])(value); break;
470 			}
471 		}
472 
473 		// parse build settings
474 		this.buildSettings.parseJson(json);
475 
476 		if (auto pv = "configurations" in json) {
477 			TargetType deftargettp = TargetType.library;
478 			if (this.buildSettings.targetType != TargetType.autodetect)
479 				deftargettp = this.buildSettings.targetType;
480 
481 			foreach (settings; *pv) {
482 				ConfigurationInfo ci;
483 				ci.parseJson(settings, deftargettp);
484 				this.configurations ~= ci;
485 			}
486 		}
487 
488 		enforce(this.name.length > 0, "The package \"name\" field is missing or empty.");
489 	}
490 
491 	Json toJson()
492 	const {
493 		auto ret = buildSettings.toJson();
494 		ret.name = this.name;
495 		if( !this.version_.empty ) ret["version"] = this.version_;
496 		if( !this.description.empty ) ret.description = this.description;
497 		if( !this.homepage.empty ) ret.homepage = this.homepage;
498 		if( !this.authors.empty ) ret.authors = serializeToJson(this.authors);
499 		if( !this.copyright.empty ) ret.copyright = this.copyright;
500 		if( !this.license.empty ) ret.license = this.license;
501 		if( this.subPackages.type != Json.Type.undefined ) {
502 			auto copy = this.subPackages.toString();
503 			ret.subPackages = dub.internal.vibecompat.data.json.parseJson(copy);
504 		}
505 		if( this.configurations ){
506 			Json[] configs;
507 			foreach(config; this.configurations)
508 				configs ~= config.toJson();
509 			ret.configurations = configs;
510 		}
511 		if( this.buildTypes.length ) {
512 			Json[string] types;
513 			foreach(name, settings; this.buildTypes)
514 				types[name] = settings.toJson();
515 		}
516 		if( !this.ddoxFilterArgs.empty ) ret["-ddoxFilterArgs"] = this.ddoxFilterArgs.serializeToJson();
517 		return ret;
518 	}
519 }
520 
521 /// Bundles information about a build configuration.
522 struct ConfigurationInfo {
523 	string name;
524 	string[] platforms;
525 	BuildSettingsTemplate buildSettings;
526 
527 	this(string name, BuildSettingsTemplate build_settings)
528 	{
529 		enforce(!name.empty, "Configuration name is empty.");
530 		this.name = name;
531 		this.buildSettings = build_settings;
532 	}
533 
534 	void parseJson(Json json, TargetType default_target_type = TargetType.library)
535 	{
536 		this.buildSettings.targetType = default_target_type;
537 
538 		foreach(string name, value; json){
539 			switch(name){
540 				default: break;
541 				case "name":
542 					this.name = value.get!string();
543 					enforce(!this.name.empty, "Configurations must have a non-empty name.");
544 					break;
545 				case "platforms": this.platforms = deserializeJson!(string[])(value); break;
546 			}
547 		}
548 
549 		enforce(!this.name.empty, "Configuration is missing a name.");
550 
551 		BuildSettingsTemplate bs;
552 		this.buildSettings.parseJson(json);
553 	}
554 
555 	Json toJson()
556 	const {
557 		auto ret = buildSettings.toJson();
558 		ret.name = name;
559 		if( this.platforms.length ) ret.platforms = serializeToJson(platforms);
560 		return ret;
561 	}
562 
563 	bool matchesPlatform(in BuildPlatform platform)
564 	const {
565 		if( platforms.empty ) return true;
566 		foreach(p; platforms)
567 			if( platform.matchesSpecification("-"~p) )
568 				return true;
569 		return false;
570 	}
571 }
572 
573 /// This keeps general information about how to build a package.
574 /// It contains functions to create a specific BuildSetting, targeted at
575 /// a certain BuildPlatform.
576 struct BuildSettingsTemplate {
577 	Dependency[string] dependencies;
578 	TargetType targetType = TargetType.autodetect;
579 	string targetPath;
580 	string targetName;
581 	string workingDirectory;
582 	string mainSourceFile;
583 	string[string] subConfigurations;
584 	string[][string] dflags;
585 	string[][string] lflags;
586 	string[][string] libs;
587 	string[][string] sourceFiles;
588 	string[][string] sourcePaths;
589 	string[][string] excludedSourceFiles;
590 	string[][string] copyFiles;
591 	string[][string] versions;
592 	string[][string] debugVersions;
593 	string[][string] importPaths;
594 	string[][string] stringImportPaths;
595 	string[][string] preGenerateCommands;
596 	string[][string] postGenerateCommands;
597 	string[][string] preBuildCommands;
598 	string[][string] postBuildCommands;
599 	BuildRequirements[string] buildRequirements;
600 	BuildOptions[string] buildOptions;
601 
602 	void parseJson(Json json)
603 	{
604 		foreach(string name, value; json)
605 		{
606 			auto idx = std..string.indexOf(name, "-");
607 			string basename, suffix;
608 			if( idx >= 0 ) basename = name[0 .. idx], suffix = name[idx .. $];
609 			else basename = name;
610 			switch(basename){
611 				default: break;
612 				case "dependencies":
613 					foreach( string pkg, verspec; value ) {
614 						enforce(pkg !in this.dependencies, "The dependency '"~pkg~"' is specified more than once." );
615 						Dependency dep;
616 						if( verspec.type == Json.Type.object ){
617 							enforce("version" in verspec, "Package information provided for package " ~ pkg ~ " is missing a version field.");
618 							auto ver = verspec["version"].get!string;
619 							if( auto pp = "path" in verspec ) {
620 								// This enforces the "version" specifier to be a simple version, 
621 								// without additional range specifiers.
622 								dep = Dependency(Version(ver));
623 								dep.path = Path(verspec.path.get!string());
624 							} else {
625 								// Using the string to be able to specifiy a range of versions.
626 								dep = Dependency(ver);
627 							}
628 							if( auto po = "optional" in verspec ) {
629 								dep.optional = verspec.optional.get!bool();
630 							}
631 						} else {
632 							// canonical "package-id": "version"
633 							dep = Dependency(verspec.get!string());
634 						}
635 						this.dependencies[pkg] = dep;
636 					}
637 					break;
638 				case "targetType":
639 					enforce(suffix.empty, "targetType does not support platform customization.");
640 					targetType = value.get!string().to!TargetType();
641 					break;
642 				case "targetPath":
643 					enforce(suffix.empty, "targetPath does not support platform customization.");
644 					this.targetPath = value.get!string;
645 					if (this.workingDirectory is null) this.workingDirectory = this.targetPath;
646 					break;
647 				case "targetName":
648 					enforce(suffix.empty, "targetName does not support platform customization.");
649 					this.targetName = value.get!string;
650 					break;
651 				case "workingDirectory":
652 					enforce(suffix.empty, "workingDirectory does not support platform customization.");
653 					this.workingDirectory = value.get!string;
654 					break;
655 				case "mainSourceFile":
656 					enforce(suffix.empty, "mainSourceFile does not support platform customization.");
657 					this.mainSourceFile = value.get!string;
658 					break;
659 				case "subConfigurations":
660 					enforce(suffix.empty, "subConfigurations does not support platform customization.");
661 					this.subConfigurations = deserializeJson!(string[string])(value);
662 					break;
663 				case "dflags": this.dflags[suffix] = deserializeJson!(string[])(value); break;
664 				case "lflags": this.lflags[suffix] = deserializeJson!(string[])(value); break;
665 				case "libs": this.libs[suffix] = deserializeJson!(string[])(value); break;
666 				case "files":
667 				case "sourceFiles": this.sourceFiles[suffix] = deserializeJson!(string[])(value); break;
668 				case "sourcePaths": this.sourcePaths[suffix] = deserializeJson!(string[])(value); break;
669 				case "sourcePath": this.sourcePaths[suffix] ~= [value.get!string()]; break; // deprecated
670 				case "excludedSourceFiles": this.excludedSourceFiles[suffix] = deserializeJson!(string[])(value); break;
671 				case "copyFiles": this.copyFiles[suffix] = deserializeJson!(string[])(value); break;
672 				case "versions": this.versions[suffix] = deserializeJson!(string[])(value); break;
673 				case "debugVersions": this.debugVersions[suffix] = deserializeJson!(string[])(value); break;
674 				case "importPaths": this.importPaths[suffix] = deserializeJson!(string[])(value); break;
675 				case "stringImportPaths": this.stringImportPaths[suffix] = deserializeJson!(string[])(value); break;
676 				case "preGenerateCommands": this.preGenerateCommands[suffix] = deserializeJson!(string[])(value); break;
677 				case "postGenerateCommands": this.postGenerateCommands[suffix] = deserializeJson!(string[])(value); break;
678 				case "preBuildCommands": this.preBuildCommands[suffix] = deserializeJson!(string[])(value); break;
679 				case "postBuildCommands": this.postBuildCommands[suffix] = deserializeJson!(string[])(value); break;
680 				case "buildRequirements":
681 					BuildRequirements reqs;
682 					foreach (req; deserializeJson!(string[])(value))
683 						reqs |= to!BuildRequirements(req);
684 					this.buildRequirements[suffix] = reqs;
685 					break;
686 				case "buildOptions":
687 					BuildOptions options;
688 					foreach (opt; deserializeJson!(string[])(value))
689 						options |= to!BuildOptions(opt);
690 					this.buildOptions[suffix] = options;
691 					break;
692 			}
693 		}
694 	}
695 
696 	Json toJson()
697 	const {
698 		auto ret = Json.emptyObject;
699 		if( this.dependencies !is null ){
700 			auto deps = Json.emptyObject;
701 			foreach( pack, d; this.dependencies ){
702 				if( d.path.empty && !d.optional ){
703 					deps[pack] = d.toString();
704 				} else {
705 					auto vjson = Json.emptyObject;
706 					vjson["version"] = d.version_.toString();
707 					if (!d.path.empty) vjson["path"] = d.path.toString();
708 					if (d.optional) vjson["optional"] = true;
709 					deps[pack] = vjson;
710 				}
711 			}
712 			ret.dependencies = deps;
713 		}
714 		if (targetType != TargetType.autodetect) ret["targetType"] = targetType.to!string();
715 		if (!targetPath.empty) ret["targetPath"] = targetPath;
716 		if (!targetName.empty) ret["targetName"] = targetName;
717 		if (!workingDirectory.empty) ret["workingDirectory"] = workingDirectory;
718 		if (!mainSourceFile.empty) ret["mainSourceFile"] = mainSourceFile;
719 		foreach (suffix, arr; dflags) ret["dflags"~suffix] = serializeToJson(arr);
720 		foreach (suffix, arr; lflags) ret["lflags"~suffix] = serializeToJson(arr);
721 		foreach (suffix, arr; libs) ret["libs"~suffix] = serializeToJson(arr);
722 		foreach (suffix, arr; sourceFiles) ret["sourceFiles"~suffix] = serializeToJson(arr);
723 		foreach (suffix, arr; sourcePaths) ret["sourcePaths"~suffix] = serializeToJson(arr);
724 		foreach (suffix, arr; excludedSourceFiles) ret["excludedSourceFiles"~suffix] = serializeToJson(arr);
725 		foreach (suffix, arr; copyFiles) ret["copyFiles"~suffix] = serializeToJson(arr);
726 		foreach (suffix, arr; versions) ret["versions"~suffix] = serializeToJson(arr);
727 		foreach (suffix, arr; debugVersions) ret["debugVersions"~suffix] = serializeToJson(arr);
728 		foreach (suffix, arr; importPaths) ret["importPaths"~suffix] = serializeToJson(arr);
729 		foreach (suffix, arr; stringImportPaths) ret["stringImportPaths"~suffix] = serializeToJson(arr);
730 		foreach (suffix, arr; preGenerateCommands) ret["preGenerateCommands"~suffix] = serializeToJson(arr);
731 		foreach (suffix, arr; postGenerateCommands) ret["postGenerateCommands"~suffix] = serializeToJson(arr);
732 		foreach (suffix, arr; preBuildCommands) ret["preBuildCommands"~suffix] = serializeToJson(arr);
733 		foreach (suffix, arr; postBuildCommands) ret["postBuildCommands"~suffix] = serializeToJson(arr);
734 		foreach (suffix, arr; buildRequirements) {
735 			string[] val;
736 			foreach (i; [EnumMembers!BuildRequirements])
737 				if (arr & i) val ~= to!string(i);
738 			ret["buildRequirements"~suffix] = serializeToJson(val);
739 		}
740 		foreach (suffix, arr; buildOptions) {
741 			string[] val;
742 			foreach (i; [EnumMembers!BuildOptions])
743 				if (arr & i) val ~= to!string(i);
744 			ret["buildOptions"~suffix] = serializeToJson(val);
745 		}
746 		return ret;
747 	}
748 
749 	/// Constructs a BuildSettings object from this template.
750 	void getPlatformSettings(ref BuildSettings dst, in BuildPlatform platform, Path base_path)
751 	const {
752 		dst.targetType = this.targetType;
753 		if (!this.targetPath.empty) dst.targetPath = this.targetPath;
754 		if (!this.targetName.empty) dst.targetName = this.targetName;
755 		if (!this.workingDirectory.empty) dst.workingDirectory = this.workingDirectory;
756 		if (!this.mainSourceFile.empty) dst.mainSourceFile = this.mainSourceFile;
757 
758 		void collectFiles(string method)(in string[][string] paths_map, string pattern)
759 		{
760 			foreach (suffix, paths; paths_map) {
761 				if (!platform.matchesSpecification(suffix))
762 					continue;
763 
764 				foreach (spath; paths) {
765 					enforce(!spath.empty, "Paths must not be empty strings.");
766 					auto path = Path(spath);
767 					if (!path.absolute) path = base_path ~ path;
768 					if (!existsFile(path) || !isDir(path.toNativeString())) {
769 						logWarn("Invalid source/import path: %s", path.toNativeString());
770 						continue;
771 					}
772 
773 					foreach (d; dirEntries(path.toNativeString(), pattern, SpanMode.depth)) {
774 						if (isDir(d.name)) continue;
775 						auto src = Path(d.name).relativeTo(base_path);
776 						__traits(getMember, dst, method)(src.toNativeString());
777 					}
778 				}
779 			}
780 		}
781 
782 		// collect files from all source/import folders
783 		collectFiles!"addSourceFiles"(sourcePaths, "*.d");
784 		collectFiles!"addImportFiles"(importPaths, "*.{d,di}");
785 		dst.removeImportFiles(dst.sourceFiles);
786 		collectFiles!"addStringImportFiles"(stringImportPaths, "*");
787 
788 		getPlatformSetting!("dflags", "addDFlags")(dst, platform);
789 		getPlatformSetting!("lflags", "addLFlags")(dst, platform);
790 		getPlatformSetting!("libs", "addLibs")(dst, platform);
791 		getPlatformSetting!("sourceFiles", "addSourceFiles")(dst, platform);
792 		getPlatformSetting!("excludedSourceFiles", "removeSourceFiles")(dst, platform);
793 		getPlatformSetting!("copyFiles", "addCopyFiles")(dst, platform);
794 		getPlatformSetting!("versions", "addVersions")(dst, platform);
795 		getPlatformSetting!("debugVersions", "addDebugVersions")(dst, platform);
796 		getPlatformSetting!("importPaths", "addImportPaths")(dst, platform);
797 		getPlatformSetting!("stringImportPaths", "addStringImportPaths")(dst, platform);
798 		getPlatformSetting!("preGenerateCommands", "addPreGenerateCommands")(dst, platform);
799 		getPlatformSetting!("postGenerateCommands", "addPostGenerateCommands")(dst, platform);
800 		getPlatformSetting!("preBuildCommands", "addPreBuildCommands")(dst, platform);
801 		getPlatformSetting!("postBuildCommands", "addPostBuildCommands")(dst, platform);
802 		getPlatformSetting!("buildRequirements", "addRequirements")(dst, platform);
803 		getPlatformSetting!("buildOptions", "addOptions")(dst, platform);
804 	}
805 
806 	void getPlatformSetting(string name, string addname)(ref BuildSettings dst, in BuildPlatform platform)
807 	const {
808 		foreach(suffix, values; __traits(getMember, this, name)){
809 			if( platform.matchesSpecification(suffix) )
810 				__traits(getMember, dst, addname)(values);
811 		}
812 	}
813 
814 	void warnOnSpecialCompilerFlags(string package_name, string config_name)
815 	{
816 		auto nodef = false;
817 		auto noprop = false;
818 		foreach (req; this.buildRequirements) {
819 			if (req & BuildRequirements.noDefaultFlags) nodef = true;
820 			if (req & BuildRequirements.relaxProperties) noprop = true;
821 		}
822 
823 		if (noprop) {
824 			logWarn(`Warning: "buildRequirements": ["relaxProperties"] is deprecated and is now the default behavior. Note that the -property switch will probably be removed in future versions of DMD.`);
825 			logWarn("");
826 		}
827 
828 		if (nodef) {
829 			logWarn("Warning: This package uses the \"noDefaultFlags\" build requirement. Please use only for development purposes and not for released packages.");
830 			logWarn("");
831 		} else {
832 			string[] all_dflags;
833 			foreach (flags; this.dflags)
834 				all_dflags ~= flags;
835 			.warnOnSpecialCompilerFlags(all_dflags, package_name, config_name);
836 		}
837 	}
838 }
839 
840 /// Returns all package names, starting with the root package in [0].
841 string[] getSubPackagePath(string package_name)
842 {
843 	return package_name.split(":");
844 }
845 
846 /// Returns the name of the base package in the case of some sub package or the
847 /// package itself, if it is already a full package.
848 string getBasePackage(string package_name)
849 {
850 	return package_name.getSubPackagePath()[0];
851 }