1 /**
2 	Abstract representation of a package description file.
3 
4 	Copyright: © 2012-2014 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, Matthias Dondorff
7 */
8 module dub.recipe.packagerecipe;
9 
10 import dub.compilers.compiler;
11 import dub.compilers.utils : warnOnSpecialCompilerFlags;
12 import dub.dependency;
13 import dub.internal.logging;
14 
15 import dub.internal.vibecompat.core.file;
16 import dub.internal.vibecompat.inet.path;
17 
18 import dub.internal.configy.Attributes;
19 
20 import std.algorithm : findSplit, sort;
21 import std.array : join, split;
22 import std.exception : enforce;
23 import std.file;
24 import std.range;
25 import std.process : environment;
26 
27 
28 /**
29 	Returns the individual parts of a qualified package name.
30 
31 	Sub qualified package names are lists of package names separated by ":". For
32 	example, "packa:packb:packc" references a package named "packc" that is a
33 	sub package of "packb", which in turn is a sub package of "packa".
34 */
35 string[] getSubPackagePath(string package_name) @safe pure
36 {
37 	return package_name.split(":");
38 }
39 
40 /**
41 	Returns the name of the top level package for a given (sub) package name.
42 
43 	In case of a top level package, the qualified name is returned unmodified.
44 */
45 string getBasePackageName(string package_name) @safe pure
46 {
47 	return package_name.findSplit(":")[0];
48 }
49 
50 /**
51 	Returns the qualified sub package part of the given package name.
52 
53 	This is the part of the package name excluding the base package
54 	name. See also $(D getBasePackageName).
55 */
56 string getSubPackageName(string package_name) @safe pure
57 {
58 	return package_name.findSplit(":")[2];
59 }
60 
61 @safe unittest
62 {
63 	assert(getSubPackagePath("packa:packb:packc") == ["packa", "packb", "packc"]);
64 	assert(getSubPackagePath("pack") == ["pack"]);
65 	assert(getBasePackageName("packa:packb:packc") == "packa");
66 	assert(getBasePackageName("pack") == "pack");
67 	assert(getSubPackageName("packa:packb:packc") == "packb:packc");
68 	assert(getSubPackageName("pack") == "");
69 }
70 
71 /**
72 	Represents the contents of a package recipe file (dub.json/dub.sdl) in an abstract way.
73 
74 	This structure is used to reason about package descriptions in isolation.
75 	For higher level package handling, see the $(D Package) class.
76 */
77 struct PackageRecipe {
78 	/**
79 	 * Name of the package, used to uniquely identify the package.
80 	 *
81 	 * This field is the only mandatory one.
82 	 * Must be comprised of only lower case ASCII alpha-numeric characters,
83 	 * "-" or "_".
84 	 */
85 	string name;
86 
87 	/// Brief description of the package.
88 	@Optional string description;
89 
90 	/// URL of the project website
91 	@Optional string homepage;
92 
93 	/**
94 	 * List of project authors
95 	 *
96 	 * the suggested format is either:
97 	 * "Peter Parker"
98 	 * or
99 	 * "Peter Parker <pparker@example.com>"
100 	 */
101 	@Optional string[] authors;
102 
103 	/// Copyright declaration string
104 	@Optional string copyright;
105 
106 	/// License(s) under which the project can be used
107 	@Optional string license;
108 
109 	/// Set of version requirements for DUB, compilers and/or language frontend.
110 	@Optional ToolchainRequirements toolchainRequirements;
111 
112 	/**
113 	 * Speficies an optional list of build configurations
114 	 *
115 	 * By default, the first configuration present in the package recipe
116 	 * will be used, except for special configurations (e.g. "unittest").
117 	 * A specific configuration can be chosen from the command line using
118 	 * `--config=name` or `-c name`. A package can select a specific
119 	 * configuration in one of its dependency by using the `subConfigurations`
120 	 * build setting.
121 	 * Build settings defined at the top level affect all configurations.
122 	 */
123 	@Optional @Key("name") ConfigurationInfo[] configurations;
124 
125 	/**
126 	 * Defines additional custom build types or overrides the default ones
127 	 *
128 	 * Build types can be selected from the command line using `--build=name`
129 	 * or `-b name`. The default build type is `debug`.
130 	 */
131 	@Optional BuildSettingsTemplate[string] buildTypes;
132 
133 	/**
134 	 * Build settings influence the command line arguments and options passed
135 	 * to the compiler and linker.
136 	 *
137 	 * All build settings can be present at the top level, and are optional.
138 	 * Build settings can also be found in `configurations`.
139 	 */
140 	@Optional BuildSettingsTemplate buildSettings;
141 	alias buildSettings this;
142 
143 	/**
144 	 * Specifies a list of command line flags usable for controlling
145 	 * filter behavior for `--build=ddox` [experimental]
146 	 */
147 	@Optional @Name("-ddoxFilterArgs") string[] ddoxFilterArgs;
148 
149 	/// Specify which tool to use with `--build=ddox` (experimental)
150 	@Optional @Name("-ddoxTool") string ddoxTool;
151 
152 	/**
153 	 * Sub-packages path or definitions
154 	 *
155 	 * Sub-packages allow to break component of a large framework into smaller
156 	 * packages. In the recipe file, subpackages entry can take one of two forms:
157 	 * either the path to a sub-folder where a recipe file exists,
158 	 * or an object of the same format as a recipe file (or `PackageRecipe`).
159 	 */
160 	@Optional SubPackage[] subPackages;
161 
162 	/// Usually unused by users, this is set by dub automatically
163 	@Optional @Name("version") string version_;
164 
165 	inout(ConfigurationInfo) getConfiguration(string name)
166 	inout {
167 		foreach (c; configurations)
168 			if (c.name == name)
169 				return c;
170 		throw new Exception("Unknown configuration: "~name);
171 	}
172 
173 	/** Clones the package recipe recursively.
174 	*/
175 	PackageRecipe clone() const { return .clone(this); }
176 }
177 
178 struct SubPackage
179 {
180 	string path;
181 	PackageRecipe recipe;
182 
183 	/**
184 	 * Given a YAML parser, recurses into `recipe` or use `path`
185 	 * depending on the node type.
186 	 *
187 	 * Two formats are supported for `subpackages`: a string format,
188 	 * which is just the path to the subpackage, and embedding the
189 	 * full subpackage recipe into the parent package recipe.
190 	 *
191 	 * To support such a dual syntax, Configy requires the use
192 	 * of a `fromYAML` method, as it exposes the underlying format.
193 	 */
194 	static SubPackage fromYAML (scope ConfigParser!SubPackage p)
195 	{
196 		import dub.internal.dyaml.node;
197 
198 		if (p.node.nodeID == NodeID.mapping)
199 			return SubPackage(null, p.parseAs!PackageRecipe);
200 		else
201 			return SubPackage(p.parseAs!string);
202 	}
203 }
204 
205 /// Describes minimal toolchain requirements
206 struct ToolchainRequirements
207 {
208 	import std.typecons : Tuple, tuple;
209 
210 	// TODO: We can remove `@Optional` once bosagora/configy#30 is resolved,
211 	// currently it fails because `Dependency.opCmp` is not CTFE-able.
212 
213 	/// DUB version requirement
214 	@Optional @converter((scope ConfigParser!Dependency p) => p.node.as!string.parseDependency)
215 	Dependency dub = Dependency.any;
216 	/// D front-end version requirement
217 	@Optional @converter((scope ConfigParser!Dependency p) => p.node.as!string.parseDMDDependency)
218 	Dependency frontend = Dependency.any;
219 	/// DMD version requirement
220 	@Optional @converter((scope ConfigParser!Dependency p) => p.node.as!string.parseDMDDependency)
221 	Dependency dmd = Dependency.any;
222 	/// LDC version requirement
223 	@Optional @converter((scope ConfigParser!Dependency p) => p.node.as!string.parseDependency)
224 	Dependency ldc = Dependency.any;
225 	/// GDC version requirement
226 	@Optional @converter((scope ConfigParser!Dependency p) => p.node.as!string.parseDependency)
227 	Dependency gdc = Dependency.any;
228 
229 	/** Get the list of supported compilers.
230 
231 		Returns:
232 			An array of couples of compiler name and compiler requirement
233 	*/
234 	@property Tuple!(string, Dependency)[] supportedCompilers() const
235 	{
236 		Tuple!(string, Dependency)[] res;
237 		if (dmd != Dependency.invalid) res ~= Tuple!(string, Dependency)("dmd", dmd);
238 		if (ldc != Dependency.invalid) res ~= Tuple!(string, Dependency)("ldc", ldc);
239 		if (gdc != Dependency.invalid) res ~= Tuple!(string, Dependency)("gdc", gdc);
240 		return res;
241 	}
242 
243 	bool empty()
244 	const {
245 		import std.algorithm.searching : all;
246 		return only(dub, frontend, dmd, ldc, gdc)
247 			.all!(r => r == Dependency.any);
248 	}
249 }
250 
251 
252 /// Bundles information about a build configuration.
253 struct ConfigurationInfo {
254 	string name;
255 	@Optional string[] platforms;
256 	@Optional BuildSettingsTemplate buildSettings;
257 	alias buildSettings this;
258 
259 	/**
260 	 * Equivalent to the default constructor, used by Configy
261 	 */
262 	this(string name, string[] p, BuildSettingsTemplate build_settings)
263 		@safe pure nothrow @nogc
264 	{
265 		this.name = name;
266 		this.platforms = p;
267 		this.buildSettings = build_settings;
268 	}
269 
270 	this(string name, BuildSettingsTemplate build_settings)
271 	{
272 		enforce(!name.empty, "Configuration name is empty.");
273 		this.name = name;
274 		this.buildSettings = build_settings;
275 	}
276 
277 	bool matchesPlatform(in BuildPlatform platform)
278 	const {
279 		if( platforms.empty ) return true;
280 		foreach(p; platforms)
281 			if (platform.matchesSpecification(p))
282 				return true;
283 		return false;
284 	}
285 }
286 
287 /**
288  * A dependency with possible `BuildSettingsTemplate`
289  *
290  * Currently only `dflags` is taken into account, but the parser accepts any
291  * value that is in `BuildSettingsTemplate`.
292  * This feature was originally introduced to support `-preview`, as setting
293  * a `-preview` in `dflags` does not propagate down to dependencies.
294  */
295 public struct RecipeDependency
296 {
297 	/// The dependency itself
298 	public Dependency dependency;
299 
300 	/// Additional dflags, if any
301 	public BuildSettingsTemplate settings;
302 
303 	/// Convenience alias as most uses just want to deal with the `Dependency`
304 	public alias dependency this;
305 
306 	/**
307 	 * Read a `Dependency` and `BuildSettingsTemplate` from the config file
308 	 *
309 	 * Required to support both short and long form
310 	 */
311 	static RecipeDependency fromYAML (scope ConfigParser!RecipeDependency p)
312 	{
313 		import dub.internal.dyaml.node;
314 
315 		if (p.node.nodeID == NodeID.scalar) {
316 			auto d = YAMLFormat(p.node.as!string);
317 			return RecipeDependency(d.toDependency());
318 		}
319 		auto d = p.parseAs!YAMLFormat;
320 		return RecipeDependency(d.toDependency(), d.settings);
321 	}
322 
323 	/// In-file representation of a dependency as specified by the user
324 	private struct YAMLFormat
325 	{
326 		@Name("version") @Optional string version_;
327 		@Optional string path;
328 		@Optional string repository;
329 		bool optional;
330 		@Name("default") bool default_;
331 
332 		@Optional BuildSettingsTemplate settings;
333 		alias settings this;
334 
335 		/**
336 		 * Used by Configy to provide rich error message when parsing.
337 		 *
338 		 * Exceptions thrown from `validate` methods will be wrapped with field/file
339 		 * informations and rethrown from Configy, providing the user
340 		 * with the location of the configuration that triggered the error.
341 		 */
342 		public void validate () const
343 		{
344 			enforce(this.optional || !this.default_,
345 				"Setting default to 'true' has no effect if 'optional' is not set");
346 			enforce(this.version_.length || this.path.length || this.repository.length,
347 				"Need to provide one of the following fields: 'version', 'path', or 'repository'");
348 
349 			enforce(!this.path.length || !this.repository.length,
350 				"Cannot provide a 'path' dependency if a repository dependency is used");
351 			enforce(!this.repository.length || this.version_.length,
352 				"Need to provide a commit hash in 'version' field with 'repository' dependency");
353 
354 			// Need to deprecate this as it's fairly common
355 			version (none) {
356 				enforce(!this.path.length || !this.version_.length,
357 					"Cannot provide a 'path' dependency if a 'version' dependency is used");
358 			}
359 		}
360 
361 		/// Turns this struct into a `Dependency`
362 		public Dependency toDependency () const
363 		{
364 			auto result = () {
365 				if (this.path.length)
366 					return Dependency(NativePath(this.path));
367 				if (this.repository.length)
368 					return Dependency(Repository(this.repository, this.version_));
369 				return Dependency(VersionRange.fromString(this.version_));
370 			}();
371 			result.optional = this.optional;
372 			result.default_ = this.default_;
373 			return result;
374 		}
375 	}
376 }
377 
378 /// Type used to avoid a breaking change when `Dependency[string]`
379 /// was changed to `RecipeDependency[string]`
380 package struct RecipeDependencyAA
381 {
382 	/// The underlying data, `public` as `alias this` to `private` field doesn't
383 	/// always work.
384 	public RecipeDependency[string] data;
385 
386 	/// Expose base function, e.g. `clear`
387 	alias data this;
388 
389 	/// Supports assignment from a `RecipeDependency` (used in the parser)
390 	public void opIndexAssign(RecipeDependency dep, string key)
391 		pure nothrow
392 	{
393 		this.data[key] = dep;
394 	}
395 
396 	/// Supports assignment from a `Dependency`, used in user code mostly
397 	public void opIndexAssign(Dependency dep, string key)
398 		pure nothrow
399 	{
400 		this.data[key] = RecipeDependency(dep);
401 	}
402 
403 	/// Configy doesn't like `alias this` to an AA
404 	static RecipeDependencyAA fromYAML (scope ConfigParser!RecipeDependencyAA p)
405 	{
406 		return RecipeDependencyAA(p.parseAs!(typeof(this.data)));
407 	}
408 }
409 
410 /// This keeps general information about how to build a package.
411 /// It contains functions to create a specific BuildSetting, targeted at
412 /// a certain BuildPlatform.
413 struct BuildSettingsTemplate {
414 	@Optional RecipeDependencyAA dependencies;
415 	@Optional string systemDependencies;
416 	@Optional TargetType targetType = TargetType.autodetect;
417 	@Optional string targetPath;
418 	@Optional string targetName;
419 	@Optional string workingDirectory;
420 	@Optional string mainSourceFile;
421 	@Optional string[string] subConfigurations;
422 	@StartsWith("dflags") string[][string] dflags;
423 	@StartsWith("lflags") string[][string] lflags;
424 	@StartsWith("libs") string[][string] libs;
425 	@StartsWith("sourceFiles") string[][string] sourceFiles;
426 	@StartsWith("sourcePaths") string[][string] sourcePaths;
427 	@StartsWith("excludedSourceFiles") string[][string] excludedSourceFiles;
428 	@StartsWith("injectSourceFiles") string[][string] injectSourceFiles;
429 	@StartsWith("copyFiles") string[][string] copyFiles;
430 	@StartsWith("extraDependencyFiles") string[][string] extraDependencyFiles;
431 	@StartsWith("versions") string[][string] versions;
432 	@StartsWith("debugVersions") string[][string] debugVersions;
433 	@StartsWith("versionFilters") string[][string] versionFilters;
434 	@StartsWith("debugVersionFilters") string[][string] debugVersionFilters;
435 	@StartsWith("importPaths") string[][string] importPaths;
436 	@StartsWith("stringImportPaths") string[][string] stringImportPaths;
437 	@StartsWith("preGenerateCommands") string[][string] preGenerateCommands;
438 	@StartsWith("postGenerateCommands") string[][string] postGenerateCommands;
439 	@StartsWith("preBuildCommands") string[][string] preBuildCommands;
440 	@StartsWith("postBuildCommands") string[][string] postBuildCommands;
441 	@StartsWith("preRunCommands") string[][string] preRunCommands;
442 	@StartsWith("postRunCommands") string[][string] postRunCommands;
443 	@StartsWith("environments") string[string][string] environments;
444 	@StartsWith("buildEnvironments")string[string][string] buildEnvironments;
445 	@StartsWith("runEnvironments") string[string][string] runEnvironments;
446 	@StartsWith("preGenerateEnvironments") string[string][string] preGenerateEnvironments;
447 	@StartsWith("postGenerateEnvironments") string[string][string] postGenerateEnvironments;
448 	@StartsWith("preBuildEnvironments") string[string][string] preBuildEnvironments;
449 	@StartsWith("postBuildEnvironments") string[string][string] postBuildEnvironments;
450 	@StartsWith("preRunEnvironments") string[string][string] preRunEnvironments;
451 	@StartsWith("postRunEnvironments") string[string][string] postRunEnvironments;
452 
453 	@StartsWith("buildRequirements") @Optional
454 	Flags!BuildRequirement[string] buildRequirements;
455 	@StartsWith("buildOptions") @Optional
456 	Flags!BuildOption[string] buildOptions;
457 
458 
459 	BuildSettingsTemplate dup() const {
460 		return clone(this);
461 	}
462 
463 	/// Constructs a BuildSettings object from this template.
464 	void getPlatformSettings(ref BuildSettings dst, in BuildPlatform platform, NativePath base_path)
465 	const {
466 		dst.targetType = this.targetType;
467 		if (!this.targetPath.empty) dst.targetPath = this.targetPath;
468 		if (!this.targetName.empty) dst.targetName = this.targetName;
469 		if (!this.workingDirectory.empty) dst.workingDirectory = this.workingDirectory;
470 		if (!this.mainSourceFile.empty) {
471 			auto p = NativePath(this.mainSourceFile);
472 			p.normalize();
473 			dst.mainSourceFile = p.toNativeString();
474 			dst.addSourceFiles(dst.mainSourceFile);
475 		}
476 
477 		string[] collectFiles(in string[][string] paths_map, string pattern)
478 		{
479 			auto files = appender!(string[]);
480 
481 			import dub.project : buildSettingsVars;
482 			import std.typecons : Nullable;
483 
484 			static Nullable!(string[string]) envVarCache;
485 
486 			if (envVarCache.isNull) envVarCache = environment.toAA();
487 
488 			foreach (suffix, paths; paths_map) {
489 				if (!platform.matchesSpecification(suffix))
490 					continue;
491 
492 				foreach (spath; paths) {
493 					enforce(!spath.empty, "Paths must not be empty strings.");
494 					auto path = NativePath(spath);
495 					if (!path.absolute) path = base_path ~ path;
496 					if (!existsDirectory(path)) {
497 						import std.algorithm : any, find;
498 						const hasVar = chain(buildSettingsVars, envVarCache.get.byKey).any!((string var) {
499 							return spath.find("$"~var).length > 0 || spath.find("${"~var~"}").length > 0;
500 						});
501 						if (!hasVar)
502 							logWarn("Invalid source/import path: %s", path.toNativeString());
503 						continue;
504 					}
505 
506 					auto pstr = path.toNativeString();
507 					foreach (d; dirEntries(pstr, pattern, SpanMode.depth)) {
508 						import std.path : baseName, pathSplitter;
509 						import std.algorithm.searching : canFind;
510 						// eliminate any hidden files, or files in hidden directories. But always include
511 						// files that are listed inside hidden directories that are specifically added to
512 						// the project.
513 						if (d.isDir || pathSplitter(d.name[pstr.length .. $])
514 								   .canFind!(name => name.length && name[0] == '.'))
515 							continue;
516 						auto src = NativePath(d.name).relativeTo(base_path);
517 						files ~= src.toNativeString();
518 					}
519 				}
520 			}
521 
522 			return files.data;
523 		}
524 
525  		// collect source files
526 		dst.addSourceFiles(collectFiles(sourcePaths, "*.d"));
527 		auto sourceFiles = dst.sourceFiles.sort();
528 
529  		// collect import files and remove sources
530 		import std.algorithm : copy, setDifference;
531 
532 		auto importFiles = collectFiles(importPaths, "*.{d,di}").sort();
533 		immutable nremoved = importFiles.setDifference(sourceFiles).copy(importFiles.release).length;
534 		importFiles = importFiles[0 .. $ - nremoved];
535 		dst.addImportFiles(importFiles.release);
536 
537 		dst.addStringImportFiles(collectFiles(stringImportPaths, "*"));
538 
539 		getPlatformSetting!("dflags", "addDFlags")(dst, platform);
540 		getPlatformSetting!("lflags", "addLFlags")(dst, platform);
541 		getPlatformSetting!("libs", "addLibs")(dst, platform);
542 		getPlatformSetting!("sourceFiles", "addSourceFiles")(dst, platform);
543 		getPlatformSetting!("excludedSourceFiles", "removeSourceFiles")(dst, platform);
544 		getPlatformSetting!("injectSourceFiles", "addInjectSourceFiles")(dst, platform);
545 		getPlatformSetting!("copyFiles", "addCopyFiles")(dst, platform);
546 		getPlatformSetting!("extraDependencyFiles", "addExtraDependencyFiles")(dst, platform);
547 		getPlatformSetting!("versions", "addVersions")(dst, platform);
548 		getPlatformSetting!("debugVersions", "addDebugVersions")(dst, platform);
549 		getPlatformSetting!("versionFilters", "addVersionFilters")(dst, platform);
550 		getPlatformSetting!("debugVersionFilters", "addDebugVersionFilters")(dst, platform);
551 		getPlatformSetting!("importPaths", "addImportPaths")(dst, platform);
552 		getPlatformSetting!("stringImportPaths", "addStringImportPaths")(dst, platform);
553 		getPlatformSetting!("preGenerateCommands", "addPreGenerateCommands")(dst, platform);
554 		getPlatformSetting!("postGenerateCommands", "addPostGenerateCommands")(dst, platform);
555 		getPlatformSetting!("preBuildCommands", "addPreBuildCommands")(dst, platform);
556 		getPlatformSetting!("postBuildCommands", "addPostBuildCommands")(dst, platform);
557 		getPlatformSetting!("preRunCommands", "addPreRunCommands")(dst, platform);
558 		getPlatformSetting!("postRunCommands", "addPostRunCommands")(dst, platform);
559 		getPlatformSetting!("environments", "addEnvironments")(dst, platform);
560 		getPlatformSetting!("buildEnvironments", "addBuildEnvironments")(dst, platform);
561 		getPlatformSetting!("runEnvironments", "addRunEnvironments")(dst, platform);
562 		getPlatformSetting!("preGenerateEnvironments", "addPreGenerateEnvironments")(dst, platform);
563 		getPlatformSetting!("postGenerateEnvironments", "addPostGenerateEnvironments")(dst, platform);
564 		getPlatformSetting!("preBuildEnvironments", "addPreBuildEnvironments")(dst, platform);
565 		getPlatformSetting!("postBuildEnvironments", "addPostBuildEnvironments")(dst, platform);
566 		getPlatformSetting!("preRunEnvironments", "addPreRunEnvironments")(dst, platform);
567 		getPlatformSetting!("postRunEnvironments", "addPostRunEnvironments")(dst, platform);
568 		getPlatformSetting!("buildRequirements", "addRequirements")(dst, platform);
569 		getPlatformSetting!("buildOptions", "addOptions")(dst, platform);
570 	}
571 
572 	void getPlatformSetting(string name, string addname)(ref BuildSettings dst, in BuildPlatform platform)
573 	const {
574 		foreach(suffix, values; __traits(getMember, this, name)){
575 			if( platform.matchesSpecification(suffix) )
576 				__traits(getMember, dst, addname)(values);
577 		}
578 	}
579 
580 	void warnOnSpecialCompilerFlags(string package_name, string config_name)
581 	{
582 		auto nodef = false;
583 		auto noprop = false;
584 		foreach (req; this.buildRequirements) {
585 			if (req & BuildRequirement.noDefaultFlags) nodef = true;
586 			if (req & BuildRequirement.relaxProperties) noprop = true;
587 		}
588 
589 		if (noprop) {
590 			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.`);
591 			logWarn("");
592 		}
593 
594 		if (nodef) {
595 			logWarn("Warning: This package uses the \"noDefaultFlags\" build requirement. Please use only for development purposes and not for released packages.");
596 			logWarn("");
597 		} else {
598 			string[] all_dflags;
599 			Flags!BuildOption all_options;
600 			foreach (flags; this.dflags) all_dflags ~= flags;
601 			foreach (options; this.buildOptions) all_options |= options;
602 			.warnOnSpecialCompilerFlags(all_dflags, all_options, package_name, config_name);
603 		}
604 	}
605 }
606 
607 package(dub) void checkPlatform(const scope ref ToolchainRequirements tr, BuildPlatform platform, string package_name)
608 {
609 	import dub.compilers.utils : dmdLikeVersionToSemverLike;
610 	import std.algorithm.iteration : map;
611 	import std.format : format;
612 
613 	string compilerver;
614 	Dependency compilerspec;
615 
616 	switch (platform.compiler) {
617 		default:
618 			compilerspec = Dependency.any;
619 			compilerver = "0.0.0";
620 			break;
621 		case "dmd":
622 			compilerspec = tr.dmd;
623 			compilerver = platform.compilerVersion.length
624 				? dmdLikeVersionToSemverLike(platform.compilerVersion)
625 				: "0.0.0";
626 			break;
627 		case "ldc":
628 			compilerspec = tr.ldc;
629 			compilerver = platform.compilerVersion;
630 			if (!compilerver.length) compilerver = "0.0.0";
631 			break;
632 		case "gdc":
633 			compilerspec = tr.gdc;
634 			compilerver = platform.compilerVersion;
635 			if (!compilerver.length) compilerver = "0.0.0";
636 			break;
637 	}
638 
639 	enforce(compilerspec != Dependency.invalid,
640 		format(
641 			"Installed %s %s is not supported by %s. Supported compiler(s):\n%s",
642 			platform.compiler, platform.compilerVersion, package_name,
643 			tr.supportedCompilers.map!((cs) {
644 				auto str = "  - " ~ cs[0];
645 				if (cs[1] != Dependency.any) str ~= ": " ~ cs[1].toString();
646 				return str;
647 			}).join("\n")
648 		)
649 	);
650 
651 	enforce(compilerspec.matches(compilerver),
652 		format(
653 			"Installed %s-%s does not comply with %s compiler requirement: %s %s\n" ~
654 			"Please consider upgrading your installation.",
655 			platform.compiler, platform.compilerVersion,
656 			package_name, platform.compiler, compilerspec
657 		)
658 	);
659 
660 	enforce(tr.frontend.matches(dmdLikeVersionToSemverLike(platform.frontendVersionString)),
661 		format(
662 			"Installed %s-%s with frontend %s does not comply with %s frontend requirement: %s\n" ~
663 			"Please consider upgrading your installation.",
664 			platform.compiler, platform.compilerVersion,
665 			platform.frontendVersionString, package_name, tr.frontend
666 		)
667 	);
668 }
669 
670 package bool addRequirement(ref ToolchainRequirements req, string name, string value)
671 {
672 	switch (name) {
673 		default: return false;
674 		case "dub": req.dub = parseDependency(value); break;
675 		case "frontend": req.frontend = parseDMDDependency(value); break;
676 		case "ldc": req.ldc = parseDependency(value); break;
677 		case "gdc": req.gdc = parseDependency(value); break;
678 		case "dmd": req.dmd = parseDMDDependency(value); break;
679 	}
680 	return true;
681 }
682 
683 private static Dependency parseDependency(string dep)
684 {
685 	if (dep == "no") return Dependency.invalid;
686 	return Dependency(dep);
687 }
688 
689 private static Dependency parseDMDDependency(string dep)
690 {
691 	import dub.compilers.utils : dmdLikeVersionToSemverLike;
692 	import dub.dependency : Dependency;
693 	import std.algorithm : map, splitter;
694 	import std.array : join;
695 
696 	if (dep == "no") return Dependency.invalid;
697 	return dep
698 		.splitter(' ')
699 		.map!(r => dmdLikeVersionToSemverLike(r))
700 		.join(' ')
701 		.Dependency;
702 }
703 
704 private T clone(T)(ref const(T) val)
705 {
706 	import dub.internal.dyaml.stdsumtype;
707 	import std.traits : isSomeString, isDynamicArray, isAssociativeArray, isBasicType, ValueType;
708 
709 	static if (is(T == immutable)) return val;
710 	else static if (isBasicType!T) return val;
711 	else static if (isDynamicArray!T) {
712 		alias V = typeof(T.init[0]);
713 		static if (is(V == immutable)) return val;
714 		else {
715 			T ret = new V[val.length];
716 			foreach (i, ref f; val)
717 				ret[i] = clone!V(f);
718 			return ret;
719 		}
720 	} else static if (isAssociativeArray!T) {
721 		alias V = ValueType!T;
722 		T ret;
723 		foreach (k, ref f; val)
724 			ret[k] = clone!V(f);
725 		return ret;
726 	} else static if (is(T == SumType!A, A...)) {
727 		return val.match!((any) => T(clone(any)));
728 	} else static if (is(T == struct)) {
729 		T ret;
730 		foreach (i, M; typeof(T.tupleof))
731 			ret.tupleof[i] = clone!M(val.tupleof[i]);
732 		return ret;
733 	} else static assert(false, "Unsupported type: "~T.stringof);
734 }
735 
736 unittest { // issue #1407 - duplicate main source file
737 	{
738 		BuildSettingsTemplate t;
739 		t.mainSourceFile = "./foo.d";
740 		t.sourceFiles[""] = ["foo.d"];
741 		BuildSettings bs;
742 		t.getPlatformSettings(bs, BuildPlatform.init, NativePath("/"));
743 		assert(bs.sourceFiles == ["foo.d"]);
744 	}
745 
746 	version (Windows) {{
747 		BuildSettingsTemplate t;
748 		t.mainSourceFile = "src/foo.d";
749 		t.sourceFiles[""] = ["src\\foo.d"];
750 		BuildSettings bs;
751 		t.getPlatformSettings(bs, BuildPlatform.init, NativePath("/"));
752 		assert(bs.sourceFiles == ["src\\foo.d"]);
753 	}}
754 }
755 
756 /**
757  * Edit all dependency names from `:foo` to `name:foo`.
758  *
759  * TODO: Remove the special case in the parser and remove this hack.
760  */
761 package void fixDependenciesNames (T) (string root, ref T aggr) nothrow
762 {
763 	static foreach (idx, FieldRef; T.tupleof) {
764 		static if (is(immutable typeof(FieldRef) == immutable RecipeDependencyAA)) {
765 			string[] toReplace;
766 			foreach (key; aggr.tupleof[idx].byKey)
767 				if (key.length && key[0] == ':')
768 					toReplace ~= key;
769 			foreach (k; toReplace) {
770 				aggr.tupleof[idx][root ~ k] = aggr.tupleof[idx][k];
771 				aggr.tupleof[idx].data.remove(k);
772 			}
773 		}
774 		else static if (is(typeof(FieldRef) == struct))
775 			fixDependenciesNames(root, aggr.tupleof[idx]);
776 	}
777 }