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