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