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.url;
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 
24 
25 /**
26 	Returns the individual parts of a qualified package name.
27 
28 	Sub qualified package names are lists of package names separated by ":". For
29 	example, "packa:packb:packc" references a package named "packc" that is a
30 	sub package of "packb", which in turn is a sub package of "packa".
31 */
32 string[] getSubPackagePath(string package_name)
33 {
34 	return package_name.split(":");
35 }
36 
37 /**
38 	Returns the name of the top level package for a given (sub) package name.
39 
40 	In case of a top level package, the qualified name is returned unmodified.
41 */
42 string getBasePackageName(string package_name)
43 {
44 	return package_name.findSplit(":")[0];
45 }
46 
47 /**
48 	Returns the qualified sub package part of the given package name.
49 
50 	This is the part of the package name excluding the base package
51 	name. See also $(D getBasePackageName).
52 */
53 string getSubPackageName(string package_name)
54 {
55 	return package_name.findSplit(":")[2];
56 }
57 
58 unittest
59 {
60 	assert(getSubPackagePath("packa:packb:packc") == ["packa", "packb", "packc"]);
61 	assert(getSubPackagePath("pack") == ["pack"]);
62 	assert(getBasePackageName("packa:packb:packc") == "packa");
63 	assert(getBasePackageName("pack") == "pack");
64 	assert(getSubPackageName("packa:packb:packc") == "packb:packc");
65 	assert(getSubPackageName("pack") == "");
66 }
67 
68 /**
69 	Represents the contents of a package recipe file (dub.json/dub.sdl) in an abstract way.
70 
71 	This structure is used to reason about package descriptions in isolation.
72 	For higher level package handling, see the $(D Package) class.
73 */
74 struct PackageRecipe {
75 	string name;
76 	string version_;
77 	string description;
78 	string homepage;
79 	string[] authors;
80 	string copyright;
81 	string license;
82 	string[] ddoxFilterArgs;
83 	string ddoxTool;
84 	BuildSettingsTemplate buildSettings;
85 	ConfigurationInfo[] configurations;
86 	BuildSettingsTemplate[string] buildTypes;
87 
88 	SubPackage[] subPackages;
89 
90 	inout(ConfigurationInfo) getConfiguration(string name)
91 	inout {
92 		foreach (c; configurations)
93 			if (c.name == name)
94 				return c;
95 		throw new Exception("Unknown configuration: "~name);
96 	}
97 
98 	/** Clones the package recipe recursively.
99 	*/
100 	PackageRecipe clone() const { return .clone(this); }
101 }
102 
103 struct SubPackage
104 {
105 	string path;
106 	PackageRecipe recipe;
107 }
108 
109 
110 /// Bundles information about a build configuration.
111 struct ConfigurationInfo {
112 	string name;
113 	string[] platforms;
114 	BuildSettingsTemplate buildSettings;
115 
116 	this(string name, BuildSettingsTemplate build_settings)
117 	{
118 		enforce(!name.empty, "Configuration name is empty.");
119 		this.name = name;
120 		this.buildSettings = build_settings;
121 	}
122 
123 	bool matchesPlatform(in BuildPlatform platform)
124 	const {
125 		if( platforms.empty ) return true;
126 		foreach(p; platforms)
127 			if( platform.matchesSpecification("-"~p) )
128 				return true;
129 		return false;
130 	}
131 }
132 
133 /// This keeps general information about how to build a package.
134 /// It contains functions to create a specific BuildSetting, targeted at
135 /// a certain BuildPlatform.
136 struct BuildSettingsTemplate {
137 	Dependency[string] dependencies;
138 	string systemDependencies;
139 	TargetType targetType = TargetType.autodetect;
140 	string targetPath;
141 	string targetName;
142 	string workingDirectory;
143 	string mainSourceFile;
144 	string[string] subConfigurations;
145 	string[][string] dflags;
146 	string[][string] lflags;
147 	string[][string] libs;
148 	string[][string] sourceFiles;
149 	string[][string] sourcePaths;
150 	string[][string] excludedSourceFiles;
151 	string[][string] copyFiles;
152 	string[][string] versions;
153 	string[][string] debugVersions;
154 	string[][string] importPaths;
155 	string[][string] stringImportPaths;
156 	string[][string] preGenerateCommands;
157 	string[][string] postGenerateCommands;
158 	string[][string] preBuildCommands;
159 	string[][string] postBuildCommands;
160 	BuildRequirements[string] buildRequirements;
161 	BuildOptions[string] buildOptions;
162 
163 
164 	/// Constructs a BuildSettings object from this template.
165 	void getPlatformSettings(ref BuildSettings dst, in BuildPlatform platform, NativePath base_path)
166 	const {
167 		dst.targetType = this.targetType;
168 		if (!this.targetPath.empty) dst.targetPath = this.targetPath;
169 		if (!this.targetName.empty) dst.targetName = this.targetName;
170 		if (!this.workingDirectory.empty) dst.workingDirectory = this.workingDirectory;
171 		if (!this.mainSourceFile.empty) {
172 			dst.mainSourceFile = this.mainSourceFile;
173 			dst.addSourceFiles(this.mainSourceFile);
174 		}
175 
176 		string[] collectFiles(in string[][string] paths_map, string pattern)
177 		{
178 			auto files = appender!(string[]);
179 
180 			foreach (suffix, paths; paths_map) {
181 				if (!platform.matchesSpecification(suffix))
182 					continue;
183 
184 				foreach (spath; paths) {
185 					enforce(!spath.empty, "Paths must not be empty strings.");
186 					auto path = NativePath(spath);
187 					if (!path.absolute) path = base_path ~ path;
188 					if (!existsFile(path) || !isDir(path.toNativeString())) {
189 						logWarn("Invalid source/import path: %s", path.toNativeString());
190 						continue;
191 					}
192 
193 					foreach (d; dirEntries(path.toNativeString(), pattern, SpanMode.depth)) {
194 						import std.path : baseName;
195 						if (baseName(d.name)[0] == '.' || d.isDir) continue;
196 						auto src = NativePath(d.name).relativeTo(base_path);
197 						files ~= src.toNativeString();
198 					}
199 				}
200 			}
201 
202 			return files.data;
203 		}
204 
205  		// collect source files
206 		dst.addSourceFiles(collectFiles(sourcePaths, "*.d"));
207 		auto sourceFiles = dst.sourceFiles.sort();
208 
209  		// collect import files and remove sources
210 		import std.algorithm : copy, setDifference;
211 
212 		auto importFiles = collectFiles(importPaths, "*.{d,di}").sort();
213 		immutable nremoved = importFiles.setDifference(sourceFiles).copy(importFiles.release).length;
214 		importFiles = importFiles[0 .. $ - nremoved];
215 		dst.addImportFiles(importFiles.release);
216 
217 		dst.addStringImportFiles(collectFiles(stringImportPaths, "*"));
218 
219 		getPlatformSetting!("dflags", "addDFlags")(dst, platform);
220 		getPlatformSetting!("lflags", "addLFlags")(dst, platform);
221 		getPlatformSetting!("libs", "addLibs")(dst, platform);
222 		getPlatformSetting!("sourceFiles", "addSourceFiles")(dst, platform);
223 		getPlatformSetting!("excludedSourceFiles", "removeSourceFiles")(dst, platform);
224 		getPlatformSetting!("copyFiles", "addCopyFiles")(dst, platform);
225 		getPlatformSetting!("versions", "addVersions")(dst, platform);
226 		getPlatformSetting!("debugVersions", "addDebugVersions")(dst, platform);
227 		getPlatformSetting!("importPaths", "addImportPaths")(dst, platform);
228 		getPlatformSetting!("stringImportPaths", "addStringImportPaths")(dst, platform);
229 		getPlatformSetting!("preGenerateCommands", "addPreGenerateCommands")(dst, platform);
230 		getPlatformSetting!("postGenerateCommands", "addPostGenerateCommands")(dst, platform);
231 		getPlatformSetting!("preBuildCommands", "addPreBuildCommands")(dst, platform);
232 		getPlatformSetting!("postBuildCommands", "addPostBuildCommands")(dst, platform);
233 		getPlatformSetting!("buildRequirements", "addRequirements")(dst, platform);
234 		getPlatformSetting!("buildOptions", "addOptions")(dst, platform);
235 	}
236 
237 	void getPlatformSetting(string name, string addname)(ref BuildSettings dst, in BuildPlatform platform)
238 	const {
239 		foreach(suffix, values; __traits(getMember, this, name)){
240 			if( platform.matchesSpecification(suffix) )
241 				__traits(getMember, dst, addname)(values);
242 		}
243 	}
244 
245 	void warnOnSpecialCompilerFlags(string package_name, string config_name)
246 	{
247 		auto nodef = false;
248 		auto noprop = false;
249 		foreach (req; this.buildRequirements) {
250 			if (req & BuildRequirement.noDefaultFlags) nodef = true;
251 			if (req & BuildRequirement.relaxProperties) noprop = true;
252 		}
253 
254 		if (noprop) {
255 			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.`);
256 			logWarn("");
257 		}
258 
259 		if (nodef) {
260 			logWarn("Warning: This package uses the \"noDefaultFlags\" build requirement. Please use only for development purposes and not for released packages.");
261 			logWarn("");
262 		} else {
263 			string[] all_dflags;
264 			BuildOptions all_options;
265 			foreach (flags; this.dflags) all_dflags ~= flags;
266 			foreach (options; this.buildOptions) all_options |= options;
267 			.warnOnSpecialCompilerFlags(all_dflags, all_options, package_name, config_name);
268 		}
269 	}
270 }
271 
272 private T clone(T)(ref const(T) val)
273 {
274 	import std.traits : isSomeString, isDynamicArray, isAssociativeArray, isBasicType, ValueType;
275 
276 	static if (is(T == immutable)) return val;
277 	else static if (isBasicType!T) return val;
278 	else static if (isDynamicArray!T) {
279 		alias V = typeof(T.init[0]);
280 		static if (is(V == immutable)) return val;
281 		else {
282 			T ret = new V[val.length];
283 			foreach (i, ref f; val)
284 				ret[i] = clone!V(f);
285 			return ret;
286 		}
287 	} else static if (isAssociativeArray!T) {
288 		alias V = ValueType!T;
289 		T ret;
290 		foreach (k, ref f; val)
291 			ret[k] = clone!V(f);
292 		return ret;
293 	} else static if (is(T == struct)) {
294 		T ret;
295 		foreach (i, M; typeof(T.tupleof))
296 			ret.tupleof[i] = clone!M(val.tupleof[i]);
297 		return ret;
298 	} else static assert(false, "Unsupported type: "~T.stringof);
299 }