1 /**
2 	Generator for project files
3 
4 	Copyright: © 2012-2013 Matthias Dondorff
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 		if (!settings.config.length) settings.config = m_project.getDefaultConfiguration(settings.platform);
89 
90 		TargetInfo[string] targets;
91 		string[string] configs = m_project.getPackageConfigs(settings.platform, settings.config);
92 
93 		foreach (pack; m_project.getTopologicalPackageList(true, null, configs)) {
94 			BuildSettings buildsettings;
95 			buildsettings.processVars(m_project, pack, pack.getBuildSettings(settings.platform, configs[pack.name]), true);
96 			prepareGeneration(pack.name, buildsettings);
97 		}
98 
99 		string[] mainfiles;
100 		collect(settings, m_project.rootPackage, targets, configs, mainfiles, null);
101 		downwardsInheritSettings(m_project.rootPackage.name, targets, targets[m_project.rootPackage.name].buildSettings);
102 		auto bs = &targets[m_project.rootPackage.name].buildSettings;
103 		if (bs.targetType == TargetType.executable) bs.addSourceFiles(mainfiles);
104 
105 		generateTargets(settings, targets);
106 
107 		foreach (pack; m_project.getTopologicalPackageList(true, null, configs)) {
108 			BuildSettings buildsettings;
109 			buildsettings.processVars(m_project, pack, pack.getBuildSettings(settings.platform, configs[pack.name]), true);
110 			bool generate_binary = !(buildsettings.options & BuildOptions.syntaxOnly);
111 			finalizeGeneration(pack.name, buildsettings, pack.path, Path(bs.targetPath), generate_binary);
112 		}
113 
114 		performPostGenerateActions(settings, targets);
115 	}
116 
117 	/** Overridden in derived classes to implement the actual generator functionality.
118 
119 		The function should go through all targets recursively. The first target
120 		(which is guaranteed to be there) is
121 		$(D targets[m_project.rootPackage.name]). The recursive descent is then
122 		done using the $(D TargetInfo.linkDependencies) list.
123 
124 		This method is also potentially responsible for running the pre and post
125 		build commands, while pre and post generate commands are already taken
126 		care of by the $(D generate) method.
127 
128 		Params:
129 			settings = The generator settings used for this run
130 			targets = A map from package name to TargetInfo that contains all
131 				binary targets to be built.
132 	*/
133 	protected abstract void generateTargets(GeneratorSettings settings, in TargetInfo[string] targets);
134 
135 	/** Overridable method to be invoked after the generator process has finished.
136 
137 		An examples of functionality placed here is to run the application that
138 		has just been built.
139 	*/
140 	protected void performPostGenerateActions(GeneratorSettings settings, in TargetInfo[string] targets) {}
141 
142 	private BuildSettings collect(GeneratorSettings settings, Package pack, ref TargetInfo[string] targets, in string[string] configs, ref string[] main_files, string bin_pack)
143 	{
144 		if (auto pt = pack.name in targets) return pt.buildSettings;
145 
146 		// determine the actual target type
147 		auto shallowbs = pack.getBuildSettings(settings.platform, configs[pack.name]);
148 		TargetType tt = shallowbs.targetType;
149 		if (pack is m_project.rootPackage) {
150 			if (tt == TargetType.autodetect || tt == TargetType.library) tt = TargetType.staticLibrary;
151 		} else {
152 			if (tt == TargetType.autodetect || tt == TargetType.library) tt = settings.combined ? TargetType.sourceLibrary : TargetType.staticLibrary;
153 			else if (tt == TargetType.dynamicLibrary) {
154 				logWarn("Dynamic libraries are not yet supported as dependencies - building as static library.");
155 				tt = TargetType.staticLibrary;
156 			}
157 		}
158 		if (tt != TargetType.none && tt != TargetType.sourceLibrary && shallowbs.sourceFiles.empty) {
159 			logWarn(`Configuration '%s' of package %s contains no source files. Please add {"targetType": "none"} to it's package description to avoid building it.`,
160 				configs[pack.name], pack.name);
161 			tt = TargetType.none;
162 		}
163 
164 		shallowbs.targetType = tt;
165 		bool generates_binary = tt != TargetType.sourceLibrary && tt != TargetType.none;
166 
167 		enforce (generates_binary || pack !is m_project.rootPackage,
168 			format("Main package must have a binary target type, not %s. Cannot build.", tt));
169 
170 		if (tt == TargetType.none) {
171 			// ignore any build settings for targetType none (only dependencies will be processed)
172 			shallowbs = BuildSettings.init;
173 		}
174 
175 		// start to build up the build settings
176 		BuildSettings buildsettings;
177 		if (generates_binary) buildsettings = settings.buildSettings.dup;
178 		processVars(buildsettings, m_project, pack, shallowbs, true);
179 
180 		// remove any mainSourceFile from library builds
181 		if (buildsettings.targetType != TargetType.executable && buildsettings.mainSourceFile.length) {
182 			buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(f => f != buildsettings.mainSourceFile)().array;
183 			main_files ~= buildsettings.mainSourceFile;
184 		}
185 
186 		logDiagnostic("Generate target %s (%s %s %s)", pack.name, buildsettings.targetType, buildsettings.targetPath, buildsettings.targetName);
187 		if (generates_binary)
188 			targets[pack.name] = TargetInfo(pack, [pack], configs[pack.name], buildsettings, null);
189 
190 		foreach (depname, depspec; pack.dependencies) {
191 			if (!pack.hasDependency(depname, configs[pack.name])) continue;
192 			auto dep = m_project.getDependency(depname, depspec.optional);
193 			if (!dep) continue;
194 
195 			auto depbs = collect(settings, dep, targets, configs, main_files, generates_binary ? pack.name : bin_pack);
196 
197 			if (depbs.targetType != TargetType.sourceLibrary && depbs.targetType != TargetType.none) {
198 				// add a reference to the target binary and remove all source files in the dependency build settings
199 				depbs.sourceFiles = depbs.sourceFiles.filter!(f => f.isLinkerFile()).array;
200 				depbs.importFiles = null;
201 			}
202 
203 			buildsettings.add(depbs);
204 
205 			if (depbs.targetType == TargetType.executable)
206 				continue;
207 
208 			auto pt = (generates_binary ? pack.name : bin_pack) in targets;
209 			assert(pt !is null);
210 			if (auto pdt = depname in targets) {
211 				pt.dependencies ~= depname;
212 				pt.linkDependencies ~= depname;
213 				if (depbs.targetType == TargetType.staticLibrary)
214 					pt.linkDependencies = pt.linkDependencies.filter!(d => !pdt.linkDependencies.canFind(d)).array ~ pdt.linkDependencies;
215 			} else pt.packages ~= dep;
216 		}
217 
218 		if (generates_binary) {
219 			// add build type settings and convert plain DFLAGS to build options
220 			m_project.addBuildTypeSettings(buildsettings, settings.platform, settings.buildType);
221 			settings.compiler.extractBuildOptions(buildsettings);
222 			enforceBuildRequirements(buildsettings);
223 			targets[pack.name].buildSettings = buildsettings.dup;
224 		}
225 
226 		return buildsettings;
227 	}
228 
229 	private string[] downwardsInheritSettings(string target, TargetInfo[string] targets, in BuildSettings root_settings)
230 	{
231 		auto ti = &targets[target];
232 		ti.buildSettings.addVersions(root_settings.versions);
233 		ti.buildSettings.addDebugVersions(root_settings.debugVersions);
234 		ti.buildSettings.addOptions(root_settings.options);
235 
236 		// special support for overriding string imports in parent packages
237 		// this is a candidate for deprecation, once an alternative approach
238 		// has been found
239 		if (ti.buildSettings.stringImportPaths.length) {
240 			// override string import files (used for up to date checking)
241 			foreach (ref f; ti.buildSettings.stringImportFiles)
242 				foreach (fi; root_settings.stringImportFiles)
243 					if (f != fi && Path(f).head == Path(fi).head) {
244 						f = fi;
245 					}
246 
247 			// add the string import paths (used by the compiler to find the overridden files)
248 			ti.buildSettings.prependStringImportPaths(root_settings.stringImportPaths);
249 		}
250 
251 		string[] packs = ti.packages.map!(p => p.name).array;
252 		foreach (d; ti.dependencies)
253 			packs ~= downwardsInheritSettings(d, targets, root_settings);
254 
255 		logDebug("%s: %s", target, packs);
256 
257 		// Add Have_* versions *after* downwards inheritance, so that dependencies
258 		// are build independently of the parent packages w.r.t the other parent
259 		// dependencies. This enables sharing of the same package build for
260 		// multiple dependees.
261 		ti.buildSettings.addVersions(packs.map!(pn => "Have_" ~ stripDlangSpecialChars(pn)).array);
262 
263 		return packs;
264 	}
265 }
266 
267 
268 struct GeneratorSettings {
269 	BuildPlatform platform;
270 	Compiler compiler;
271 	string config;
272 	string buildType;
273 	BuildSettings buildSettings;
274 	BuildMode buildMode = BuildMode.separate;
275 
276 	bool combined; // compile all in one go instead of each dependency separately
277 
278 	// only used for generator "build"
279 	bool run, force, direct, clean, rdmd, tempBuild, parallelBuild;
280 	string[] runArgs;
281 	void delegate(int status, string output) compileCallback;
282 	void delegate(int status, string output) linkCallback;
283 	void delegate(int status, string output) runCallback;
284 }
285 
286 
287 /**
288 	Determines the mode in which the compiler and linker are invoked.
289 */
290 enum BuildMode {
291 	separate,                 /// Compile and link separately
292 	allAtOnce,                /// Perform compile and link with a single compiler invocation
293 	singleFile,               /// Compile each file separately
294 	//multipleObjects,          /// Generate an object file per module
295 	//multipleObjectsPerModule, /// Use the -multiobj switch to generate multiple object files per module
296 	//compileOnly               /// Do not invoke the linker (can be done using a post build command)
297 }
298 
299 
300 /**
301 	Creates a project generator of the given type for the specified project.
302 */
303 ProjectGenerator createProjectGenerator(string generator_type, Project project)
304 {
305 	assert(project !is null, "Project instance needed to create a generator.");
306 
307 	generator_type = generator_type.toLower();
308 	switch(generator_type) {
309 		default:
310 			throw new Exception("Unknown project generator: "~generator_type);
311 		case "build":
312 			logDebug("Creating build generator.");
313 			return new BuildGenerator(project);
314 		case "mono-d":
315 			throw new Exception("The Mono-D generator has been removed. Use Mono-D's built in DUB support instead.");
316 		case "visuald":
317 			logDebug("Creating VisualD generator.");
318 			return new VisualDGenerator(project);
319 		case "sublimetext":
320 			logDebug("Creating SublimeText generator.");
321 			return new SublimeTextGenerator(project);
322 		case "cmake":
323 			logDebug("Creating CMake generator.");
324 			return new CMakeGenerator(project);
325 	}
326 }
327 
328 
329 /**
330 	Runs pre-build commands and performs other required setup before project files are generated.
331 */
332 private void prepareGeneration(string pack, in BuildSettings buildsettings)
333 {
334 	if( buildsettings.preGenerateCommands.length ){
335 		logInfo("Running pre-generate commands for %s...", pack);
336 		runBuildCommands(buildsettings.preGenerateCommands, buildsettings);
337 	}
338 }
339 
340 /**
341 	Runs post-build commands and copies required files to the binary directory.
342 */
343 private void finalizeGeneration(string pack, in BuildSettings buildsettings, Path pack_path, Path target_path, bool generate_binary)
344 {
345 	if (buildsettings.postGenerateCommands.length) {
346 		logInfo("Running post-generate commands for %s...", pack);
347 		runBuildCommands(buildsettings.postGenerateCommands, buildsettings);
348 	}
349 
350 	if (generate_binary) {
351 		if (!exists(buildsettings.targetPath))
352 			mkdirRecurse(buildsettings.targetPath);
353 
354 		if (buildsettings.copyFiles.length) {
355 			void copyFolderRec(Path folder, Path dstfolder)
356 			{
357 				mkdirRecurse(dstfolder.toNativeString());
358 				foreach (de; iterateDirectory(folder.toNativeString())) {
359 					if (de.isDirectory) {
360 						copyFolderRec(folder ~ de.name, dstfolder ~ de.name);
361 					} else {
362 						try hardLinkFile(folder ~ de.name, dstfolder ~ de.name, true);
363 						catch (Exception e) {
364 							logWarn("Failed to copy file %s: %s", (folder ~ de.name).toNativeString(), e.msg);
365 						}
366 					}
367 				}
368 			}
369 
370 			void tryCopyDir(string file)
371 			{
372 				auto src = Path(file);
373 				if (!src.absolute) src = pack_path ~ src;
374 				auto dst = target_path ~ Path(file).head;
375 				if (src == dst) {
376 					logDiagnostic("Skipping copy of %s (same source and destination)", file);
377 					return;
378 				}
379 				logDiagnostic("  %s to %s", src.toNativeString(), dst.toNativeString());
380 				try {
381 					copyFolderRec(src, dst);
382 				} catch(Exception e) logWarn("Failed to copy %s to %s: %s", src.toNativeString(), dst.toNativeString(), e.msg);
383 			}
384 
385 			void tryCopyFile(string file)
386 			{
387 				auto src = Path(file);
388 				if (!src.absolute) src = pack_path ~ src;
389 				auto dst = target_path ~ Path(file).head;
390 				if (src == dst) {
391 					logDiagnostic("Skipping copy of %s (same source and destination)", file);
392 					return;
393 				}
394 				logDiagnostic("  %s to %s", src.toNativeString(), dst.toNativeString());
395 				try {
396 					hardLinkFile(src, dst, true);
397 				} catch(Exception e) logWarn("Failed to copy %s to %s: %s", src.toNativeString(), dst.toNativeString(), e.msg);
398 			}
399 			logInfo("Copying files for %s...", pack);
400 			string[] globs;
401 			foreach (f; buildsettings.copyFiles)
402 			{
403 				if (f.canFind("*", "?") ||
404 					(f.canFind("{") && f.balancedParens('{', '}')) ||
405 					(f.canFind("[") && f.balancedParens('[', ']')))
406 				{
407 					globs ~= f;
408 				}
409 				else
410 				{
411 					if (f.isDir)
412 						tryCopyDir(f);
413 					else
414 						tryCopyFile(f);
415 				}
416 			}
417 			if (globs.length) // Search all files for glob matches
418 			{
419 				foreach (f; dirEntries(pack_path.toNativeString(), SpanMode.breadth))
420 				{
421 					foreach (glob; globs)
422 					{
423 						if (f.globMatch(glob))
424 						{
425 							if (f.isDir)
426 								tryCopyDir(f);
427 							else
428 								tryCopyFile(f);
429 							break;
430 						}
431 					}
432 				}
433 			}
434 		}
435 
436 	}
437 }
438 
439 void runBuildCommands(in string[] commands, in BuildSettings build_settings)
440 {
441 	import std.process;
442 	import dub.internal.utils;
443 
444 	string[string] env = environment.toAA();
445 	// TODO: do more elaborate things here
446 	// TODO: escape/quote individual items appropriately
447 	env["DFLAGS"] = join(cast(string[])build_settings.dflags, " ");
448 	env["LFLAGS"] = join(cast(string[])build_settings.lflags," ");
449 	env["VERSIONS"] = join(cast(string[])build_settings.versions," ");
450 	env["LIBS"] = join(cast(string[])build_settings.libs," ");
451 	env["IMPORT_PATHS"] = join(cast(string[])build_settings.importPaths," ");
452 	env["STRING_IMPORT_PATHS"] = join(cast(string[])build_settings.stringImportPaths," ");
453 	runCommands(commands, env);
454 }