1 /**
2 	Stuff with dependencies.
3 
4 	Copyright: © 2012-2013 Matthias Dondorff, © 2012-2015 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
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 
35 enum PackageFormat { json, sdl }
36 struct FilenameAndFormat
37 {
38 	string filename;
39 	PackageFormat format;
40 }
41 struct PathAndFormat
42 {
43 	Path path;
44 	PackageFormat format;
45 	@property bool empty() { return path.empty; }
46 	string toString() { return path.toString(); }
47 }
48 
49 // Supported package descriptions in decreasing order of preference.
50 static immutable FilenameAndFormat[] packageInfoFiles = [
51 	{"dub.json", PackageFormat.json},
52 	/*{"dub.sdl",PackageFormat.sdl},*/
53 	{"package.json", PackageFormat.json}
54 ];
55 
56 @property string[] packageInfoFilenames() { return packageInfoFiles.map!(f => cast(string)f.filename).array; }
57 
58 @property string defaultPackageFilename() { return packageInfoFiles[0].filename; }
59 
60 
61 /**
62 	Represents a package, including its sub packages
63 
64 	Documentation of the dub.json can be found at
65 	http://registry.vibed.org/package-format
66 */
67 class Package {
68 	private {
69 		Path m_path;
70 		PathAndFormat m_infoFile;
71 		PackageRecipe m_info;
72 		Package m_parentPackage;
73 	}
74 
75 	static PathAndFormat findPackageFile(Path path)
76 	{
77 		foreach(file; packageInfoFiles) {
78 			auto filename = path ~ file.filename;
79 			if(existsFile(filename)) return PathAndFormat(filename, file.format);
80 		}
81 		return PathAndFormat(Path());
82 	}
83 
84 	this(Path root, PathAndFormat infoFile = PathAndFormat(), Package parent = null, string versionOverride = "")
85 	{
86 		RawPackage raw_package;
87 		m_infoFile = infoFile;
88 
89 		try {
90 			if(m_infoFile.empty) {
91 				m_infoFile = findPackageFile(root);
92 				if(m_infoFile.empty)
93 					throw new Exception(
94 						"No package file found in %s, expected one of %s"
95 							.format(root.toNativeString(), packageInfoFiles.map!(f => cast(string)f.filename).join("/")));
96 			}
97 			raw_package = rawPackageFromFile(m_infoFile);
98 		} catch (Exception ex) throw ex;//throw new Exception(format("Failed to load package %s: %s", m_infoFile.toNativeString(), ex.msg));
99 
100 		enforce(raw_package !is null, format("Missing package description for package at %s", root.toNativeString()));
101 		this(raw_package, root, parent, versionOverride);
102 	}
103 
104 	this(Json package_info, Path root = Path(), Package parent = null, string versionOverride = "")
105 	{
106 		this(new JsonPackage(package_info), root, parent, versionOverride);
107 	}
108 
109 	this(RawPackage raw_package, Path root = Path(), Package parent = null, string versionOverride = "")
110 	{
111 		PackageRecipe recipe;
112 
113 		// parse the Package description
114 		if(raw_package !is null)
115 		{
116 			scope(failure) logError("Failed to parse package description for %s %s in %s.",
117 				raw_package.package_name, versionOverride.length ? versionOverride : raw_package.version_,
118 				root.length ? root.toNativeString() : "remote location");
119 			raw_package.parseInto(recipe, parent ? parent.name : null);
120 
121 			if (!versionOverride.empty)
122 				recipe.version_ = versionOverride;
123 
124 			// try to run git to determine the version of the package if no explicit version was given
125 			if (recipe.version_.length == 0 && !parent) {
126 				try recipe.version_ = determineVersionFromSCM(root);
127 				catch (Exception e) logDebug("Failed to determine version by SCM: %s", e.msg);
128 
129 				if (recipe.version_.length == 0) {
130 					logDiagnostic("Note: Failed to determine version of package %s at %s. Assuming ~master.", recipe.name, this.path.toNativeString());
131 					// TODO: Assume unknown version here?
132 					// recipe.version_ = Version.UNKNOWN.toString();
133 					recipe.version_ = Version.MASTER.toString();
134 				} else logDiagnostic("Determined package version using GIT: %s %s", recipe.name, recipe.version_);
135 			}
136 		}
137 
138 		this(recipe, root, parent);
139 	}
140 
141 	this(PackageRecipe recipe, Path root = Path(), Package parent = null)
142 	{
143 		m_parentPackage = parent;
144 		m_path = root;
145 		m_path.endsWithSlash = true;
146 
147 		// use the given recipe as the basis
148 		m_info = recipe;
149 
150 		fillWithDefaults();
151 		simpleLint();
152 	}
153 
154 	@property string name()
155 	const {
156 		if (m_parentPackage) return m_parentPackage.name ~ ":" ~ m_info.name;
157 		else return m_info.name;
158 	}
159 	@property string vers() const { return m_parentPackage ? m_parentPackage.vers : m_info.version_; }
160 	@property Version ver() const { return Version(this.vers); }
161 	@property void ver(Version ver) { assert(m_parentPackage is null); m_info.version_ = ver.toString(); }
162 	@property ref inout(PackageRecipe) info() inout { return m_info; }
163 	@property Path path() const { return m_path; }
164 	@property Path packageInfoFilename() const { return m_infoFile.path; }
165 	@property const(Dependency[string]) dependencies() const { return m_info.dependencies; }
166 	@property inout(Package) basePackage() inout { return m_parentPackage ? m_parentPackage.basePackage : this; }
167 	@property inout(Package) parentPackage() inout { return m_parentPackage; }
168 	@property inout(SubPackage)[] subPackages() inout { return m_info.subPackages; }
169 
170 	@property string[] configurations()
171 	const {
172 		auto ret = appender!(string[])();
173 		foreach( ref config; m_info.configurations )
174 			ret.put(config.name);
175 		return ret.data;
176 	}
177 
178 	const(Dependency[string]) getDependencies(string config)
179 	const {
180 		Dependency[string] ret;
181 		foreach (k, v; m_info.buildSettings.dependencies)
182 			ret[k] = v;
183 		foreach (ref conf; m_info.configurations)
184 			if (conf.name == config) {
185 				foreach (k, v; conf.buildSettings.dependencies)
186 					ret[k] = v;
187 				break;
188 			}
189 		return ret;
190 	}
191 
192 	/** Overwrites the packge description file using the default filename with the current information.
193 	*/
194 	void storeInfo()
195 	{
196 		storeInfo(m_path);
197 		m_infoFile = PathAndFormat(m_path ~ defaultPackageFilename);
198 	}
199 	/// ditto
200 	void storeInfo(Path path)
201 	const {
202 		enforce(!ver.isUnknown, "Trying to store a package with an 'unknown' version, this is not supported.");
203 		auto filename = path ~ defaultPackageFilename;
204 		auto dstFile = openFile(filename.toNativeString(), FileMode.CreateTrunc);
205 		scope(exit) dstFile.close();
206 		dstFile.writePrettyJsonString(m_info.toJson());
207 	}
208 
209 	Nullable!PackageRecipe getInternalSubPackage(string name)
210 	{
211 		foreach (ref p; m_info.subPackages)
212 			if (p.path.empty && p.recipe.name == name)
213 				return Nullable!PackageRecipe(p.recipe);
214 		return Nullable!PackageRecipe();
215 	}
216 
217 	void warnOnSpecialCompilerFlags()
218 	{
219 		// warn about use of special flags
220 		m_info.buildSettings.warnOnSpecialCompilerFlags(m_info.name, null);
221 		foreach (ref config; m_info.configurations)
222 			config.buildSettings.warnOnSpecialCompilerFlags(m_info.name, config.name);
223 	}
224 
225 	const(BuildSettingsTemplate) getBuildSettings(string config = null)
226 	const {
227 		if (config.length) {
228 			foreach (ref conf; m_info.configurations)
229 				if (conf.name == config)
230 					return conf.buildSettings;
231 			assert(false, "Unknown configuration: "~config);
232 		} else {
233 			return m_info.buildSettings;
234 		}
235 	}
236 
237 	/// Returns all BuildSettings for the given platform and config.
238 	BuildSettings getBuildSettings(in BuildPlatform platform, string config)
239 	const {
240 		BuildSettings ret;
241 		m_info.buildSettings.getPlatformSettings(ret, platform, this.path);
242 		bool found = false;
243 		foreach(ref conf; m_info.configurations){
244 			if( conf.name != config ) continue;
245 			conf.buildSettings.getPlatformSettings(ret, platform, this.path);
246 			found = true;
247 			break;
248 		}
249 		assert(found || config is null, "Unknown configuration for "~m_info.name~": "~config);
250 
251 		// construct default target name based on package name
252 		if( ret.targetName.empty ) ret.targetName = this.name.replace(":", "_");
253 
254 		// special support for DMD style flags
255 		getCompiler("dmd").extractBuildOptions(ret);
256 
257 		return ret;
258 	}
259 
260 	/// Returns the combination of all build settings for all configurations and platforms
261 	BuildSettings getCombinedBuildSettings()
262 	const {
263 		BuildSettings ret;
264 		m_info.buildSettings.getPlatformSettings(ret, BuildPlatform.any, this.path);
265 		foreach(ref conf; m_info.configurations)
266 			conf.buildSettings.getPlatformSettings(ret, BuildPlatform.any, this.path);
267 
268 		// construct default target name based on package name
269 		if (ret.targetName.empty) ret.targetName = this.name.replace(":", "_");
270 
271 		// special support for DMD style flags
272 		getCompiler("dmd").extractBuildOptions(ret);
273 
274 		return ret;
275 	}
276 
277 	void addBuildTypeSettings(ref BuildSettings settings, in BuildPlatform platform, string build_type)
278 	const {
279 		if (build_type == "$DFLAGS") {
280 			import std.process;
281 			string dflags = environment.get("DFLAGS");
282 			settings.addDFlags(dflags.split());
283 			return;
284 		}
285 
286 		if (auto pbt = build_type in m_info.buildTypes) {
287 			logDiagnostic("Using custom build type '%s'.", build_type);
288 			pbt.getPlatformSettings(settings, platform, this.path);
289 		} else {
290 			with(BuildOption) switch (build_type) {
291 				default: throw new Exception(format("Unknown build type for %s: '%s'", this.name, build_type));
292 				case "plain": break;
293 				case "debug": settings.addOptions(debugMode, debugInfo); break;
294 				case "release": settings.addOptions(releaseMode, optimize, inline); break;
295 				case "release-nobounds": settings.addOptions(releaseMode, optimize, inline, noBoundsCheck); break;
296 				case "unittest": settings.addOptions(unittests, debugMode, debugInfo); break;
297 				case "docs": settings.addOptions(syntaxOnly); settings.addDFlags("-c", "-Dddocs"); break;
298 				case "ddox": settings.addOptions(syntaxOnly); settings.addDFlags("-c", "-Df__dummy.html", "-Xfdocs.json"); break;
299 				case "profile": settings.addOptions(profile, optimize, inline, debugInfo); break;
300 				case "cov": settings.addOptions(coverage, debugInfo); break;
301 				case "unittest-cov": settings.addOptions(unittests, coverage, debugMode, debugInfo); break;
302 			}
303 		}
304 	}
305 
306 	string getSubConfiguration(string config, in Package dependency, in BuildPlatform platform)
307 	const {
308 		bool found = false;
309 		foreach(ref c; m_info.configurations){
310 			if( c.name == config ){
311 				if( auto pv = dependency.name in c.buildSettings.subConfigurations ) return *pv;
312 				found = true;
313 				break;
314 			}
315 		}
316 		assert(found || config is null, "Invalid configuration \""~config~"\" for "~this.name);
317 		if( auto pv = dependency.name in m_info.buildSettings.subConfigurations ) return *pv;
318 		return null;
319 	}
320 
321 	/// Returns the default configuration to build for the given platform
322 	string getDefaultConfiguration(in BuildPlatform platform, bool allow_non_library = false)
323 	const {
324 		foreach (ref conf; m_info.configurations) {
325 			if (!conf.matchesPlatform(platform)) continue;
326 			if (!allow_non_library && conf.buildSettings.targetType == TargetType.executable) continue;
327 			return conf.name;
328 		}
329 		return null;
330 	}
331 
332 	/// Returns a list of configurations suitable for the given platform
333 	string[] getPlatformConfigurations(in BuildPlatform platform, bool is_main_package = false)
334 	const {
335 		auto ret = appender!(string[]);
336 		foreach(ref conf; m_info.configurations){
337 			if (!conf.matchesPlatform(platform)) continue;
338 			if (!is_main_package && conf.buildSettings.targetType == TargetType.executable) continue;
339 			ret ~= conf.name;
340 		}
341 		if (ret.data.length == 0) ret.put(null);
342 		return ret.data;
343 	}
344 
345 	/// Human readable information of this package and its dependencies.
346 	string generateInfoString() const {
347 		string s;
348 		s ~= m_info.name ~ ", version '" ~ m_info.version_ ~ "'";
349 		s ~= "\n  Dependencies:";
350 		foreach(string p, ref const Dependency v; m_info.dependencies)
351 			s ~= "\n    " ~ p ~ ", version '" ~ v.toString() ~ "'";
352 		return s;
353 	}
354 
355 	bool hasDependency(string depname, string config)
356 	const {
357 		if (depname in m_info.buildSettings.dependencies) return true;
358 		foreach (ref c; m_info.configurations)
359 			if ((config.empty || c.name == config) && depname in c.buildSettings.dependencies)
360 				return true;
361 		return false;
362 	}
363 
364 	/** Returns a description of the package for use in IDEs or build tools.
365 	*/
366 	PackageDescription describe(BuildPlatform platform, string config)
367 	const {
368 		PackageDescription ret;
369 		ret.path = m_path.toNativeString();
370 		ret.name = this.name;
371 		ret.version_ = this.ver;
372 		ret.description = m_info.description;
373 		ret.homepage = m_info.homepage;
374 		ret.authors = m_info.authors.dup;
375 		ret.copyright = m_info.copyright;
376 		ret.license = m_info.license;
377 		ret.dependencies = getDependencies(config).keys;
378 
379 		// save build settings
380 		BuildSettings bs = getBuildSettings(platform, config);
381 		BuildSettings allbs = getCombinedBuildSettings();
382 
383 		ret.targetType = bs.targetType;
384 		ret.targetPath = bs.targetPath;
385 		ret.targetName = bs.targetName;
386 		if (ret.targetType != TargetType.none)
387 			ret.targetFileName = getTargetFileName(bs, platform);
388 		ret.workingDirectory = bs.workingDirectory;
389 		ret.mainSourceFile = bs.mainSourceFile;
390 		ret.dflags = bs.dflags;
391 		ret.lflags = bs.lflags;
392 		ret.libs = bs.libs;
393 		ret.copyFiles = bs.copyFiles;
394 		ret.versions = bs.versions;
395 		ret.debugVersions = bs.debugVersions;
396 		ret.importPaths = bs.importPaths;
397 		ret.stringImportPaths = bs.stringImportPaths;
398 		ret.preGenerateCommands = bs.preGenerateCommands;
399 		ret.postGenerateCommands = bs.postGenerateCommands;
400 		ret.preBuildCommands = bs.preBuildCommands;
401 		ret.postBuildCommands = bs.postBuildCommands;
402 
403 		// prettify build requirements output
404 		for (int i = 1; i <= BuildRequirement.max; i <<= 1)
405 			if (bs.requirements & cast(BuildRequirement)i)
406 				ret.buildRequirements ~= cast(BuildRequirement)i;
407 
408 		// prettify options output
409 		for (int i = 1; i <= BuildOption.max; i <<= 1)
410 			if (bs.options & cast(BuildOption)i)
411 				ret.options ~= cast(BuildOption)i;
412 
413 		// collect all possible source files and determine their types
414 		SourceFileRole[string] sourceFileTypes;
415 		foreach (f; allbs.stringImportFiles) sourceFileTypes[f] = SourceFileRole.unusedStringImport;
416 		foreach (f; allbs.importFiles) sourceFileTypes[f] = SourceFileRole.unusedImport;
417 		foreach (f; allbs.sourceFiles) sourceFileTypes[f] = SourceFileRole.unusedSource;
418 		foreach (f; bs.stringImportFiles) sourceFileTypes[f] = SourceFileRole.stringImport;
419 		foreach (f; bs.importFiles) sourceFileTypes[f] = SourceFileRole.import_;
420 		foreach (f; bs.sourceFiles) sourceFileTypes[f] = SourceFileRole.source;
421 		foreach (f; sourceFileTypes.byKey.array.sort()) {
422 			SourceFileDescription sf;
423 			sf.path = f;
424 			sf.type = sourceFileTypes[f];
425 			ret.files ~= sf;
426 		}
427 
428 		return ret;
429 	}
430 	// ditto
431 	deprecated void describe(ref Json dst, BuildPlatform platform, string config)
432 	{
433 		auto res = describe(platform, config);
434 		foreach (string key, value; res.serializeToJson())
435 			dst[key] = value;
436 	}
437 
438 	private void fillWithDefaults()
439 	{
440 		auto bs = &m_info.buildSettings;
441 
442 		// check for default string import folders
443 		if ("" !in bs.stringImportPaths) {
444 			foreach(defvf; ["views"]){
445 				if( existsFile(m_path ~ defvf) )
446 					bs.stringImportPaths[""] ~= defvf;
447 			}
448 		}
449 
450 		// check for default source folders
451 		immutable hasSP = ("" in bs.sourcePaths) !is null;
452 		immutable hasIP = ("" in bs.importPaths) !is null;
453 		if (!hasSP || !hasIP) {
454 			foreach (defsf; ["source/", "src/"]) {
455 				if (existsFile(m_path ~ defsf)) {
456 					if (!hasSP) bs.sourcePaths[""] ~= defsf;
457 					if (!hasIP) bs.importPaths[""] ~= defsf;
458 				}
459 			}
460 		}
461 
462 		// check for default app_main
463 		string app_main_file;
464 		auto pkg_name = m_info.name.length ? m_info.name : "unknown";
465 		foreach(sf; bs.sourcePaths.get("", null)){
466 			auto p = m_path ~ sf;
467 			if( !existsFile(p) ) continue;
468 			foreach(fil; ["app.d", "main.d", pkg_name ~ "/main.d", pkg_name ~ "/" ~ "app.d"]){
469 				if( existsFile(p ~ fil) ) {
470 					app_main_file = (Path(sf) ~ fil).toNativeString();
471 					break;
472 				}
473 			}
474 		}
475 
476 		// generate default configurations if none are defined
477 		if (m_info.configurations.length == 0) {
478 			if (bs.targetType == TargetType.executable) {
479 				BuildSettingsTemplate app_settings;
480 				app_settings.targetType = TargetType.executable;
481 				if (bs.mainSourceFile.empty) app_settings.mainSourceFile = app_main_file;
482 				m_info.configurations ~= ConfigurationInfo("application", app_settings);
483 			} else if (bs.targetType != TargetType.none) {
484 				BuildSettingsTemplate lib_settings;
485 				lib_settings.targetType = bs.targetType == TargetType.autodetect ? TargetType.library : bs.targetType;
486 
487 				if (bs.targetType == TargetType.autodetect) {
488 					if (app_main_file.length) {
489 						lib_settings.excludedSourceFiles[""] ~= app_main_file;
490 
491 						BuildSettingsTemplate app_settings;
492 						app_settings.targetType = TargetType.executable;
493 						app_settings.mainSourceFile = app_main_file;
494 						m_info.configurations ~= ConfigurationInfo("application", app_settings);
495 					}
496 				}
497 
498 				m_info.configurations ~= ConfigurationInfo("library", lib_settings);
499 			}
500 		}
501 	}
502 
503 	private void simpleLint() const {
504 		if (m_parentPackage) {
505 			if (m_parentPackage.path != path) {
506 				if (info.license.length && info.license != m_parentPackage.info.license)
507 					logWarn("License in subpackage %s is different than it's parent package, this is discouraged.", name);
508 			}
509 		}
510 		if (name.empty) logWarn("The package in %s has no name.", path);
511 	}
512 
513 	private static RawPackage rawPackageFromFile(PathAndFormat file, bool silent_fail = false) {
514 		if( silent_fail && !existsFile(file.path) ) return null;
515 
516 		string text;
517 
518 		{
519 			auto f = openFile(file.path.toNativeString(), FileMode.Read);
520 			scope(exit) f.close();
521 			text = stripUTF8Bom(cast(string)f.readAll());
522 		}
523 
524 		final switch(file.format) {
525 			case PackageFormat.json:
526 				return new JsonPackage(parseJsonString(text, file.path.toNativeString()));
527 			case PackageFormat.sdl:
528 				if(silent_fail) return null; throw new Exception("SDL not implemented");
529 		}
530 	}
531 
532 	static abstract class RawPackage
533 	{
534 		string package_name; // Should already be lower case
535 		string version_;
536 		abstract void parseInto(ref PackageRecipe package_, string parent_name);
537 	}
538 	private static class JsonPackage : RawPackage
539 	{
540 		Json json;
541 		this(Json json) {
542 			this.json = json;
543 
544 			string nameLower;
545 			if(json.type == Json.Type..string) {
546 				nameLower = json.get!string.toLower();
547 				this.json = nameLower;
548 			} else {
549 				nameLower = json.name.get!string.toLower();
550 				this.json.name = nameLower;
551 				this.package_name = nameLower;
552 
553 				Json versionJson = json["version"];
554 				this.version_ = (versionJson.type == Json.Type.undefined) ? null : versionJson.get!string;
555 			}
556 
557 			this.package_name = nameLower;
558 		}
559 		override void parseInto(ref PackageRecipe recipe, string parent_name)
560 		{
561 			recipe.parseJson(json, parent_name);
562 		}
563 	}
564 	private static class SdlPackage : RawPackage
565 	{
566 		override void parseInto(ref PackageRecipe package_, string parent_name)
567 		{
568 			throw new Exception("SDL packages not implemented yet");
569 		}
570 	}
571 }
572 
573 private string determineVersionFromSCM(Path path)
574 {
575 	import std.process;
576 	import dub.semver;
577 
578 	auto git_dir = path ~ ".git";
579 	if (!existsFile(git_dir) || !isDir(git_dir.toNativeString)) return null;
580 	auto git_dir_param = "--git-dir=" ~ git_dir.toNativeString();
581 
582 	static string exec(scope string[] params...) {
583 		auto ret = executeShell(escapeShellCommand(params));
584 		if (ret.status == 0) return ret.output.strip;
585 		logDebug("'%s' failed with exit code %s: %s", params.join(" "), ret.status, ret.output.strip);
586 		return null;
587 	}
588 
589 	auto tag = exec("git", git_dir_param, "describe", "--long", "--tags");
590 	if (tag !is null) {
591 		auto parts = tag.split("-");
592 		auto commit = parts[$-1];
593 		auto num = parts[$-2].to!int;
594 		tag = parts[0 .. $-2].join("-");
595 		if (tag.startsWith("v") && isValidVersion(tag[1 .. $])) {
596 			if (num == 0) return tag[1 .. $];
597 			else if (tag.canFind("+")) return format("%s.commit.%s.%s", tag[1 .. $], num, commit);
598 			else return format("%s+commit.%s.%s", tag[1 .. $], num, commit);
599 		}
600 	}
601 
602 	auto branch = exec("git", git_dir_param, "rev-parse", "--abbrev-ref", "HEAD");
603 	if (branch !is null) {
604 		if (branch != "HEAD") return "~" ~ branch;
605 	}
606 
607 	return null;
608 }