1 /**
2 	Contains high-level functionality for working with packages.
3 
4 	Copyright: © 2012-2013 Matthias Dondorff, © 2012-2016 Sönke Ludwig
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Matthias Dondorff, Sönke Ludwig, Martin Nowak, Nick Sabalausky
7 */
8 module dub.package_;
9 
10 public import dub.recipe.packagerecipe;
11 
12 import dub.compilers.compiler;
13 import dub.dependency;
14 import dub.description;
15 import dub.recipe.json;
16 import dub.recipe.sdl;
17 
18 import dub.internal.utils;
19 import dub.internal.vibecompat.core.log;
20 import dub.internal.vibecompat.core.file;
21 import dub.internal.vibecompat.data.json;
22 import dub.internal.vibecompat.inet.url;
23 
24 import std.algorithm;
25 import std.array;
26 import std.conv;
27 import std.exception;
28 import std.file;
29 import std.range;
30 import std.string;
31 import std.typecons : Nullable;
32 
33 
34 /// Lists the supported package recipe formats.
35 enum PackageFormat {
36 	json, /// JSON based, using the ".json" file extension
37 	sdl   /// SDLang based, using the ".sdl" file extension
38 }
39 
40 struct FilenameAndFormat {
41 	string filename;
42 	PackageFormat format;
43 }
44 
45 /// Supported package descriptions in decreasing order of preference.
46 static immutable FilenameAndFormat[] packageInfoFiles = [
47 	{"dub.json", PackageFormat.json},
48 	{"dub.sdl", PackageFormat.sdl},
49 	{"package.json", PackageFormat.json}
50 ];
51 
52 /// Returns a list of all recognized package recipe file names in descending order of precedence.
53 @property string[] packageInfoFilenames() { return packageInfoFiles.map!(f => cast(string)f.filename).array; }
54 
55 /// Returns the default package recile file name.
56 @property string defaultPackageFilename() { return packageInfoFiles[0].filename; }
57 
58 
59 /**	Represents a package, including its sub packages.
60 */
61 class Package {
62 	private {
63 		NativePath m_path;
64 		NativePath m_infoFile;
65 		PackageRecipe m_info;
66 		PackageRecipe m_rawRecipe;
67 		Package m_parentPackage;
68 	}
69 
70 	/** Constructs a `Package` using an in-memory package recipe.
71 
72 		Params:
73 			json_recipe = The package recipe in JSON format
74 			recipe = The package recipe in generic format
75 			root = The directory in which the package resides (if any).
76 			parent = Reference to the parent package, if the new package is a
77 				sub package.
78 			version_override = Optional version to associate to the package
79 				instead of the one declared in the package recipe, or the one
80 				determined by invoking the VCS (GIT currently).
81 	*/
82 	this(Json json_recipe, NativePath root = NativePath(), Package parent = null, string version_override = "")
83 	{
84 		import dub.recipe.json;
85 
86 		PackageRecipe recipe;
87 		parseJson(recipe, json_recipe, parent ? parent.name : null);
88 		this(recipe, root, parent, version_override);
89 	}
90 	/// ditto
91 	this(PackageRecipe recipe, NativePath root = NativePath(), Package parent = null, string version_override = "")
92 	{
93 		// save the original recipe
94 		m_rawRecipe = recipe.clone;
95 
96 		if (!version_override.empty)
97 			recipe.version_ = version_override;
98 
99 		// try to run git to determine the version of the package if no explicit version was given
100 		if (recipe.version_.length == 0 && !parent) {
101 			try recipe.version_ = determineVersionFromSCM(root);
102 			catch (Exception e) logDebug("Failed to determine version by SCM: %s", e.msg);
103 
104 			if (recipe.version_.length == 0) {
105 				logDiagnostic("Note: Failed to determine version of package %s at %s. Assuming ~master.", recipe.name, this.path.toNativeString());
106 				// TODO: Assume unknown version here?
107 				// recipe.version_ = Version.unknown.toString();
108 				recipe.version_ = Version.masterBranch.toString();
109 			} else logDiagnostic("Determined package version using GIT: %s %s", recipe.name, recipe.version_);
110 		}
111 
112 		m_parentPackage = parent;
113 		m_path = root;
114 		m_path.endsWithSlash = true;
115 
116 		// use the given recipe as the basis
117 		m_info = recipe;
118 
119 		fillWithDefaults();
120 	}
121 
122 	/** Searches the given directory for package recipe files.
123 
124 		Params:
125 			directory = The directory to search
126 
127 		Returns:
128 			Returns the full path to the package file, if any was found.
129 			Otherwise returns an empty path.
130 	*/
131 	static NativePath findPackageFile(NativePath directory)
132 	{
133 		foreach (file; packageInfoFiles) {
134 			auto filename = directory ~ file.filename;
135 			if (existsFile(filename)) return filename;
136 		}
137 		return NativePath.init;
138 	}
139 
140 	/** Constructs a `Package` using a package that is physically present on the local file system.
141 
142 		Params:
143 			root = The directory in which the package resides.
144 			recipe_file = Optional path to the package recipe file. If left
145 				empty, the `root` directory will be searched for a recipe file.
146 			parent = Reference to the parent package, if the new package is a
147 				sub package.
148 			version_override = Optional version to associate to the package
149 				instead of the one declared in the package recipe, or the one
150 				determined by invoking the VCS (GIT currently).
151 	*/
152 	static Package load(NativePath root, NativePath recipe_file = NativePath.init, Package parent = null, string version_override = "")
153 	{
154 		import dub.recipe.io;
155 
156 		if (recipe_file.empty) recipe_file = findPackageFile(root);
157 
158 		enforce(!recipe_file.empty,
159 			"No package file found in %s, expected one of %s"
160 				.format(root.toNativeString(),
161 					packageInfoFiles.map!(f => cast(string)f.filename).join("/")));
162 
163 		auto recipe = readPackageRecipe(recipe_file, parent ? parent.name : null);
164 
165 		auto ret = new Package(recipe, root, parent, version_override);
166 		ret.m_infoFile = recipe_file;
167 		return ret;
168 	}
169 
170 	/** Returns the qualified name of the package.
171 
172 		The qualified name includes any possible parent package if this package
173 		is a sub package.
174 	*/
175 	@property string name()
176 	const {
177 		if (m_parentPackage) return m_parentPackage.name ~ ":" ~ m_info.name;
178 		else return m_info.name;
179 	}
180 
181 	/** Returns the directory in which the package resides.
182 
183 		Note that this can be empty for packages that are not stored in the
184 		local file system.
185 	*/
186 	@property NativePath path() const { return m_path; }
187 
188 
189 	/** Accesses the version associated with this package.
190 
191 		Note that this is a shortcut to `this.recipe.version_`.
192 	*/
193 	@property Version version_() const { return m_parentPackage ? m_parentPackage.version_ : Version(m_info.version_); }
194 	/// ditto
195 	@property void version_(Version value) { assert(m_parentPackage is null); m_info.version_ = value.toString(); }
196 
197 	/** Accesses the recipe contents of this package.
198 
199 		The recipe contains any default values and configurations added by DUB.
200 		To access the raw user recipe, use the `rawRecipe` property.
201 
202 		See_Also: `rawRecipe`
203 	*/
204 	@property ref inout(PackageRecipe) recipe() inout { return m_info; }
205 
206 	/** Accesses the original package recipe.
207 
208 		The returned recipe matches exactly the contents of the original package
209 		recipe. For the effective package recipe, augmented with DUB generated
210 		default settings and configurations, use the `recipe` property.
211 
212 		See_Also: `recipe`
213 	*/
214 	@property ref const(PackageRecipe) rawRecipe() const { return m_rawRecipe; }
215 
216 	/** Returns the path to the package recipe file.
217 
218 		Note that this can be empty for packages that are not stored in the
219 		local file system.
220 	*/
221 	@property NativePath recipePath() const { return m_infoFile; }
222 
223 
224 	/** Returns the base package of this package.
225 
226 		The base package is the root of the sub package hierarchy (i.e. the
227 		topmost parent). This will be `null` for packages that are not sub
228 		packages.
229 	*/
230 	@property inout(Package) basePackage() inout { return m_parentPackage ? m_parentPackage.basePackage : this; }
231 
232 	/** Returns the parent of this package.
233 
234 		The parent package is the package that contains a sub package. This will
235 		be `null` for packages that are not sub packages.
236 	*/
237 	@property inout(Package) parentPackage() inout { return m_parentPackage; }
238 
239 	/** Returns the list of all sub packages.
240 
241 		Note that this is a shortcut for `this.recipe.subPackages`.
242 	*/
243 	@property inout(SubPackage)[] subPackages() inout { return m_info.subPackages; }
244 
245 	/** Returns the list of all build configuration names.
246 
247 		Configuration contents can be accessed using `this.recipe.configurations`.
248 	*/
249 	@property string[] configurations()
250 	const {
251 		auto ret = appender!(string[])();
252 		foreach (ref config; m_info.configurations)
253 			ret.put(config.name);
254 		return ret.data;
255 	}
256 
257 	/** Writes the current recipe contents to a recipe file.
258 
259 		The parameter-less overload writes to `this.path`, which must not be
260 		empty. The default recipe file name will be used in this case.
261 	*/
262 	void storeInfo()
263 	{
264 		storeInfo(m_path);
265 		m_infoFile = m_path ~ defaultPackageFilename;
266 	}
267 	/// ditto
268 	void storeInfo(NativePath path)
269 	const {
270 		enforce(!version_.isUnknown, "Trying to store a package with an 'unknown' version, this is not supported.");
271 		auto filename = path ~ defaultPackageFilename;
272 		auto dstFile = openFile(filename.toNativeString(), FileMode.createTrunc);
273 		scope(exit) dstFile.close();
274 		dstFile.writePrettyJsonString(m_info.toJson());
275 	}
276 
277 	/** Returns the package recipe of a non-path-based sub package.
278 
279 		For sub packages that are declared within the package recipe of the
280 		parent package, this function will return the corresponding recipe. Sub
281 		packages declared using a path must be loaded manually (or using the
282 		`PackageManager`).
283 	*/
284 	Nullable!PackageRecipe getInternalSubPackage(string name)
285 	{
286 		foreach (ref p; m_info.subPackages)
287 			if (p.path.empty && p.recipe.name == name)
288 				return Nullable!PackageRecipe(p.recipe);
289 		return Nullable!PackageRecipe();
290 	}
291 
292 	/** Searches for use of compiler-specific flags that have generic
293 		alternatives.
294 
295 		This will output a warning message for each such flag to the console.
296 	*/
297 	void warnOnSpecialCompilerFlags()
298 	{
299 		// warn about use of special flags
300 		m_info.buildSettings.warnOnSpecialCompilerFlags(m_info.name, null);
301 		foreach (ref config; m_info.configurations)
302 			config.buildSettings.warnOnSpecialCompilerFlags(m_info.name, config.name);
303 	}
304 
305 	/** Retrieves a build settings template.
306 
307 		If no `config` is given, this returns the build settings declared at the
308 		root level of the package recipe. Otherwise returns the settings
309 		declared within the given configuration (excluding those at the root
310 		level).
311 
312 		Note that this is a shortcut to accessing `this.recipe.buildSettings` or
313 		`this.recipe.configurations[].buildSettings`.
314 	*/
315 	const(BuildSettingsTemplate) getBuildSettings(string config = null)
316 	const {
317 		if (config.length) {
318 			foreach (ref conf; m_info.configurations)
319 				if (conf.name == config)
320 					return conf.buildSettings;
321 			assert(false, "Unknown configuration: "~config);
322 		} else {
323 			return m_info.buildSettings;
324 		}
325 	}
326 
327 	/** Returns all BuildSettings for the given platform and configuration.
328 
329 		This will gather the effective build settings declared in tha package
330 		recipe for when building on a particular platform and configuration.
331 		Root build settings and configuration specific settings will be
332 		merged.
333 	*/
334 	BuildSettings getBuildSettings(in BuildPlatform platform, string config)
335 	const {
336 		BuildSettings ret;
337 		m_info.buildSettings.getPlatformSettings(ret, platform, this.path);
338 		bool found = false;
339 		foreach(ref conf; m_info.configurations){
340 			if( conf.name != config ) continue;
341 			conf.buildSettings.getPlatformSettings(ret, platform, this.path);
342 			found = true;
343 			break;
344 		}
345 		assert(found || config is null, "Unknown configuration for "~m_info.name~": "~config);
346 
347 		// construct default target name based on package name
348 		if( ret.targetName.empty ) ret.targetName = this.name.replace(":", "_");
349 
350 		// special support for DMD style flags
351 		getCompiler("dmd").extractBuildOptions(ret);
352 
353 		return ret;
354 	}
355 
356 	/** Returns the combination of all build settings for all configurations
357 		and platforms.
358 
359 		This can be useful for IDEs to gather a list of all potentially used
360 		files or settings.
361 	*/
362 	BuildSettings getCombinedBuildSettings()
363 	const {
364 		BuildSettings ret;
365 		m_info.buildSettings.getPlatformSettings(ret, BuildPlatform.any, this.path);
366 		foreach(ref conf; m_info.configurations)
367 			conf.buildSettings.getPlatformSettings(ret, BuildPlatform.any, this.path);
368 
369 		// construct default target name based on package name
370 		if (ret.targetName.empty) ret.targetName = this.name.replace(":", "_");
371 
372 		// special support for DMD style flags
373 		getCompiler("dmd").extractBuildOptions(ret);
374 
375 		return ret;
376 	}
377 
378 	/** Adds build type specific settings to an existing set of build settings.
379 
380 		This function searches the package recipe for overridden build types. If
381 		none is found, the default build settings will be applied, if
382 		`build_type` matches a default build type name. An exception is thrown
383 		otherwise.
384 	*/
385 	void addBuildTypeSettings(ref BuildSettings settings, in BuildPlatform platform, string build_type)
386 	const {
387 		if (build_type == "$DFLAGS") {
388 			import std.process;
389 			string dflags = environment.get("DFLAGS");
390 			settings.addDFlags(dflags.split());
391 			return;
392 		}
393 
394 		if (auto pbt = build_type in m_info.buildTypes) {
395 			logDiagnostic("Using custom build type '%s'.", build_type);
396 			pbt.getPlatformSettings(settings, platform, this.path);
397 		} else {
398 			with(BuildOption) switch (build_type) {
399 				default: throw new Exception(format("Unknown build type for %s: '%s'", this.name, build_type));
400 				case "plain": break;
401 				case "debug": settings.addOptions(debugMode, debugInfo); break;
402 				case "release": settings.addOptions(releaseMode, optimize, inline); break;
403 				case "release-debug": settings.addOptions(releaseMode, optimize, inline, debugInfo); break;
404 				case "release-nobounds": settings.addOptions(releaseMode, optimize, inline, noBoundsCheck); break;
405 				case "unittest": settings.addOptions(unittests, debugMode, debugInfo); break;
406 				case "docs": settings.addOptions(syntaxOnly, _docs); break;
407 				case "ddox": settings.addOptions(syntaxOnly,  _ddox); break;
408 				case "profile": settings.addOptions(profile, optimize, inline, debugInfo); break;
409 				case "profile-gc": settings.addOptions(profileGC, debugInfo); break;
410 				case "cov": settings.addOptions(coverage, debugInfo); break;
411 				case "unittest-cov": settings.addOptions(unittests, coverage, debugMode, debugInfo); break;
412 			}
413 		}
414 	}
415 
416 	/** Returns the selected configuration for a certain dependency.
417 
418 		If no configuration is specified in the package recipe, null will be
419 		returned instead.
420 
421 		FIXME: The `platform` parameter is currently ignored, as the
422 			`"subConfigurations"` field doesn't support platform suffixes.
423 	*/
424 	string getSubConfiguration(string config, in Package dependency, in BuildPlatform platform)
425 	const {
426 		bool found = false;
427 		foreach(ref c; m_info.configurations){
428 			if( c.name == config ){
429 				if( auto pv = dependency.name in c.buildSettings.subConfigurations ) return *pv;
430 				found = true;
431 				break;
432 			}
433 		}
434 		assert(found || config is null, "Invalid configuration \""~config~"\" for "~this.name);
435 		if( auto pv = dependency.name in m_info.buildSettings.subConfigurations ) return *pv;
436 		return null;
437 	}
438 
439 	/** Returns the default configuration to build for the given platform.
440 
441 		This will return the first configuration that is applicable to the given
442 		platform, or `null` if none is applicable. By default, only library
443 		configurations will be returned. Setting `allow_non_library` to `true`
444 		will also return executable configurations.
445 
446 		See_Also: `getPlatformConfigurations`
447 	*/
448 	string getDefaultConfiguration(in BuildPlatform platform, bool allow_non_library = false)
449 	const {
450 		foreach (ref conf; m_info.configurations) {
451 			if (!conf.matchesPlatform(platform)) continue;
452 			if (!allow_non_library && conf.buildSettings.targetType == TargetType.executable) continue;
453 			return conf.name;
454 		}
455 		return null;
456 	}
457 
458 	/** Returns a list of configurations suitable for the given platform.
459 
460 		Params:
461 			platform = The platform against which to match configurations
462 			allow_non_library = If set to true, executable configurations will
463 				also be included.
464 
465 		See_Also: `getDefaultConfiguration`
466 	*/
467 	string[] getPlatformConfigurations(in BuildPlatform platform, bool allow_non_library = false)
468 	const {
469 		auto ret = appender!(string[]);
470 		foreach(ref conf; m_info.configurations){
471 			if (!conf.matchesPlatform(platform)) continue;
472 			if (!allow_non_library && conf.buildSettings.targetType == TargetType.executable) continue;
473 			ret ~= conf.name;
474 		}
475 		if (ret.data.length == 0) ret.put(null);
476 		return ret.data;
477 	}
478 
479 	/** Determines if the package has a dependency to a certain package.
480 
481 		Params:
482 			dependency_name = The name of the package to search for
483 			config = Name of the configuration to use when searching
484 				for dependencies
485 
486 		See_Also: `getDependencies`
487 	*/
488 	bool hasDependency(string dependency_name, string config)
489 	const {
490 		if (dependency_name in m_info.buildSettings.dependencies) return true;
491 		foreach (ref c; m_info.configurations)
492 			if ((config.empty || c.name == config) && dependency_name in c.buildSettings.dependencies)
493 				return true;
494 		return false;
495 	}
496 
497 	/** Retrieves all dependencies for a particular configuration.
498 
499 		This includes dependencies that are declared at the root level of the
500 		package recipe, as well as those declared within the specified
501 		configuration. If no configuration with the given name exists, only
502 		dependencies declared at the root level will be returned.
503 
504 		See_Also: `hasDependency`
505 	*/
506 	const(Dependency[string]) getDependencies(string config)
507 	const {
508 		Dependency[string] ret;
509 		foreach (k, v; m_info.buildSettings.dependencies)
510 			ret[k] = v;
511 		foreach (ref conf; m_info.configurations)
512 			if (conf.name == config) {
513 				foreach (k, v; conf.buildSettings.dependencies)
514 					ret[k] = v;
515 				break;
516 			}
517 		return ret;
518 	}
519 
520 	/** Returns a list of all possible dependencies of the package.
521 
522 		This list includes all dependencies of all configurations. The same
523 		package may occur multiple times with possibly different `Dependency`
524 		values.
525 	*/
526 	PackageDependency[] getAllDependencies()
527 	const {
528 		auto ret = appender!(PackageDependency[]);
529 		getAllDependenciesRange().copy(ret);
530 		return ret.data;
531 	}
532 
533 	// Left as package until the final API for this has been found
534 	package auto getAllDependenciesRange()
535 	const {
536 		return this.recipe.buildSettings.dependencies.byKeyValue
537 			.map!(bs => PackageDependency(bs.key, bs.value))
538 			.chain(
539 				this.recipe.configurations
540 					.map!(c => c.buildSettings.dependencies.byKeyValue
541 						.map!(bs => PackageDependency(bs.key, bs.value))
542 					)
543 					.joiner()
544 			);
545 	}
546 
547 
548 	/** Returns a description of the package for use in IDEs or build tools.
549 	*/
550 	PackageDescription describe(BuildPlatform platform, string config)
551 	const {
552 		return describe(platform, getCompiler(platform.compilerBinary), config);
553 	}
554 	/// ditto
555 	PackageDescription describe(BuildPlatform platform, Compiler compiler, string config)
556 	const {
557 		PackageDescription ret;
558 		ret.configuration = config;
559 		ret.path = m_path.toNativeString();
560 		ret.name = this.name;
561 		ret.version_ = this.version_;
562 		ret.description = m_info.description;
563 		ret.homepage = m_info.homepage;
564 		ret.authors = m_info.authors.dup;
565 		ret.copyright = m_info.copyright;
566 		ret.license = m_info.license;
567 		ret.dependencies = getDependencies(config).keys;
568 
569 		// save build settings
570 		BuildSettings bs = getBuildSettings(platform, config);
571 		BuildSettings allbs = getCombinedBuildSettings();
572 
573 		ret.targetType = bs.targetType;
574 		ret.targetPath = bs.targetPath;
575 		ret.targetName = bs.targetName;
576 		if (ret.targetType != TargetType.none && compiler)
577 			ret.targetFileName = compiler.getTargetFileName(bs, platform);
578 		ret.workingDirectory = bs.workingDirectory;
579 		ret.mainSourceFile = bs.mainSourceFile;
580 		ret.dflags = bs.dflags;
581 		ret.lflags = bs.lflags;
582 		ret.libs = bs.libs;
583 		ret.copyFiles = bs.copyFiles;
584 		ret.versions = bs.versions;
585 		ret.debugVersions = bs.debugVersions;
586 		ret.importPaths = bs.importPaths;
587 		ret.stringImportPaths = bs.stringImportPaths;
588 		ret.preGenerateCommands = bs.preGenerateCommands;
589 		ret.postGenerateCommands = bs.postGenerateCommands;
590 		ret.preBuildCommands = bs.preBuildCommands;
591 		ret.postBuildCommands = bs.postBuildCommands;
592 
593 		// prettify build requirements output
594 		for (int i = 1; i <= BuildRequirement.max; i <<= 1)
595 			if (bs.requirements & cast(BuildRequirement)i)
596 				ret.buildRequirements ~= cast(BuildRequirement)i;
597 
598 		// prettify options output
599 		for (int i = 1; i <= BuildOption.max; i <<= 1)
600 			if (bs.options & cast(BuildOption)i)
601 				ret.options ~= cast(BuildOption)i;
602 
603 		// collect all possible source files and determine their types
604 		SourceFileRole[string] sourceFileTypes;
605 		foreach (f; allbs.stringImportFiles) sourceFileTypes[f] = SourceFileRole.unusedStringImport;
606 		foreach (f; allbs.importFiles) sourceFileTypes[f] = SourceFileRole.unusedImport;
607 		foreach (f; allbs.sourceFiles) sourceFileTypes[f] = SourceFileRole.unusedSource;
608 		foreach (f; bs.stringImportFiles) sourceFileTypes[f] = SourceFileRole.stringImport;
609 		foreach (f; bs.importFiles) sourceFileTypes[f] = SourceFileRole.import_;
610 		foreach (f; bs.sourceFiles) sourceFileTypes[f] = SourceFileRole.source;
611 		foreach (f; sourceFileTypes.byKey.array.sort()) {
612 			SourceFileDescription sf;
613 			sf.path = f;
614 			sf.role = sourceFileTypes[f];
615 			ret.files ~= sf;
616 		}
617 
618 		return ret;
619 	}
620 
621 	private void fillWithDefaults()
622 	{
623 		auto bs = &m_info.buildSettings;
624 
625 		// check for default string import folders
626 		if ("" !in bs.stringImportPaths) {
627 			foreach(defvf; ["views"]){
628 				if( existsFile(m_path ~ defvf) )
629 					bs.stringImportPaths[""] ~= defvf;
630 			}
631 		}
632 
633 		// check for default source folders
634 		immutable hasSP = ("" in bs.sourcePaths) !is null;
635 		immutable hasIP = ("" in bs.importPaths) !is null;
636 		if (!hasSP || !hasIP) {
637 			foreach (defsf; ["source/", "src/"]) {
638 				if (existsFile(m_path ~ defsf)) {
639 					if (!hasSP) bs.sourcePaths[""] ~= defsf;
640 					if (!hasIP) bs.importPaths[""] ~= defsf;
641 				}
642 			}
643 		}
644 
645 		// check for default app_main
646 		string app_main_file;
647 		auto pkg_name = m_info.name.length ? m_info.name : "unknown";
648 		foreach(sf; bs.sourcePaths.get("", null)){
649 			auto p = m_path ~ sf;
650 			if( !existsFile(p) ) continue;
651 			foreach(fil; ["app.d", "main.d", pkg_name ~ "/main.d", pkg_name ~ "/" ~ "app.d"]){
652 				if( existsFile(p ~ fil) ) {
653 					app_main_file = (NativePath(sf) ~ fil).toNativeString();
654 					break;
655 				}
656 			}
657 		}
658 
659 		// generate default configurations if none are defined
660 		if (m_info.configurations.length == 0) {
661 			if (bs.targetType == TargetType.executable) {
662 				BuildSettingsTemplate app_settings;
663 				app_settings.targetType = TargetType.executable;
664 				if (bs.mainSourceFile.empty) app_settings.mainSourceFile = app_main_file;
665 				m_info.configurations ~= ConfigurationInfo("application", app_settings);
666 			} else if (bs.targetType != TargetType.none) {
667 				BuildSettingsTemplate lib_settings;
668 				lib_settings.targetType = bs.targetType == TargetType.autodetect ? TargetType.library : bs.targetType;
669 
670 				if (bs.targetType == TargetType.autodetect) {
671 					if (app_main_file.length) {
672 						lib_settings.excludedSourceFiles[""] ~= app_main_file;
673 
674 						BuildSettingsTemplate app_settings;
675 						app_settings.targetType = TargetType.executable;
676 						app_settings.mainSourceFile = app_main_file;
677 						m_info.configurations ~= ConfigurationInfo("application", app_settings);
678 					}
679 				}
680 
681 				m_info.configurations ~= ConfigurationInfo("library", lib_settings);
682 			}
683 		}
684 	}
685 
686 	package void simpleLint()
687 	const {
688 		if (m_parentPackage) {
689 			if (m_parentPackage.path != path) {
690 				if (this.recipe.license.length && this.recipe.license != m_parentPackage.recipe.license)
691 					logWarn("Warning: License in subpackage %s is different than it's parent package, this is discouraged.", name);
692 			}
693 		}
694 		if (name.empty) logWarn("Warning: The package in %s has no name.", path);
695 		bool[string] cnames;
696 		foreach (ref c; this.recipe.configurations) {
697 			if (c.name in cnames)
698 				logWarn("Warning: Multiple configurations with the name \"%s\" are defined in package \"%s\". This will most likely cause configuration resolution issues.",
699 					c.name, this.name);
700 			cnames[c.name] = true;
701 		}
702 	}
703 }
704 
705 private string determineVersionFromSCM(NativePath path)
706 {
707 	// On Windows, which is slow at running external processes,
708 	// cache the version numbers that are determined using
709 	// GIT to speed up the initialization phase.
710 	version (Windows) {
711 		import std.file : exists, readText;
712 
713 		// quickly determine head commit without invoking GIT
714 		string head_commit;
715 		auto hpath = (path ~ ".git/HEAD").toNativeString();
716 		if (exists(hpath)) {
717 			auto head_ref = readText(hpath).strip();
718 			if (head_ref.startsWith("ref: ")) {
719 				auto rpath = (path ~ (".git/"~head_ref[5 .. $])).toNativeString();
720 				if (exists(rpath))
721 					head_commit = readText(rpath).strip();
722 			}
723 		}
724 
725 		// return the last determined version for that commit
726 		// not that this is not always correct, most notably when
727 		// a tag gets added/removed/changed and changes the outcome
728 		// of the full version detection computation
729 		auto vcachepath = path ~ ".dub/version.json";
730 		if (existsFile(vcachepath)) {
731 			auto ver = jsonFromFile(vcachepath);
732 			if (head_commit == ver["commit"].opt!string)
733 				return ver["version"].get!string;
734 		}
735 	}
736 
737 	// if no cache file or the HEAD commit changed, perform full detection
738 	auto ret = determineVersionWithGIT(path);
739 
740 	version (Windows) {
741 		// update version cache file
742 		if (head_commit.length) {
743 			if (!existsFile(path ~".dub")) createDirectory(path ~ ".dub");
744 			atomicWriteJsonFile(vcachepath, Json(["commit": Json(head_commit), "version": Json(ret)]));
745 		}
746 	}
747 
748 	return ret;
749 }
750 
751 // determines the version of a package that is stored in a GIT working copy
752 // by invoking the "git" executable
753 private string determineVersionWithGIT(NativePath path)
754 {
755 	import std.process;
756 	import dub.semver;
757 
758 	auto git_dir = path ~ ".git";
759 	if (!existsFile(git_dir) || !isDir(git_dir.toNativeString)) return null;
760 	auto git_dir_param = "--git-dir=" ~ git_dir.toNativeString();
761 
762 	static string exec(scope string[] params...) {
763 		auto ret = executeShell(escapeShellCommand(params));
764 		if (ret.status == 0) return ret.output.strip;
765 		logDebug("'%s' failed with exit code %s: %s", params.join(" "), ret.status, ret.output.strip);
766 		return null;
767 	}
768 
769 	auto tag = exec("git", git_dir_param, "describe", "--long", "--tags");
770 	if (tag !is null) {
771 		auto parts = tag.split("-");
772 		auto commit = parts[$-1];
773 		auto num = parts[$-2].to!int;
774 		tag = parts[0 .. $-2].join("-");
775 		if (tag.startsWith("v") && isValidVersion(tag[1 .. $])) {
776 			if (num == 0) return tag[1 .. $];
777 			else if (tag.canFind("+")) return format("%s.commit.%s.%s", tag[1 .. $], num, commit);
778 			else return format("%s+commit.%s.%s", tag[1 .. $], num, commit);
779 		}
780 	}
781 
782 	auto branch = exec("git", git_dir_param, "rev-parse", "--abbrev-ref", "HEAD");
783 	if (branch !is null) {
784 		if (branch != "HEAD") return "~" ~ branch;
785 	}
786 
787 	return null;
788 }