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, compilerCheckPragmas, platformCheck, pragmaGen;
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 				// Also extract C preprocessor flags from pkg-config --cflags
153 				auto cflags = execute([pkgconfig_bin, "--cflags"] ~ pkgconfig_libs);
154 				if (cflags.status == 0) {
155 					logDiagnostic("Using pkg-config to resolve C preprocessor flags for %s.", pkgconfig_libs.join(", "));
156 					applyPkgConfigCFlags(settings, cflags.output);
157 				}
158 			}
159 			if (settings.libs.length) logDiagnostic("Using direct -l... flags for %s.", settings.libs.array.join(", "));
160 		} catch (Exception e) {
161 			logWarn("'pkg-config' does not seem to be installed or functional as it failed with: %s", e.msg);
162 			logWarn("'dub' will automatically fall back to directly using '-l' flags, " ~
163 				"but this is error prone. Please install 'pkg-config', or use " ~
164 				"'lflags' directly.");
165 		}
166 	}
167 }
168 
169 
170 /**
171 	Parses pkg-config --cflags output and applies C preprocessor flags to build settings.
172 
173 	All C preprocessor flags (-I, -D, -U, -include, -isystem, -idirafter) are passed
174 	to the D compiler via -P prefix (for DMD/LDC) so they get forwarded to the C preprocessor.
175 	This is called from resolveLibs() which runs after cImportPaths are already processed,
176 	so we add directly to dflags with the -P prefix.
177 */
178 package void applyPkgConfigCFlags(ref BuildSettings settings, string cflagsOutput)
179 {
180 	import std.algorithm : splitter, startsWith;
181 
182 	// Note: We split by whitespace for consistency with --libs parsing above.
183 	// This won't handle shell-quoted paths with spaces, but neither does --libs.
184 	foreach (f; cflagsOutput.splitter()) {
185 		if (f.startsWith("-I") || f.startsWith("-D") || f.startsWith("-U") ||
186 		    f.startsWith("-include") || f.startsWith("-isystem") ||
187 		    f.startsWith("-idirafter")) {
188 			// Pass all C preprocessor flags via -P (for DMD/LDC)
189 			settings.addDFlags("-P" ~ f);
190 		}
191 	}
192 }
193 
194 unittest {
195 	BuildSettings settings;
196 
197 	// Test -I flag extraction (passed via -P prefix)
198 	applyPkgConfigCFlags(settings, "-I/usr/include/foo -I/usr/include/bar");
199 	assert(settings.dflags == ["-P-I/usr/include/foo", "-P-I/usr/include/bar"]);
200 
201 	// Test preprocessor define flags
202 	settings = BuildSettings.init;
203 	applyPkgConfigCFlags(settings, "-DFOO=1 -DBAR -UBAZ");
204 	assert(settings.dflags == ["-P-DFOO=1", "-P-DBAR", "-P-UBAZ"]);
205 
206 	// Test mixed flags
207 	settings = BuildSettings.init;
208 	applyPkgConfigCFlags(settings, "-I/usr/include -DVERSION=2 -isystem/usr/local/include");
209 	assert(settings.dflags == ["-P-I/usr/include", "-P-DVERSION=2", "-P-isystem/usr/local/include"]);
210 
211 	// Test -idirafter flag (attached path)
212 	settings = BuildSettings.init;
213 	applyPkgConfigCFlags(settings, "-idirafter/fallback/include");
214 	assert(settings.dflags == ["-P-idirafter/fallback/include"]);
215 
216 	// Note: -include with space-separated argument (e.g., "-include config.h") is not
217 	// fully supported - the flag is passed but the argument may be lost if space-separated.
218 	// Most pkg-config files use attached arguments (e.g., -I/path, -DFOO).
219 
220 	// Test empty input
221 	settings = BuildSettings.init;
222 	applyPkgConfigCFlags(settings, "");
223 	assert(settings.dflags.length == 0);
224 
225 	// Test unknown flags are ignored (e.g., -pthread which is for linker)
226 	settings = BuildSettings.init;
227 	applyPkgConfigCFlags(settings, "-I/inc -pthread -Wall");
228 	assert(settings.dflags == ["-P-I/inc"]);
229 }
230 
231 
232 /** Searches the given list of compiler flags for ones that have a generic
233 	equivalent.
234 
235 	Certain compiler flags should, instead of using compiler-specific syntax,
236 	be specified as build options (`BuildOption`) or built requirements
237 	(`BuildRequirements`). This function will output warning messages to
238 	assist the user in making the best choice.
239 */
240 void warnOnSpecialCompilerFlags(string[] compiler_flags, Flags!BuildOption options, string package_name, string config_name)
241 {
242 	import std.algorithm : any, endsWith, startsWith;
243 	import std.range : empty;
244 
245 	struct SpecialFlag {
246 		string[] flags;
247 		string alternative;
248 	}
249 	static immutable SpecialFlag[] s_specialFlags = [
250 		{["-c", "-o-"], "Automatically issued by DUB, do not specify in dub.json"},
251 		{["-w", "-Wall", "-Werr"], `Use "buildRequirements" to control warning behavior`},
252 		{["-property", "-fproperty"], "Using this flag may break building of dependencies and it will probably be removed from DMD in the future"},
253 		{["-wi"], `Use the "buildRequirements" field to control warning behavior`},
254 		{["-d", "-de", "-dw"], `Use the "buildRequirements" field to control deprecation behavior`},
255 		{["-of"], `Use "targetPath" and "targetName" to customize the output file`},
256 		{["-debug", "-fdebug", "-g"], "Call dub with --build=debug"},
257 		{["-release", "-frelease", "-O", "-inline"], "Call dub with --build=release"},
258 		{["-unittest", "-funittest"], "Call dub with --build=unittest"},
259 		{["-lib"], `Use {"targetType": "staticLibrary"} or let dub manage this`},
260 		{["-D"], "Call dub with --build=docs or --build=ddox"},
261 		{["-X"], "Call dub with --build=ddox"},
262 		{["-cov"], "Call dub with --build=cov or --build=unittest-cov"},
263 		{["-cov=ctfe"], "Call dub with --build=cov-ctfe or --build=unittest-cov-ctfe"},
264 		{["-profile"], "Call dub with --build=profile"},
265 		{["-version="], `Use "versions" to specify version constants in a compiler independent way`},
266 		{["-debug="], `Use "debugVersions" to specify version constants in a compiler independent way`},
267 		{["-I"], `Use "importPaths" to specify import paths in a compiler independent way`},
268 		{["-J"], `Use "stringImportPaths" to specify import paths in a compiler independent way`},
269 		{["-m32", "-m64", "-m32mscoff"], `Use --arch=x86/--arch=x86_64/--arch=x86_mscoff to specify the target architecture, e.g. 'dub build --arch=x86_64'`}
270 	];
271 
272 	struct SpecialOption {
273 		BuildOption[] flags;
274 		string alternative;
275 	}
276 	static immutable SpecialOption[] s_specialOptions = [
277 		{[BuildOption.debugMode], "Call DUB with --build=debug"},
278 		{[BuildOption.releaseMode], "Call DUB with --build=release"},
279 		{[BuildOption.coverage], "Call DUB with --build=cov or --build=unittest-cov"},
280 		{[BuildOption.coverageCTFE], "Call DUB with --build=cov-ctfe or --build=unittest-cov-ctfe"},
281 		{[BuildOption.debugInfo], "Call DUB with --build=debug"},
282 		{[BuildOption.inline], "Call DUB with --build=release"},
283 		{[BuildOption.noBoundsCheck], "Call DUB with --build=release-nobounds"},
284 		{[BuildOption.optimize], "Call DUB with --build=release"},
285 		{[BuildOption.profile], "Call DUB with --build=profile"},
286 		{[BuildOption.unittests], "Call DUB with --build=unittest"},
287 		{[BuildOption.syntaxOnly], "Call DUB with --build=syntax"},
288 		{[BuildOption.warnings, BuildOption.warningsAsErrors], "Use \"buildRequirements\" to control the warning level"},
289 		{[BuildOption.ignoreDeprecations, BuildOption.deprecationWarnings, BuildOption.deprecationErrors], "Use \"buildRequirements\" to control the deprecation warning level"},
290 		{[BuildOption.property], "This flag is deprecated and has no effect"}
291 	];
292 
293 	bool got_preamble = false;
294 	void outputPreamble()
295 	{
296 		if (got_preamble) return;
297 		got_preamble = true;
298 		logWarn("");
299 		if (config_name.empty) logWarn("## Warning for package %s ##", package_name);
300 		else logWarn("## Warning for package %s, configuration %s ##", package_name, config_name);
301 		logWarn("");
302 		logWarn("The following compiler flags have been specified in the package description");
303 		logWarn("file. They are handled by DUB and direct use in packages is discouraged.");
304 		logWarn("Alternatively, you can set the DFLAGS environment variable to pass custom flags");
305 		logWarn("to the compiler, or use one of the suggestions below:");
306 		logWarn("");
307 	}
308 
309 	foreach (f; compiler_flags) {
310 		foreach (sf; s_specialFlags) {
311 			if (sf.flags.any!(sff => f == sff || (sff.endsWith("=") && f.startsWith(sff)))) {
312 				outputPreamble();
313 				logWarn("%s: %s", f, sf.alternative);
314 				break;
315 			}
316 		}
317 	}
318 
319 	foreach (sf; s_specialOptions) {
320 		foreach (f; sf.flags) {
321 			if (options & f) {
322 				outputPreamble();
323 				logWarn("%s: %s", f, sf.alternative);
324 				break;
325 			}
326 		}
327 	}
328 
329 	if (got_preamble) logWarn("");
330 }
331 
332 private enum probeBeginMark = "__dub_probe_begin__";
333 private enum probeEndMark = "__dub_probe_end__";
334 
335 /**
336 	Generate a file that will give, at compile time, information about the compiler (architecture, frontend version...)
337 
338 	See_Also: `readPlatformProbe`
339 */
340 NativePath generatePlatformProbeFile()
341 {
342 	import dub.internal.vibecompat.core.file;
343 	import dub.internal.utils;
344 	import std.string : format;
345 
346 	enum moduleInfo = q{
347 		module object;
348 		alias string = const(char)[];
349 	};
350 
351 	// avoid druntime so that this compiles without a compiler's builtin object.d
352 	enum probe = q{
353 		%1$s
354 		pragma(msg, `%2$s`);
355 		pragma(msg, `\n`);
356 		pragma(msg, `compiler`);
357 		%6$s
358 		pragma(msg, `\n`);
359 		pragma(msg, `frontendVersion "`);
360 		pragma(msg, __VERSION__.stringof);
361 		pragma(msg, `"\n`);
362 		pragma(msg, `compilerVendor "`);
363 		pragma(msg, __VENDOR__);
364 		pragma(msg, `"\n`);
365 		pragma(msg, `platform`);
366 		%4$s
367 		pragma(msg, `\n`);
368 		pragma(msg, `architecture `);
369 		%5$s
370 		pragma(msg, `\n`);
371 		pragma(msg, `%3$s`);
372 	}.format(moduleInfo, probeBeginMark, probeEndMark, pragmaGen(platformCheck), pragmaGen(archCheck), compilerCheckPragmas);
373 
374 	auto path = getTempFile("dub_platform_probe", ".d");
375 	writeFile(path, probe);
376 	return path;
377 }
378 
379 
380 /**
381 	Processes the SDL output generated by compiling the platform probe file.
382 
383 	See_Also: `generatePlatformProbeFile`.
384 */
385 BuildPlatform readPlatformSDLProbe(string output)
386 {
387 	import std.algorithm : map, max, splitter, joiner, count, filter;
388 	import std.array : array;
389 	import std.exception : enforce;
390 	import std.range : front;
391 	import std.ascii : newline;
392 	import std.string;
393 	import dub.internal.sdlang.parser;
394 	import dub.internal.sdlang.ast;
395 	import std.conv;
396 
397 	// work around possible additional output of the compiler
398 	auto idx1 = output.indexOf(probeBeginMark ~ newline ~ "\\n");
399 	auto idx2 = output[max(0, idx1) .. $].indexOf(probeEndMark) + idx1;
400 	enforce(idx1 >= 0 && idx1 < idx2,
401 		"Unexpected platform information output - does not contain a JSON object.");
402 	output = output[idx1 + probeBeginMark.length .. idx2].replace(newline, "").replace("\\n", "\n");
403 
404 	output = output.splitter("\n").filter!((e) => e.length > 0)
405 		.map!((e) {
406 			if (e.count("\"") == 0)
407 			{
408 				return e ~ ` ""`;
409 			}
410 			return e;
411 		})
412 		.joiner("\n").array().to!string;
413 
414 	BuildPlatform build_platform;
415 	Tag sdl = parseSource(output);
416 
417 	foreach (n; sdl.all.tags)
418 	{
419 		switch (n.name)
420 		{
421 		default:
422 			break;
423 		case "platform":
424 			build_platform.platform = n.values.map!(e => e.toString()).array();
425 			break;
426 		case "architecture":
427 			build_platform.architecture = n.values.map!(e => e.toString()).array();
428 			break;
429 		case "compiler":
430 			build_platform.compiler = n.values.front.toString();
431 			break;
432 		case "frontendVersion":
433 			build_platform.frontendVersion = n.values.front.toString()
434 				.filter!((e) => e >= '0' && e <= '9').array().to!string
435 				.to!int;
436 			break;
437 		}
438 	}
439 	return build_platform;
440 }