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