1 /**
2 	Generator for project files
3 
4 	Copyright: © 2012-2013 Matthias Dondorff, © 2013-2016 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.generators.generator;
9 
10 import dub.compilers.compiler;
11 import dub.generators.cmake;
12 import dub.generators.build;
13 import dub.generators.sublimetext;
14 import dub.generators.visuald;
15 import dub.internal.vibecompat.core.file;
16 import dub.internal.vibecompat.core.log;
17 import dub.internal.vibecompat.inet.path;
18 import dub.package_;
19 import dub.packagemanager;
20 import dub.project;
21 
22 import std.algorithm : map, filter, canFind, balancedParens;
23 import std.array : array;
24 import std.array;
25 import std.exception;
26 import std.file;
27 import std.string;
28 
29 
30 /**
31 	Common interface for project generators/builders.
32 */
33 class ProjectGenerator
34 {
35 	/** Information about a single binary target.
36 
37 		A binary target can either be an executable or a static/dynamic library.
38 		It consists of one or more packages.
39 	*/
40 	struct TargetInfo {
41 		/// The root package of this target
42 		Package pack;
43 
44 		/// All packages compiled into this target
45 		Package[] packages;
46 
47 		/// The configuration used for building the root package
48 		string config;
49 
50 		/** Build settings used to build the target.
51 
52 			The build settings include all sources of all contained packages.
53 
54 			Depending on the specific generator implementation, it may be
55 			necessary to add any static or dynamic libraries generated for
56 			child targets ($(D linkDependencies)).
57 		*/
58 		BuildSettings buildSettings;
59 
60 		/** List of all dependencies.
61 
62 			This list includes dependencies that are not the root of a binary
63 			target.
64 		*/
65 		string[] dependencies;
66 
67 		/** List of all binary dependencies.
68 
69 			This list includes all dependencies that are the root of a binary
70 			target.
71 		*/
72 		string[] linkDependencies;
73 	}
74 
75 	protected {
76 		Project m_project;
77 	}
78 
79 	this(Project project)
80 	{
81 		m_project = project;
82 	}
83 
84 	/** Performs the full generator process.
85 	*/
86 	final void generate(GeneratorSettings settings)
87 	{
88 		import dub.compilers.utils : enforceBuildRequirements;
89 
90 		if (!settings.config.length) settings.config = m_project.getDefaultConfiguration(settings.platform);
91 
92 		TargetInfo[string] targets;
93 		string[string] configs = m_project.getPackageConfigs(settings.platform, settings.config);
94 
95 		foreach (pack; m_project.getTopologicalPackageList(true, null, configs)) {
96 			BuildSettings buildsettings;
97 			buildsettings.processVars(m_project, pack, pack.getBuildSettings(settings.platform, configs[pack.name]), true);
98 			prepareGeneration(pack, m_project, settings, buildsettings);
99 		}
100 
101 		string[] mainfiles;
102 		collect(settings, m_project.rootPackage, targets, configs, mainfiles, null);
103 		downwardsInheritSettings(m_project.rootPackage.name, targets, targets[m_project.rootPackage.name].buildSettings);
104 		addBuildTypeSettings(targets, settings);
105 		foreach (ref t; targets.byValue) enforceBuildRequirements(t.buildSettings);
106 		auto bs = &targets[m_project.rootPackage.name].buildSettings;
107 		if (bs.targetType == TargetType.executable) bs.addSourceFiles(mainfiles);
108 
109 		generateTargets(settings, targets);
110 
111 		foreach (pack; m_project.getTopologicalPackageList(true, null, configs)) {
112 			BuildSettings buildsettings;
113 			buildsettings.processVars(m_project, pack, pack.getBuildSettings(settings.platform, configs[pack.name]), true);
114 			bool generate_binary = !(buildsettings.options & BuildOption.syntaxOnly);
115 			finalizeGeneration(pack, m_project, settings, buildsettings, Path(bs.targetPath), generate_binary);
116 		}
117 
118 		performPostGenerateActions(settings, targets);
119 	}
120 
121 	/** Overridden in derived classes to implement the actual generator functionality.
122 
123 		The function should go through all targets recursively. The first target
124 		(which is guaranteed to be there) is
125 		$(D targets[m_project.rootPackage.name]). The recursive descent is then
126 		done using the $(D TargetInfo.linkDependencies) list.
127 
128 		This method is also potentially responsible for running the pre and post
129 		build commands, while pre and post generate commands are already taken
130 		care of by the $(D generate) method.
131 
132 		Params:
133 			settings = The generator settings used for this run
134 			targets = A map from package name to TargetInfo that contains all
135 				binary targets to be built.
136 	*/
137 	protected abstract void generateTargets(GeneratorSettings settings, in TargetInfo[string] targets);
138 
139 	/** Overridable method to be invoked after the generator process has finished.
140 
141 		An examples of functionality placed here is to run the application that
142 		has just been built.
143 	*/
144 	protected void performPostGenerateActions(GeneratorSettings settings, in TargetInfo[string] targets) {}
145 
146 	private BuildSettings collect(GeneratorSettings settings, Package pack, ref TargetInfo[string] targets, in string[string] configs, ref string[] main_files, string bin_pack)
147 	{
148 		import std.algorithm : sort;
149 		import dub.compilers.utils : isLinkerFile;
150 
151 		if (auto pt = pack.name in targets) return pt.buildSettings;
152 
153 		// determine the actual target type
154 		auto shallowbs = pack.getBuildSettings(settings.platform, configs[pack.name]);
155 		TargetType tt = shallowbs.targetType;
156 		if (pack is m_project.rootPackage) {
157 			if (tt == TargetType.autodetect || tt == TargetType.library) tt = TargetType.staticLibrary;
158 		} else {
159 			if (tt == TargetType.autodetect || tt == TargetType.library) tt = settings.combined ? TargetType.sourceLibrary : TargetType.staticLibrary;
160 			else if (tt == TargetType.dynamicLibrary) {
161 				logWarn("Dynamic libraries are not yet supported as dependencies - building as static library.");
162 				tt = TargetType.staticLibrary;
163 			}
164 		}
165 		if (tt != TargetType.none && tt != TargetType.sourceLibrary && shallowbs.sourceFiles.empty) {
166 			logWarn(`Configuration '%s' of package %s contains no source files. Please add {"targetType": "none"} to it's package description to avoid building it.`,
167 				configs[pack.name], pack.name);
168 			tt = TargetType.none;
169 		}
170 
171 		shallowbs.targetType = tt;
172 		bool generates_binary = tt != TargetType.sourceLibrary && tt != TargetType.none;
173 		bool is_target = generates_binary || pack is m_project.rootPackage;
174 
175 		if (tt == TargetType.none) {
176 			// ignore any build settings for targetType none (only dependencies will be processed)
177 			shallowbs = BuildSettings.init;
178 			shallowbs.targetType = TargetType.none;
179 		}
180 
181 		// start to build up the build settings
182 		BuildSettings buildsettings;
183 		processVars(buildsettings, m_project, pack, shallowbs, true);
184 
185 		// remove any mainSourceFile from library builds
186 		if (buildsettings.targetType != TargetType.executable && buildsettings.mainSourceFile.length) {
187 			buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(f => f != buildsettings.mainSourceFile)().array;
188 			main_files ~= buildsettings.mainSourceFile;
189 		}
190 
191 		logDiagnostic("Generate target %s (%s %s %s)", pack.name, buildsettings.targetType, buildsettings.targetPath, buildsettings.targetName);
192 		if (is_target)
193 			targets[pack.name] = TargetInfo(pack, [pack], configs[pack.name], buildsettings, null);
194 
195 		auto deps = pack.getDependencies(configs[pack.name]);
196 		foreach (depname; deps.keys.sort()) {
197 			auto depspec = deps[depname];
198 			auto dep = m_project.getDependency(depname, depspec.optional);
199 			if (!dep) continue;
200 
201 			auto depbs = collect(settings, dep, targets, configs, main_files, is_target ? pack.name : bin_pack);
202 
203 			if (depbs.targetType != TargetType.sourceLibrary && depbs.targetType != TargetType.none) {
204 				// add a reference to the target binary and remove all source files in the dependency build settings
205 				depbs.sourceFiles = depbs.sourceFiles.filter!(f => f.isLinkerFile()).array;
206 				depbs.importFiles = null;
207 			}
208 
209 			buildsettings.add(depbs);
210 
211 			if (depbs.targetType == TargetType.executable)
212 				continue;
213 
214 			auto pt = (is_target ? pack.name : bin_pack) in targets;
215 			assert(pt !is null);
216 			if (auto pdt = depname in targets) {
217 				pt.dependencies ~= depname;
218 				pt.linkDependencies ~= depname;
219 				if (depbs.targetType == TargetType.staticLibrary)
220 					pt.linkDependencies = pt.linkDependencies.filter!(d => !pdt.linkDependencies.canFind(d)).array ~ pdt.linkDependencies;
221 			} else pt.packages ~= dep;
222 		}
223 
224 		if (is_target) targets[pack.name].buildSettings = buildsettings.dup;
225 
226 		return buildsettings;
227 	}
228 
229 	private string[] downwardsInheritSettings(string target, TargetInfo[string] targets, in BuildSettings root_settings)
230 	{
231 		import dub.internal.utils : stripDlangSpecialChars;
232 
233 		auto ti = &targets[target];
234 		ti.buildSettings.addVersions(root_settings.versions);
235 		ti.buildSettings.addDebugVersions(root_settings.debugVersions);
236 		ti.buildSettings.addOptions(BuildOptions(cast(BuildOptions)root_settings.options & inheritedBuildOptions));
237 
238 		// special support for overriding string imports in parent packages
239 		// this is a candidate for deprecation, once an alternative approach
240 		// has been found
241 		if (ti.buildSettings.stringImportPaths.length) {
242 			// override string import files (used for up to date checking)
243 			foreach (ref f; ti.buildSettings.stringImportFiles)
244 				foreach (fi; root_settings.stringImportFiles)
245 					if (f != fi && Path(f).head == Path(fi).head) {
246 						f = fi;
247 					}
248 
249 			// add the string import paths (used by the compiler to find the overridden files)
250 			ti.buildSettings.prependStringImportPaths(root_settings.stringImportPaths);
251 		}
252 
253 		string[] packs = ti.packages.map!(p => p.name).array;
254 		foreach (d; ti.dependencies)
255 			packs ~= downwardsInheritSettings(d, targets, root_settings);
256 
257 		logDebug("%s: %s", target, packs);
258 
259 		// Add Have_* versions *after* downwards inheritance, so that dependencies
260 		// are build independently of the parent packages w.r.t the other parent
261 		// dependencies. This enables sharing of the same package build for
262 		// multiple dependees.
263 		ti.buildSettings.addVersions(packs.map!(pn => "Have_" ~ stripDlangSpecialChars(pn)).array);
264 
265 		return packs;
266 	}
267 
268 	private void addBuildTypeSettings(TargetInfo[string] targets, GeneratorSettings settings)
269 	{
270 		foreach (ref t; targets) {
271 			t.buildSettings.add(settings.buildSettings);
272 
273 			// add build type settings and convert plain DFLAGS to build options
274 			m_project.addBuildTypeSettings(t.buildSettings, settings.platform, settings.buildType, t.pack is m_project.rootPackage);
275 			settings.compiler.extractBuildOptions(t.buildSettings);
276 
277 			auto tt = t.buildSettings.targetType;
278 			bool generates_binary = tt != TargetType.sourceLibrary && tt != TargetType.none;
279 			enforce (generates_binary || t.pack !is m_project.rootPackage || (t.buildSettings.options & BuildOption.syntaxOnly),
280 				format("Main package must have a binary target type, not %s. Cannot build.", tt));
281 		}
282 	}
283 }
284 
285 
286 struct GeneratorSettings {
287 	BuildPlatform platform;
288 	Compiler compiler;
289 	string config;
290 	string buildType;
291 	BuildSettings buildSettings;
292 	BuildMode buildMode = BuildMode.separate;
293 
294 	bool combined; // compile all in one go instead of each dependency separately
295 
296 	// only used for generator "build"
297 	bool run, force, direct, clean, rdmd, tempBuild, parallelBuild;
298 	string[] runArgs;
299 	void delegate(int status, string output) compileCallback;
300 	void delegate(int status, string output) linkCallback;
301 	void delegate(int status, string output) runCallback;
302 }
303 
304 
305 /**
306 	Determines the mode in which the compiler and linker are invoked.
307 */
308 enum BuildMode {
309 	separate,                 /// Compile and link separately
310 	allAtOnce,                /// Perform compile and link with a single compiler invocation
311 	singleFile,               /// Compile each file separately
312 	//multipleObjects,          /// Generate an object file per module
313 	//multipleObjectsPerModule, /// Use the -multiobj switch to generate multiple object files per module
314 	//compileOnly               /// Do not invoke the linker (can be done using a post build command)
315 }
316 
317 
318 /**
319 	Creates a project generator of the given type for the specified project.
320 */
321 ProjectGenerator createProjectGenerator(string generator_type, Project project)
322 {
323 	assert(project !is null, "Project instance needed to create a generator.");
324 
325 	generator_type = generator_type.toLower();
326 	switch(generator_type) {
327 		default:
328 			throw new Exception("Unknown project generator: "~generator_type);
329 		case "build":
330 			logDebug("Creating build generator.");
331 			return new BuildGenerator(project);
332 		case "mono-d":
333 			throw new Exception("The Mono-D generator has been removed. Use Mono-D's built in DUB support instead.");
334 		case "visuald":
335 			logDebug("Creating VisualD generator.");
336 			return new VisualDGenerator(project);
337 		case "sublimetext":
338 			logDebug("Creating SublimeText generator.");
339 			return new SublimeTextGenerator(project);
340 		case "cmake":
341 			logDebug("Creating CMake generator.");
342 			return new CMakeGenerator(project);
343 	}
344 }
345 
346 
347 /**
348 	Runs pre-build commands and performs other required setup before project files are generated.
349 */
350 private void prepareGeneration(in Package pack, in Project proj, in GeneratorSettings settings,
351 	in BuildSettings buildsettings)
352 {
353 	if (buildsettings.preGenerateCommands.length && !isRecursiveInvocation(pack.name)) {
354 		logInfo("Running pre-generate commands for %s...", pack.name);
355 		runBuildCommands(buildsettings.preGenerateCommands, pack, proj, settings, buildsettings);
356 	}
357 }
358 
359 /**
360 	Runs post-build commands and copies required files to the binary directory.
361 */
362 private void finalizeGeneration(in Package pack, in Project proj, in GeneratorSettings settings,
363 	in BuildSettings buildsettings, Path target_path, bool generate_binary)
364 {
365 	import std.path : globMatch;
366 	
367 	if (buildsettings.postGenerateCommands.length && !isRecursiveInvocation(pack.name)) {
368 		logInfo("Running post-generate commands for %s...", pack.name);
369 		runBuildCommands(buildsettings.postGenerateCommands, pack, proj, settings, buildsettings);
370 	}
371 
372 	if (generate_binary) {
373 		if (!exists(buildsettings.targetPath))
374 			mkdirRecurse(buildsettings.targetPath);
375 
376 		if (buildsettings.copyFiles.length) {
377 			void copyFolderRec(Path folder, Path dstfolder)
378 			{
379 				mkdirRecurse(dstfolder.toNativeString());
380 				foreach (de; iterateDirectory(folder.toNativeString())) {
381 					if (de.isDirectory) {
382 						copyFolderRec(folder ~ de.name, dstfolder ~ de.name);
383 					} else {
384 						try hardLinkFile(folder ~ de.name, dstfolder ~ de.name, true);
385 						catch (Exception e) {
386 							logWarn("Failed to copy file %s: %s", (folder ~ de.name).toNativeString(), e.msg);
387 						}
388 					}
389 				}
390 			}
391 
392 			void tryCopyDir(string file)
393 			{
394 				auto src = Path(file);
395 				if (!src.absolute) src = pack.path ~ src;
396 				auto dst = target_path ~ Path(file).head;
397 				if (src == dst) {
398 					logDiagnostic("Skipping copy of %s (same source and destination)", file);
399 					return;
400 				}
401 				logDiagnostic("  %s to %s", src.toNativeString(), dst.toNativeString());
402 				try {
403 					copyFolderRec(src, dst);
404 				} catch(Exception e) logWarn("Failed to copy %s to %s: %s", src.toNativeString(), dst.toNativeString(), e.msg);
405 			}
406 
407 			void tryCopyFile(string file)
408 			{
409 				auto src = Path(file);
410 				if (!src.absolute) src = pack.path ~ src;
411 				auto dst = target_path ~ Path(file).head;
412 				if (src == dst) {
413 					logDiagnostic("Skipping copy of %s (same source and destination)", file);
414 					return;
415 				}
416 				logDiagnostic("  %s to %s", src.toNativeString(), dst.toNativeString());
417 				try {
418 					hardLinkFile(src, dst, true);
419 				} catch(Exception e) logWarn("Failed to copy %s to %s: %s", src.toNativeString(), dst.toNativeString(), e.msg);
420 			}
421 			logInfo("Copying files for %s...", pack.name);
422 			string[] globs;
423 			foreach (f; buildsettings.copyFiles)
424 			{
425 				if (f.canFind("*", "?") ||
426 					(f.canFind("{") && f.balancedParens('{', '}')) ||
427 					(f.canFind("[") && f.balancedParens('[', ']')))
428 				{
429 					globs ~= f;
430 				}
431 				else
432 				{
433 					if (f.isDir)
434 						tryCopyDir(f);
435 					else
436 						tryCopyFile(f);
437 				}
438 			}
439 			if (globs.length) // Search all files for glob matches
440 			{
441 				foreach (f; dirEntries(pack.path.toNativeString(), SpanMode.breadth))
442 				{
443 					foreach (glob; globs)
444 					{
445 						if (f.name().globMatch(glob))
446 						{
447 							if (f.isDir)
448 								tryCopyDir(f);
449 							else
450 								tryCopyFile(f);
451 							break;
452 						}
453 					}
454 				}
455 			}
456 		}
457 
458 	}
459 }
460 
461 
462 /** Runs a list of build commands for a particular package.
463 
464 	This funtion sets all DUB speficic environment variables and makes sure
465 	that recursive dub invocations are detected and don't result in infinite
466 	command execution loops. The latter could otherwise happen when a command
467 	runs "dub describe" or similar functionality.
468 */
469 void runBuildCommands(in string[] commands, in Package pack, in Project proj,
470 	in GeneratorSettings settings, in BuildSettings build_settings)
471 {
472 	import std.conv;
473 	import std.process;
474 	import dub.internal.utils;
475 
476 	string[string] env = environment.toAA();
477 	// TODO: do more elaborate things here
478 	// TODO: escape/quote individual items appropriately
479 	env["DFLAGS"]                = join(cast(string[])build_settings.dflags, " ");
480 	env["LFLAGS"]                = join(cast(string[])build_settings.lflags," ");
481 	env["VERSIONS"]              = join(cast(string[])build_settings.versions," ");
482 	env["LIBS"]                  = join(cast(string[])build_settings.libs," ");
483 	env["IMPORT_PATHS"]          = join(cast(string[])build_settings.importPaths," ");
484 	env["STRING_IMPORT_PATHS"]   = join(cast(string[])build_settings.stringImportPaths," ");
485 
486 	env["DC"]                    = settings.platform.compilerBinary;
487 	env["DC_BASE"]               = settings.platform.compiler;
488 	env["D_FRONTEND_VER"]        = to!string(settings.platform.frontendVersion);
489 
490 	env["DUB_PLATFORM"]          = join(cast(string[])settings.platform.platform," ");
491 	env["DUB_ARCH"]              = join(cast(string[])settings.platform.architecture," ");
492 
493 	env["DUB_TARGET_TYPE"]       = to!string(build_settings.targetType);
494 	env["DUB_TARGET_PATH"]       = build_settings.targetPath;
495 	env["DUB_TARGET_NAME"]       = build_settings.targetName;
496 	env["DUB_WORKING_DIRECTORY"] = build_settings.workingDirectory;
497 	env["DUB_MAIN_SOURCE_FILE"]  = build_settings.mainSourceFile;
498 
499 	env["DUB_CONFIG"]            = settings.config;
500 	env["DUB_BUILD_TYPE"]        = settings.buildType;
501 	env["DUB_BUILD_MODE"]        = to!string(settings.buildMode);
502 	env["DUB_PACKAGE"]           = pack.name;
503 	env["DUB_PACKAGE_DIR"]       = pack.path.toNativeString();
504 	env["DUB_ROOT_PACKAGE"]      = proj.rootPackage.name;
505 	env["DUB_ROOT_PACKAGE_DIR"]  = proj.rootPackage.path.toNativeString();
506 
507 	env["DUB_COMBINED"]          = settings.combined?      "TRUE" : "";
508 	env["DUB_RUN"]               = settings.run?           "TRUE" : "";
509 	env["DUB_FORCE"]             = settings.force?         "TRUE" : "";
510 	env["DUB_DIRECT"]            = settings.direct?        "TRUE" : "";
511 	env["DUB_CLEAN"]             = settings.clean?         "TRUE" : "";
512 	env["DUB_RDMD"]              = settings.rdmd?          "TRUE" : "";
513 	env["DUB_TEMP_BUILD"]        = settings.tempBuild?     "TRUE" : "";
514 	env["DUB_PARALLEL_BUILD"]    = settings.parallelBuild? "TRUE" : "";
515 
516 	env["DUB_RUN_ARGS"] = (cast(string[])settings.runArgs).map!(escapeShellFileName).join(" ");
517 	
518 	auto depNames = proj.dependencies.map!((a) => a.name).array();
519 	storeRecursiveInvokations(env, proj.rootPackage.name ~ depNames);
520 	runCommands(commands, env);
521 }
522 
523 private bool isRecursiveInvocation(string pack)
524 {
525 	import std.algorithm : canFind, splitter;
526 	import std.process : environment;
527 
528 	return environment
529         .get("DUB_PACKAGES_USED", "")
530         .splitter(",")
531         .canFind(pack);
532 }
533 
534 private void storeRecursiveInvokations(string[string] env, string[] packs)
535 {
536 	import std.algorithm : canFind, splitter;
537 	import std.range : chain;
538 	import std.process : environment;
539 
540     env["DUB_PACKAGES_USED"] = environment
541         .get("DUB_PACKAGES_USED", "")
542         .splitter(",")
543         .chain(packs)
544         .join(",");
545 }