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