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 
14 import dub.internal.vibecompat.core.file;
15 import dub.internal.vibecompat.core.log;
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.file;
22 import std.range;
23 import std.process : environment;
24 
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 string[] getSubPackagePath(string package_name) @safe pure
34 {
35 	return package_name.split(":");
36 }
37 
38 /**
39 	Returns the name of the top level package for a given (sub) package name.
40 
41 	In case of a top level package, the qualified name is returned unmodified.
42 */
43 string getBasePackageName(string package_name) @safe pure
44 {
45 	return package_name.findSplit(":")[0];
46 }
47 
48 /**
49 	Returns the qualified sub package part of the given package name.
50 
51 	This is the part of the package name excluding the base package
52 	name. See also $(D getBasePackageName).
53 */
54 string getSubPackageName(string package_name) @safe pure
55 {
56 	return package_name.findSplit(":")[2];
57 }
58 
59 @safe unittest
60 {
61 	assert(getSubPackagePath("packa:packb:packc") == ["packa", "packb", "packc"]);
62 	assert(getSubPackagePath("pack") == ["pack"]);
63 	assert(getBasePackageName("packa:packb:packc") == "packa");
64 	assert(getBasePackageName("pack") == "pack");
65 	assert(getSubPackageName("packa:packb:packc") == "packb:packc");
66 	assert(getSubPackageName("pack") == "");
67 }
68 
69 /**
70 	Represents the contents of a package recipe file (dub.json/dub.sdl) in an abstract way.
71 
72 	This structure is used to reason about package descriptions in isolation.
73 	For higher level package handling, see the $(D Package) class.
74 */
75 struct PackageRecipe {
76 	string name;
77 	string version_;
78 	string description;
79 	string homepage;
80 	string[] authors;
81 	string copyright;
82 	string license;
83 	string[] ddoxFilterArgs;
84 	string ddoxTool;
85 	BuildSettingsTemplate buildSettings;
86 	ConfigurationInfo[] configurations;
87 	BuildSettingsTemplate[string] buildTypes;
88 
89 	ToolchainRequirements toolchainRequirements;
90 
91 	SubPackage[] subPackages;
92 
93 	inout(ConfigurationInfo) getConfiguration(string name)
94 	inout {
95 		foreach (c; configurations)
96 			if (c.name == name)
97 				return c;
98 		throw new Exception("Unknown configuration: "~name);
99 	}
100 
101 	/** Clones the package recipe recursively.
102 	*/
103 	PackageRecipe clone() const { return .clone(this); }
104 }
105 
106 struct SubPackage
107 {
108 	string path;
109 	PackageRecipe recipe;
110 }
111 
112 /// Describes minimal toolchain requirements
113 struct ToolchainRequirements
114 {
115 	import std.typecons : Tuple, tuple;
116 
117 	/// DUB version requirement
118 	Dependency dub = Dependency.any;
119 	/// D front-end version requirement
120 	Dependency frontend = Dependency.any;
121 	/// DMD version requirement
122 	Dependency dmd = Dependency.any;
123 	/// LDC version requirement
124 	Dependency ldc = Dependency.any;
125 	/// GDC version requirement
126 	Dependency gdc = Dependency.any;
127 
128 	/** Get the list of supported compilers.
129 
130 		Returns:
131 			An array of couples of compiler name and compiler requirement
132 	*/
133 	@property Tuple!(string, Dependency)[] supportedCompilers() const
134 	{
135 		Tuple!(string, Dependency)[] res;
136 		if (dmd != Dependency.invalid) res ~= Tuple!(string, Dependency)("dmd", dmd);
137 		if (ldc != Dependency.invalid) res ~= Tuple!(string, Dependency)("ldc", ldc);
138 		if (gdc != Dependency.invalid) res ~= Tuple!(string, Dependency)("gdc", gdc);
139 		return res;
140 	}
141 
142 	bool empty()
143 	const {
144 		import std.algorithm.searching : all;
145 		return only(dub, frontend, dmd, ldc, gdc)
146 			.all!(r => r == Dependency.any);
147 	}
148 }
149 
150 
151 /// Bundles information about a build configuration.
152 struct ConfigurationInfo {
153 	string name;
154 	string[] platforms;
155 	BuildSettingsTemplate buildSettings;
156 
157 	this(string name, BuildSettingsTemplate build_settings)
158 	{
159 		enforce(!name.empty, "Configuration name is empty.");
160 		this.name = name;
161 		this.buildSettings = build_settings;
162 	}
163 
164 	bool matchesPlatform(in BuildPlatform platform)
165 	const {
166 		if( platforms.empty ) return true;
167 		foreach(p; platforms)
168 			if( platform.matchesSpecification("-"~p) )
169 				return true;
170 		return false;
171 	}
172 }
173 
174 /// This keeps general information about how to build a package.
175 /// It contains functions to create a specific BuildSetting, targeted at
176 /// a certain BuildPlatform.
177 struct BuildSettingsTemplate {
178 	Dependency[string] dependencies;
179 	BuildSettingsTemplate[string] dependencyBuildSettings;
180 	string systemDependencies;
181 	TargetType targetType = TargetType.autodetect;
182 	string targetPath;
183 	string targetName;
184 	string workingDirectory;
185 	string mainSourceFile;
186 	string[string] subConfigurations;
187 	string[][string] dflags;
188 	string[][string] lflags;
189 	string[][string] libs;
190 	string[][string] sourceFiles;
191 	string[][string] sourcePaths;
192 	string[][string] excludedSourceFiles;
193 	string[][string] injectSourceFiles;
194 	string[][string] copyFiles;
195 	string[][string] extraDependencyFiles;
196 	string[][string] versions;
197 	string[][string] debugVersions;
198 	string[][string] versionFilters;
199 	string[][string] debugVersionFilters;
200 	string[][string] importPaths;
201 	string[][string] stringImportPaths;
202 	string[][string] preGenerateCommands;
203 	string[][string] postGenerateCommands;
204 	string[][string] preBuildCommands;
205 	string[][string] postBuildCommands;
206 	string[][string] preRunCommands;
207 	string[][string] postRunCommands;
208 	string[string][string] environments;
209 	string[string][string] buildEnvironments;
210 	string[string][string] runEnvironments;
211 	string[string][string] preGenerateEnvironments;
212 	string[string][string] postGenerateEnvironments;
213 	string[string][string] preBuildEnvironments;
214 	string[string][string] postBuildEnvironments;
215 	string[string][string] preRunEnvironments;
216 	string[string][string] postRunEnvironments;
217 	BuildRequirements[string] buildRequirements;
218 	BuildOptions[string] buildOptions;
219 
220 
221 	/// Constructs a BuildSettings object from this template.
222 	void getPlatformSettings(ref BuildSettings dst, in BuildPlatform platform, NativePath base_path)
223 	const {
224 		dst.targetType = this.targetType;
225 		if (!this.targetPath.empty) dst.targetPath = this.targetPath;
226 		if (!this.targetName.empty) dst.targetName = this.targetName;
227 		if (!this.workingDirectory.empty) dst.workingDirectory = this.workingDirectory;
228 		if (!this.mainSourceFile.empty) {
229 			auto p = NativePath(this.mainSourceFile);
230 			p.normalize();
231 			dst.mainSourceFile = p.toNativeString();
232 			dst.addSourceFiles(dst.mainSourceFile);
233 		}
234 
235 		string[] collectFiles(in string[][string] paths_map, string pattern)
236 		{
237 			auto files = appender!(string[]);
238 
239 			import dub.project : buildSettingsVars;
240 			import std.typecons : Nullable;
241 
242 			static Nullable!(string[string]) envVarCache;
243 
244 			if (envVarCache.isNull) envVarCache = environment.toAA();
245 
246 			foreach (suffix, paths; paths_map) {
247 				if (!platform.matchesSpecification(suffix))
248 					continue;
249 
250 				foreach (spath; paths) {
251 					enforce(!spath.empty, "Paths must not be empty strings.");
252 					auto path = NativePath(spath);
253 					if (!path.absolute) path = base_path ~ path;
254 					if (!existsFile(path) || !isDir(path.toNativeString())) {
255 						import std.algorithm : any, find;
256 						const hasVar = chain(buildSettingsVars, envVarCache.get.byKey).any!((string var) {
257 							return spath.find("$"~var).length > 0 || spath.find("${"~var~"}").length > 0;
258 						});
259 						if (!hasVar)
260 							logWarn("Invalid source/import path: %s", path.toNativeString());
261 						continue;
262 					}
263 
264 					auto pstr = path.toNativeString();
265 					foreach (d; dirEntries(pstr, pattern, SpanMode.depth)) {
266 						import std.path : baseName, pathSplitter;
267 						import std.algorithm.searching : canFind;
268 						// eliminate any hidden files, or files in hidden directories. But always include
269 						// files that are listed inside hidden directories that are specifically added to
270 						// the project.
271 						if (d.isDir || pathSplitter(d.name[pstr.length .. $])
272 								   .canFind!(name => name.length && name[0] == '.'))
273 							continue;
274 						auto src = NativePath(d.name).relativeTo(base_path);
275 						files ~= src.toNativeString();
276 					}
277 				}
278 			}
279 
280 			return files.data;
281 		}
282 
283  		// collect source files
284 		dst.addSourceFiles(collectFiles(sourcePaths, "*.d"));
285 		auto sourceFiles = dst.sourceFiles.sort();
286 
287  		// collect import files and remove sources
288 		import std.algorithm : copy, setDifference;
289 
290 		auto importFiles = collectFiles(importPaths, "*.{d,di}").sort();
291 		immutable nremoved = importFiles.setDifference(sourceFiles).copy(importFiles.release).length;
292 		importFiles = importFiles[0 .. $ - nremoved];
293 		dst.addImportFiles(importFiles.release);
294 
295 		dst.addStringImportFiles(collectFiles(stringImportPaths, "*"));
296 
297 		getPlatformSetting!("dflags", "addDFlags")(dst, platform);
298 		getPlatformSetting!("lflags", "addLFlags")(dst, platform);
299 		getPlatformSetting!("libs", "addLibs")(dst, platform);
300 		getPlatformSetting!("sourceFiles", "addSourceFiles")(dst, platform);
301 		getPlatformSetting!("excludedSourceFiles", "removeSourceFiles")(dst, platform);
302 		getPlatformSetting!("injectSourceFiles", "addInjectSourceFiles")(dst, platform);
303 		getPlatformSetting!("copyFiles", "addCopyFiles")(dst, platform);
304 		getPlatformSetting!("extraDependencyFiles", "addExtraDependencyFiles")(dst, platform);
305 		getPlatformSetting!("versions", "addVersions")(dst, platform);
306 		getPlatformSetting!("debugVersions", "addDebugVersions")(dst, platform);
307 		getPlatformSetting!("versionFilters", "addVersionFilters")(dst, platform);
308 		getPlatformSetting!("debugVersionFilters", "addDebugVersionFilters")(dst, platform);
309 		getPlatformSetting!("importPaths", "addImportPaths")(dst, platform);
310 		getPlatformSetting!("stringImportPaths", "addStringImportPaths")(dst, platform);
311 		getPlatformSetting!("preGenerateCommands", "addPreGenerateCommands")(dst, platform);
312 		getPlatformSetting!("postGenerateCommands", "addPostGenerateCommands")(dst, platform);
313 		getPlatformSetting!("preBuildCommands", "addPreBuildCommands")(dst, platform);
314 		getPlatformSetting!("postBuildCommands", "addPostBuildCommands")(dst, platform);
315 		getPlatformSetting!("preRunCommands", "addPreRunCommands")(dst, platform);
316 		getPlatformSetting!("postRunCommands", "addPostRunCommands")(dst, platform);
317 		getPlatformSetting!("environments", "addEnvironments")(dst, platform);
318 		getPlatformSetting!("buildEnvironments", "addBuildEnvironments")(dst, platform);
319 		getPlatformSetting!("runEnvironments", "addRunEnvironments")(dst, platform);
320 		getPlatformSetting!("preGenerateEnvironments", "addPreGenerateEnvironments")(dst, platform);
321 		getPlatformSetting!("postGenerateEnvironments", "addPostGenerateEnvironments")(dst, platform);
322 		getPlatformSetting!("preBuildEnvironments", "addPreBuildEnvironments")(dst, platform);
323 		getPlatformSetting!("postBuildEnvironments", "addPostBuildEnvironments")(dst, platform);
324 		getPlatformSetting!("preRunEnvironments", "addPreRunEnvironments")(dst, platform);
325 		getPlatformSetting!("postRunEnvironments", "addPostRunEnvironments")(dst, platform);
326 		getPlatformSetting!("buildRequirements", "addRequirements")(dst, platform);
327 		getPlatformSetting!("buildOptions", "addOptions")(dst, platform);
328 	}
329 
330 	void getPlatformSetting(string name, string addname)(ref BuildSettings dst, in BuildPlatform platform)
331 	const {
332 		foreach(suffix, values; __traits(getMember, this, name)){
333 			if( platform.matchesSpecification(suffix) )
334 				__traits(getMember, dst, addname)(values);
335 		}
336 	}
337 
338 	void warnOnSpecialCompilerFlags(string package_name, string config_name)
339 	{
340 		auto nodef = false;
341 		auto noprop = false;
342 		foreach (req; this.buildRequirements) {
343 			if (req & BuildRequirement.noDefaultFlags) nodef = true;
344 			if (req & BuildRequirement.relaxProperties) noprop = true;
345 		}
346 
347 		if (noprop) {
348 			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.`);
349 			logWarn("");
350 		}
351 
352 		if (nodef) {
353 			logWarn("Warning: This package uses the \"noDefaultFlags\" build requirement. Please use only for development purposes and not for released packages.");
354 			logWarn("");
355 		} else {
356 			string[] all_dflags;
357 			BuildOptions all_options;
358 			foreach (flags; this.dflags) all_dflags ~= flags;
359 			foreach (options; this.buildOptions) all_options |= options;
360 			.warnOnSpecialCompilerFlags(all_dflags, all_options, package_name, config_name);
361 		}
362 	}
363 }
364 
365 package(dub) void checkPlatform(const scope ref ToolchainRequirements tr, BuildPlatform platform, string package_name)
366 {
367 	import dub.compilers.utils : dmdLikeVersionToSemverLike;
368 	import std.algorithm.iteration : map;
369 	import std.format : format;
370 
371 	string compilerver;
372 	Dependency compilerspec;
373 
374 	switch (platform.compiler) {
375 		default:
376 			compilerspec = Dependency.any;
377 			compilerver = "0.0.0";
378 			break;
379 		case "dmd":
380 			compilerspec = tr.dmd;
381 			compilerver = platform.compilerVersion.length
382 				? dmdLikeVersionToSemverLike(platform.compilerVersion)
383 				: "0.0.0";
384 			break;
385 		case "ldc":
386 			compilerspec = tr.ldc;
387 			compilerver = platform.compilerVersion;
388 			if (!compilerver.length) compilerver = "0.0.0";
389 			break;
390 		case "gdc":
391 			compilerspec = tr.gdc;
392 			compilerver = platform.compilerVersion;
393 			if (!compilerver.length) compilerver = "0.0.0";
394 			break;
395 	}
396 
397 	enforce(compilerspec != Dependency.invalid,
398 		format(
399 			"Installed %s %s is not supported by %s. Supported compiler(s):\n%s",
400 			platform.compiler, platform.compilerVersion, package_name,
401 			tr.supportedCompilers.map!((cs) {
402 				auto str = "  - " ~ cs[0];
403 				if (cs[1] != Dependency.any) str ~= ": " ~ cs[1].toString();
404 				return str;
405 			}).join("\n")
406 		)
407 	);
408 
409 	enforce(compilerspec.matches(compilerver),
410 		format(
411 			"Installed %s-%s does not comply with %s compiler requirement: %s %s\n" ~
412 			"Please consider upgrading your installation.",
413 			platform.compiler, platform.compilerVersion,
414 			package_name, platform.compiler, compilerspec
415 		)
416 	);
417 
418 	enforce(tr.frontend.matches(dmdLikeVersionToSemverLike(platform.frontendVersionString)),
419 		format(
420 			"Installed %s-%s with frontend %s does not comply with %s frontend requirement: %s\n" ~
421 			"Please consider upgrading your installation.",
422 			platform.compiler, platform.compilerVersion,
423 			platform.frontendVersionString, package_name, tr.frontend
424 		)
425 	);
426 }
427 
428 package bool addRequirement(ref ToolchainRequirements req, string name, string value)
429 {
430 	switch (name) {
431 		default: return false;
432 		case "dub": req.dub = parseDependency(value); break;
433 		case "frontend": req.frontend = parseDMDDependency(value); break;
434 		case "ldc": req.ldc = parseDependency(value); break;
435 		case "gdc": req.gdc = parseDependency(value); break;
436 		case "dmd": req.dmd = parseDMDDependency(value); break;
437 	}
438 	return true;
439 }
440 
441 private static Dependency parseDependency(string dep)
442 {
443 	if (dep == "no") return Dependency.invalid;
444 	return Dependency(dep);
445 }
446 
447 private static Dependency parseDMDDependency(string dep)
448 {
449 	import dub.compilers.utils : dmdLikeVersionToSemverLike;
450 	import dub.dependency : Dependency;
451 	import std.algorithm : map, splitter;
452 	import std.array : join;
453 
454 	if (dep == "no") return Dependency.invalid;
455 	return dep
456 		.splitter(' ')
457 		.map!(r => dmdLikeVersionToSemverLike(r))
458 		.join(' ')
459 		.Dependency;
460 }
461 
462 private T clone(T)(ref const(T) val)
463 {
464 	import std.traits : isSomeString, isDynamicArray, isAssociativeArray, isBasicType, ValueType;
465 
466 	static if (is(T == immutable)) return val;
467 	else static if (isBasicType!T) return val;
468 	else static if (isDynamicArray!T) {
469 		alias V = typeof(T.init[0]);
470 		static if (is(V == immutable)) return val;
471 		else {
472 			T ret = new V[val.length];
473 			foreach (i, ref f; val)
474 				ret[i] = clone!V(f);
475 			return ret;
476 		}
477 	} else static if (isAssociativeArray!T) {
478 		alias V = ValueType!T;
479 		T ret;
480 		foreach (k, ref f; val)
481 			ret[k] = clone!V(f);
482 		return ret;
483 	} else static if (is(T == struct)) {
484 		T ret;
485 		foreach (i, M; typeof(T.tupleof))
486 			ret.tupleof[i] = clone!M(val.tupleof[i]);
487 		return ret;
488 	} else static assert(false, "Unsupported type: "~T.stringof);
489 }
490 
491 unittest { // issue #1407 - duplicate main source file
492 	{
493 		BuildSettingsTemplate t;
494 		t.mainSourceFile = "./foo.d";
495 		t.sourceFiles[""] = ["foo.d"];
496 		BuildSettings bs;
497 		t.getPlatformSettings(bs, BuildPlatform.init, NativePath("/"));
498 		assert(bs.sourceFiles == ["foo.d"]);
499 	}
500 
501 	version (Windows) {{
502 		BuildSettingsTemplate t;
503 		t.mainSourceFile = "src/foo.d";
504 		t.sourceFiles[""] = ["src\\foo.d"];
505 		BuildSettings bs;
506 		t.getPlatformSettings(bs, BuildPlatform.init, NativePath("/"));
507 		assert(bs.sourceFiles == ["src\\foo.d"]);
508 	}}
509 }