1 /**
2 	Utility functionality for compiler class implementations.
3 
4 	Copyright: © 2013-2016 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
7 */
8 module dub.compilers.utils;
9 
10 import dub.compilers.buildsettings;
11 import dub.platform : BuildPlatform, archCheck, compilerCheck, platformCheck;
12 import dub.internal.vibecompat.inet.path;
13 import dub.internal.logging;
14 
15 import std.algorithm : canFind, endsWith, filter;
16 
17 /**
18 	Alters the build options to comply with the specified build requirements.
19 
20 	And enabled options that do not comply will get disabled.
21 */
22 void enforceBuildRequirements(ref BuildSettings settings)
23 {
24 	settings.addOptions(BuildOption.warningsAsErrors);
25 	if (settings.requirements & BuildRequirement.allowWarnings) { settings.options &= ~BuildOption.warningsAsErrors; settings.options |= BuildOption.warnings; }
26 	if (settings.requirements & BuildRequirement.silenceWarnings) settings.options &= ~(BuildOption.warningsAsErrors|BuildOption.warnings);
27 	if (settings.requirements & BuildRequirement.disallowDeprecations) { settings.options &= ~(BuildOption.ignoreDeprecations|BuildOption.deprecationWarnings); settings.options |= BuildOption.deprecationErrors; }
28 	if (settings.requirements & BuildRequirement.silenceDeprecations) { settings.options &= ~(BuildOption.deprecationErrors|BuildOption.deprecationWarnings); settings.options |= BuildOption.ignoreDeprecations; }
29 	if (settings.requirements & BuildRequirement.disallowInlining) settings.options &= ~BuildOption.inline;
30 	if (settings.requirements & BuildRequirement.disallowOptimization) settings.options &= ~BuildOption.optimize;
31 	if (settings.requirements & BuildRequirement.requireBoundsCheck) settings.options &= ~BuildOption.noBoundsCheck;
32 	if (settings.requirements & BuildRequirement.requireContracts) settings.options &= ~BuildOption.releaseMode;
33 	if (settings.requirements & BuildRequirement.relaxProperties) settings.options &= ~BuildOption.property;
34 }
35 
36 
37 /**
38 	Determines if a specific file name has the extension of a linker file.
39 
40 	Linker files include static/dynamic libraries, resource files, object files
41 	and DLL definition files.
42 */
43 bool isLinkerFile(const scope ref BuildPlatform platform, string f)
44 {
45 	import std.path;
46 	switch (extension(f)) {
47 		default:
48 			return false;
49 		case ".lib", ".obj", ".res", ".def":
50 			return platform.isWindows();
51 		case ".a", ".o", ".so", ".dylib":
52 			return !platform.isWindows();
53 	}
54 }
55 
56 unittest {
57 	BuildPlatform p;
58 
59 	p.platform = ["windows"];
60 	assert(isLinkerFile(p, "test.obj"));
61 	assert(isLinkerFile(p, "test.lib"));
62 	assert(isLinkerFile(p, "test.res"));
63 	assert(!isLinkerFile(p, "test.o"));
64 	assert(!isLinkerFile(p, "test.d"));
65 
66 	p.platform = ["something else"];
67 	assert(isLinkerFile(p, "test.o"));
68 	assert(isLinkerFile(p, "test.a"));
69 	assert(isLinkerFile(p, "test.so"));
70 	assert(isLinkerFile(p, "test.dylib"));
71 	assert(!isLinkerFile(p, "test.obj"));
72 	assert(!isLinkerFile(p, "test.d"));
73 }
74 
75 
76 /**
77 	Adds a default DT_SONAME (ELF) / 'install name' (Mach-O) when linking a dynamic library.
78 	This makes dependees reference their dynamic-lib deps by filename only (DT_NEEDED etc.)
79 	instead of by the path used in the dependee linker cmdline, and enables loading the
80 	deps from the dependee's output directory - either by setting the LD_LIBRARY_PATH
81 	environment variable, or baking an rpath into the executable.
82 */
83 package void addDynamicLibName(ref BuildSettings settings, in BuildPlatform platform, string fileName)
84 {
85 	if (!platform.isWindows()) {
86 		// *pre*pend to allow the user to override it
87 		if (platform.platform.canFind("darwin"))
88 			settings.prependLFlags("-install_name", "@rpath/" ~ fileName);
89 		else
90 			settings.prependLFlags("-soname", fileName);
91 	}
92 }
93 
94 
95 /**
96 	Replaces each referenced import library by the appropriate linker flags.
97 
98 	This function tries to invoke "pkg-config" if possible and falls back to
99 	direct flag translation if that fails.
100 */
101 void resolveLibs(ref BuildSettings settings, const scope ref BuildPlatform platform)
102 {
103 	import std.string : format;
104 	import std.array : array;
105 
106 	if (settings.libs.length == 0) return;
107 
108 	if (settings.targetType == TargetType.library || settings.targetType == TargetType.staticLibrary) {
109 		logDiagnostic("Ignoring all import libraries for static library build.");
110 		settings.libs = null;
111 		if (platform.isWindows())
112 			settings.sourceFiles = settings.sourceFiles.filter!(f => !f.endsWith(".lib")).array;
113 	}
114 
115 	version (Posix) {
116 		import std.algorithm : any, map, partition, startsWith;
117 		import std.array : array, join, split;
118 		import std.exception : enforce;
119 		import std.process : execute;
120 
121 		try {
122 			enum pkgconfig_bin = "pkg-config";
123 
124 			bool exists(string lib) {
125 				return execute([pkgconfig_bin, "--exists", lib]).status == 0;
126 			}
127 
128 			auto pkgconfig_libs = settings.libs.partition!(l => !exists(l));
129 			pkgconfig_libs ~= settings.libs[0 .. $ - pkgconfig_libs.length]
130 				.partition!(l => !exists("lib"~l)).map!(l => "lib"~l).array;
131 			settings.libs = settings.libs[0 .. $ - pkgconfig_libs.length];
132 
133 			if (pkgconfig_libs.length) {
134 				logDiagnostic("Using pkg-config to resolve library flags for %s.", pkgconfig_libs.join(", "));
135 				auto libflags = execute([pkgconfig_bin, "--libs"] ~ pkgconfig_libs);
136 				enforce(libflags.status == 0, format("pkg-config exited with error code %s: %s", libflags.status, libflags.output));
137 				foreach (f; libflags.output.split()) {
138 					if (f.startsWith("-L-L")) {
139 						settings.addLFlags(f[2 .. $]);
140 					} else if (f.startsWith("-defaultlib")) {
141 						settings.addDFlags(f);
142 					} else if (f.startsWith("-L-defaultlib")) {
143 						settings.addDFlags(f[2 .. $]);
144 					} else if (f.startsWith("-pthread")) {
145 						settings.addLFlags("-lpthread");
146 					} else if (f.startsWith("-L-l")) {
147 						settings.addLFlags(f[2 .. $].split(","));
148 					} else if (f.startsWith("-Wl,")) settings.addLFlags(f[4 .. $].split(","));
149 					else settings.addLFlags(f);
150 				}
151 			}
152 			if (settings.libs.length) logDiagnostic("Using direct -l... flags for %s.", settings.libs.array.join(", "));
153 		} catch (Exception e) {
154 			logDiagnostic("pkg-config failed: %s", e.msg);
155 			logDiagnostic("Falling back to direct -l... flags.");
156 		}
157 	}
158 }
159 
160 
161 /** Searches the given list of compiler flags for ones that have a generic
162 	equivalent.
163 
164 	Certain compiler flags should, instead of using compiler-specific syntax,
165 	be specified as build options (`BuildOption`) or built requirements
166 	(`BuildRequirements`). This function will output warning messages to
167 	assist the user in making the best choice.
168 */
169 void warnOnSpecialCompilerFlags(string[] compiler_flags, Flags!BuildOption options, string package_name, string config_name)
170 {
171 	import std.algorithm : any, endsWith, startsWith;
172 	import std.range : empty;
173 
174 	struct SpecialFlag {
175 		string[] flags;
176 		string alternative;
177 	}
178 	static immutable SpecialFlag[] s_specialFlags = [
179 		{["-c", "-o-"], "Automatically issued by DUB, do not specify in dub.json"},
180 		{["-w", "-Wall", "-Werr"], `Use "buildRequirements" to control warning behavior`},
181 		{["-property", "-fproperty"], "Using this flag may break building of dependencies and it will probably be removed from DMD in the future"},
182 		{["-wi"], `Use the "buildRequirements" field to control warning behavior`},
183 		{["-d", "-de", "-dw"], `Use the "buildRequirements" field to control deprecation behavior`},
184 		{["-of"], `Use "targetPath" and "targetName" to customize the output file`},
185 		{["-debug", "-fdebug", "-g"], "Call dub with --build=debug"},
186 		{["-release", "-frelease", "-O", "-inline"], "Call dub with --build=release"},
187 		{["-unittest", "-funittest"], "Call dub with --build=unittest"},
188 		{["-lib"], `Use {"targetType": "staticLibrary"} or let dub manage this`},
189 		{["-D"], "Call dub with --build=docs or --build=ddox"},
190 		{["-X"], "Call dub with --build=ddox"},
191 		{["-cov"], "Call dub with --build=cov or --build=unittest-cov"},
192 		{["-cov=ctfe"], "Call dub with --build=cov-ctfe or --build=unittest-cov-ctfe"},
193 		{["-profile"], "Call dub with --build=profile"},
194 		{["-version="], `Use "versions" to specify version constants in a compiler independent way`},
195 		{["-debug="], `Use "debugVersions" to specify version constants in a compiler independent way`},
196 		{["-I"], `Use "importPaths" to specify import paths in a compiler independent way`},
197 		{["-J"], `Use "stringImportPaths" to specify import paths in a compiler independent way`},
198 		{["-m32", "-m64", "-m32mscoff"], `Use --arch=x86/--arch=x86_64/--arch=x86_mscoff to specify the target architecture, e.g. 'dub build --arch=x86_64'`}
199 	];
200 
201 	struct SpecialOption {
202 		BuildOption[] flags;
203 		string alternative;
204 	}
205 	static immutable SpecialOption[] s_specialOptions = [
206 		{[BuildOption.debugMode], "Call DUB with --build=debug"},
207 		{[BuildOption.releaseMode], "Call DUB with --build=release"},
208 		{[BuildOption.coverage], "Call DUB with --build=cov or --build=unittest-cov"},
209 		{[BuildOption.coverageCTFE], "Call DUB with --build=cov-ctfe or --build=unittest-cov-ctfe"},
210 		{[BuildOption.debugInfo], "Call DUB with --build=debug"},
211 		{[BuildOption.inline], "Call DUB with --build=release"},
212 		{[BuildOption.noBoundsCheck], "Call DUB with --build=release-nobounds"},
213 		{[BuildOption.optimize], "Call DUB with --build=release"},
214 		{[BuildOption.profile], "Call DUB with --build=profile"},
215 		{[BuildOption.unittests], "Call DUB with --build=unittest"},
216 		{[BuildOption.syntaxOnly], "Call DUB with --build=syntax"},
217 		{[BuildOption.warnings, BuildOption.warningsAsErrors], "Use \"buildRequirements\" to control the warning level"},
218 		{[BuildOption.ignoreDeprecations, BuildOption.deprecationWarnings, BuildOption.deprecationErrors], "Use \"buildRequirements\" to control the deprecation warning level"},
219 		{[BuildOption.property], "This flag is deprecated and has no effect"}
220 	];
221 
222 	bool got_preamble = false;
223 	void outputPreamble()
224 	{
225 		if (got_preamble) return;
226 		got_preamble = true;
227 		logWarn("");
228 		if (config_name.empty) logWarn("## Warning for package %s ##", package_name);
229 		else logWarn("## Warning for package %s, configuration %s ##", package_name, config_name);
230 		logWarn("");
231 		logWarn("The following compiler flags have been specified in the package description");
232 		logWarn("file. They are handled by DUB and direct use in packages is discouraged.");
233 		logWarn("Alternatively, you can set the DFLAGS environment variable to pass custom flags");
234 		logWarn("to the compiler, or use one of the suggestions below:");
235 		logWarn("");
236 	}
237 
238 	foreach (f; compiler_flags) {
239 		foreach (sf; s_specialFlags) {
240 			if (sf.flags.any!(sff => f == sff || (sff.endsWith("=") && f.startsWith(sff)))) {
241 				outputPreamble();
242 				logWarn("%s: %s", f, sf.alternative);
243 				break;
244 			}
245 		}
246 	}
247 
248 	foreach (sf; s_specialOptions) {
249 		foreach (f; sf.flags) {
250 			if (options & f) {
251 				outputPreamble();
252 				logWarn("%s: %s", f, sf.alternative);
253 				break;
254 			}
255 		}
256 	}
257 
258 	if (got_preamble) logWarn("");
259 }
260 
261 /**
262 	Turn a DMD-like version (e.g. 2.082.1) into a SemVer-like version (e.g. 2.82.1).
263     The function accepts a dependency operator prefix and some text postfix.
264     Prefix and postfix are returned verbatim.
265 	Params:
266 		ver	=	version string, possibly with a dependency operator prefix and some
267 				test postfix.
268 	Returns:
269 		A Semver compliant string
270 */
271 package(dub) string dmdLikeVersionToSemverLike(string ver)
272 {
273 	import std.algorithm : countUntil, joiner, map, skipOver, splitter;
274 	import std.array : join, split;
275 	import std.ascii : isDigit;
276 	import std.conv : text;
277 	import std.exception : enforce;
278 	import std.functional : not;
279 	import std.range : padRight;
280 
281 	const start = ver.countUntil!isDigit;
282 	enforce(start != -1, "Invalid semver: "~ver);
283 	const prefix = ver[0 .. start];
284 	ver = ver[start .. $];
285 
286 	const end = ver.countUntil!(c => !c.isDigit && c != '.');
287 	const postfix = end == -1 ? null : ver[end .. $];
288 	auto verStr = ver[0 .. $-postfix.length];
289 
290 	auto comps = verStr
291 		.splitter(".")
292 		.map!((a) { if (a.length > 1) a.skipOver("0"); return a;})
293 		.padRight("0", 3);
294 
295 	return text(prefix, comps.joiner("."), postfix);
296 }
297 
298 ///
299 unittest {
300 	assert(dmdLikeVersionToSemverLike("2.082.1") == "2.82.1");
301 	assert(dmdLikeVersionToSemverLike("2.082.0") == "2.82.0");
302 	assert(dmdLikeVersionToSemverLike("2.082") == "2.82.0");
303 	assert(dmdLikeVersionToSemverLike("~>2.082") == "~>2.82.0");
304 	assert(dmdLikeVersionToSemverLike("~>2.082-beta1") == "~>2.82.0-beta1");
305 	assert(dmdLikeVersionToSemverLike("2.4.6") == "2.4.6");
306 	assert(dmdLikeVersionToSemverLike("2.4.6-alpha12") == "2.4.6-alpha12");
307 }
308 
309 private enum probeBeginMark = "__dub_probe_begin__";
310 private enum probeEndMark = "__dub_probe_end__";
311 
312 /**
313 	Generate a file that will give, at compile time, information about the compiler (architecture, frontend version...)
314 
315 	See_Also: `readPlatformProbe`
316 */
317 NativePath generatePlatformProbeFile()
318 {
319 	import dub.internal.vibecompat.core.file;
320 	import dub.internal.vibecompat.data.json;
321 	import dub.internal.utils;
322 	import std.string : format;
323 
324 	// try to not use phobos in the probe to avoid long import times
325 	enum probe = q{
326 		module dub_platform_probe;
327 
328 		template toString(int v) { enum toString = v.stringof; }
329 		string stringArray(string[] ary) {
330 			string res;
331 			foreach (i, e; ary) {
332 				if (i)
333 					res ~= ", ";
334 				res ~= '"' ~ e ~ '"';
335 			}
336 			return res;
337 		}
338 
339 		pragma(msg, `%1$s`
340 			~ '\n' ~ `{`
341 			~ '\n' ~ `  "compiler": "`~ determineCompiler() ~ `",`
342 			~ '\n' ~ `  "frontendVersion": ` ~ toString!__VERSION__ ~ `,`
343 			~ '\n' ~ `  "compilerVendor": "` ~ __VENDOR__ ~ `",`
344 			~ '\n' ~ `  "platform": [`
345 			~ '\n' ~ `    ` ~ determinePlatform().stringArray
346 			~ '\n' ~ `  ],`
347 			~ '\n' ~ `  "architecture": [`
348 			~ '\n' ~ `    ` ~ determineArchitecture().stringArray
349 			~ '\n' ~ `   ],`
350 			~ '\n' ~ `}`
351 			~ '\n' ~ `%2$s`);
352 
353 		string[] determinePlatform() { %3$s }
354 		string[] determineArchitecture() { %4$s }
355 		string determineCompiler() { %5$s }
356 
357 		}.format(probeBeginMark, probeEndMark, platformCheck, archCheck, compilerCheck);
358 
359 	auto path = getTempFile("dub_platform_probe", ".d");
360 	writeFile(path, probe);
361 
362 	return path;
363 }
364 
365 /**
366 	Processes the JSON output generated by compiling the platform probe file.
367 
368 	See_Also: `generatePlatformProbeFile`.
369 */
370 BuildPlatform readPlatformJsonProbe(string output)
371 {
372 	import std.algorithm : map;
373 	import std.array : array;
374 	import std.exception : enforce;
375 	import std.string;
376 
377 	// work around possible additional output of the compiler
378 	auto idx1 = output.indexOf(probeBeginMark);
379 	auto idx2 = output.lastIndexOf(probeEndMark);
380 	enforce(idx1 >= 0 && idx1 < idx2,
381 		"Unexpected platform information output - does not contain a JSON object.");
382 	output = output[idx1+probeBeginMark.length .. idx2];
383 
384 	import dub.internal.vibecompat.data.json;
385 	auto json = parseJsonString(output);
386 
387 	BuildPlatform build_platform;
388 	build_platform.platform = json["platform"].get!(Json[]).map!(e => e.get!string()).array();
389 	build_platform.architecture = json["architecture"].get!(Json[]).map!(e => e.get!string()).array();
390 	build_platform.compiler = json["compiler"].get!string;
391 	build_platform.frontendVersion = json["frontendVersion"].get!int;
392 	return build_platform;
393 }