1 /**
2 	Generator for direct compiler builds.
3 	
4 	Copyright: © 2013-2013 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.generators.build;
9 
10 import dub.compilers.compiler;
11 import dub.generators.generator;
12 import dub.internal.utils;
13 import dub.internal.vibecompat.core.file;
14 import dub.internal.vibecompat.core.log;
15 import dub.internal.vibecompat.inet.path;
16 import dub.package_;
17 import dub.packagemanager;
18 import dub.project;
19 
20 import std.algorithm;
21 import std.array;
22 import std.conv;
23 import std.exception;
24 import std.file;
25 import std.process;
26 import std..string;
27 
28 
29 class BuildGenerator : ProjectGenerator {
30 	private {
31 		Project m_project;
32 		PackageManager m_packageMan;
33 		Path[] m_temporaryFiles;
34 	}
35 
36 	this(Project app, PackageManager mgr)
37 	{
38 		super(app);
39 		m_project = app;
40 		m_packageMan = mgr;
41 	}
42 
43 	override void generateTargets(GeneratorSettings settings, in TargetInfo[string] targets)
44 	{
45 		scope (exit) cleanupTemporaries();
46 
47 		bool[string] visited;
48 		void buildTargetRec(string target)
49 		{
50 			if (target in visited) return;
51 			visited[target] = true;
52 
53 			auto ti = targets[target];
54 
55 			foreach (dep; ti.dependencies)
56 				buildTargetRec(dep);
57 
58 			auto bs = ti.buildSettings.dup;
59 			if (bs.targetType != TargetType.staticLibrary)
60 				foreach (ldep; ti.linkDependencies) {
61 					auto dbs = targets[ldep].buildSettings;
62 					bs.addSourceFiles((Path(dbs.targetPath) ~ getTargetFileName(dbs, settings.platform)).toNativeString());
63 				}
64 			buildTarget(settings, bs, ti.pack, ti.config);
65 		}
66 
67 		// build all targets
68 		buildTargetRec(m_project.mainPackage.name);
69 
70 		// run the generated executable
71 		auto buildsettings = targets[m_project.mainPackage.name].buildSettings;
72 		if (settings.run && !(buildsettings.options & BuildOptions.syntaxOnly)) {
73 			auto exe_file_path = Path(buildsettings.targetPath) ~ getTargetFileName(buildsettings, settings.platform);
74 			runTarget(exe_file_path, buildsettings, settings.runArgs);
75 		}
76 	}
77 
78 	private void buildTarget(GeneratorSettings settings, BuildSettings buildsettings, in Package pack, string config)
79 	{
80 		auto cwd = Path(getcwd());
81 		bool generate_binary = !(buildsettings.options & BuildOptions.syntaxOnly);
82 
83 		auto build_id = computeBuildID(config, buildsettings, settings);
84 
85 		// make all paths relative to shrink the command line
86 		string makeRelative(string path) { auto p = Path(path); if (p.absolute) p = p.relativeTo(cwd); return p.toNativeString(); }
87 		foreach (ref f; buildsettings.sourceFiles) f = makeRelative(f);
88 		foreach (ref p; buildsettings.importPaths) p = makeRelative(p);
89 		foreach (ref p; buildsettings.stringImportPaths) p = makeRelative(p);
90 
91 		// perform the actual build
92 		if (settings.rdmd) performRDMDBuild(settings, buildsettings, pack, config);
93 		else if (settings.direct || !generate_binary) performDirectBuild(settings, buildsettings, pack, config);
94 		else performCachedBuild(settings, buildsettings, pack, config, build_id);
95 
96 		// run post-build commands
97 		if (buildsettings.postBuildCommands.length) {
98 			logInfo("Running post-build commands...");
99 			runBuildCommands(buildsettings.postBuildCommands, buildsettings);
100 		}
101 	}
102 
103 	void performCachedBuild(GeneratorSettings settings, BuildSettings buildsettings, in Package pack, string config, string build_id)
104 	{
105 		auto cwd = Path(getcwd());
106 		auto target_path = pack.path ~ format(".dub/build/%s/", build_id);
107 
108 		if (!settings.force && isUpToDate(target_path, buildsettings, settings.platform)) {
109 			logInfo("Target is up to date. Using existing build in %s. Use --force to force a rebuild.", target_path.toNativeString());
110 			copyTargetFile(target_path, buildsettings, settings.platform);
111 			return;
112 		}
113 
114 		if (!isWritableDir(target_path, true)) {
115 			logInfo("Build directory %s is not writable. Falling back to direct build in the system's temp folder.", target_path.relativeTo(cwd).toNativeString());
116 			performDirectBuild(settings, buildsettings, pack, config);
117 			return;
118 		}
119 
120 		// determine basic build properties
121 		auto generate_binary = !(buildsettings.options & BuildOptions.syntaxOnly);
122 
123 		// run pre-/post-generate commands and copy "copyFiles"
124 		prepareGeneration(buildsettings);
125 		finalizeGeneration(buildsettings, generate_binary);
126 
127 		logInfo("Building %s configuration \"%s\", build type %s.", pack.name, config, settings.buildType);
128 
129 		if( buildsettings.preBuildCommands.length ){
130 			logInfo("Running pre-build commands...");
131 			runBuildCommands(buildsettings.preBuildCommands, buildsettings);
132 		}
133 
134 		// override target path
135 		auto cbuildsettings = buildsettings;
136 		cbuildsettings.targetPath = target_path.relativeTo(cwd).toNativeString();
137 		buildWithCompiler(settings, cbuildsettings);
138 
139 		copyTargetFile(target_path, buildsettings, settings.platform);
140 	}
141 
142 	void performRDMDBuild(GeneratorSettings settings, ref BuildSettings buildsettings, in Package pack, string config)
143 	{
144 		auto cwd = Path(getcwd());
145 		//Added check for existance of [AppNameInPackagejson].d
146 		//If exists, use that as the starting file.
147 		auto mainsrc = buildsettings.mainSourceFile.length ? pack.path ~ buildsettings.mainSourceFile : getMainSourceFile(pack);
148 
149 		// do not pass all source files to RDMD, only the main source file
150 		buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(s => !s.endsWith(".d"))().array();
151 		settings.compiler.prepareBuildSettings(buildsettings, BuildSetting.commandLine);
152 
153 		auto generate_binary = !buildsettings.dflags.canFind("-o-");
154 
155 		// Create start script, which will be used by the calling bash/cmd script.
156 		// build "rdmd --force %DFLAGS% -I%~dp0..\source -Jviews -Isource @deps.txt %LIBS% source\app.d" ~ application arguments
157 		// or with "/" instead of "\"
158 		Path exe_file_path;
159 		bool tmp_target = false;
160 		if (generate_binary) {
161 			if (settings.run && !isWritableDir(Path(buildsettings.targetPath), true)) {
162 				import std.random;
163 				auto rnd = to!string(uniform(uint.min, uint.max)) ~ "-";
164 				auto tmpdir = getTempDir()~".rdmd/source/";
165 				buildsettings.targetPath = tmpdir.toNativeString();
166 				buildsettings.targetName = rnd ~ buildsettings.targetName;
167 				m_temporaryFiles ~= tmpdir;
168 				tmp_target = true;
169 			}
170 			exe_file_path = Path(buildsettings.targetPath) ~ getTargetFileName(buildsettings, settings.platform);
171 			settings.compiler.setTarget(buildsettings, settings.platform);
172 		}
173 
174 		logDiagnostic("Application output name is '%s'", getTargetFileName(buildsettings, settings.platform));
175 
176 		string[] flags = ["--build-only", "--compiler="~settings.platform.compilerBinary];
177 		if (settings.force) flags ~= "--force";
178 		flags ~= buildsettings.dflags;
179 		flags ~= mainsrc.relativeTo(cwd).toNativeString();
180 
181 		prepareGeneration(buildsettings);
182 		finalizeGeneration(buildsettings, generate_binary);
183 
184 		if (buildsettings.preBuildCommands.length){
185 			logInfo("Running pre-build commands...");
186 			runCommands(buildsettings.preBuildCommands);
187 		}
188 
189 		logInfo("Building configuration "~config~", build type "~settings.buildType);
190 
191 		logInfo("Running rdmd...");
192 		logDiagnostic("rdmd %s", join(flags, " "));
193 		auto rdmd_pid = spawnProcess("rdmd" ~ flags);
194 		auto result = rdmd_pid.wait();
195 		enforce(result == 0, "Build command failed with exit code "~to!string(result));
196 
197 		if (tmp_target) {
198 			m_temporaryFiles ~= exe_file_path;
199 			foreach (f; buildsettings.copyFiles)
200 				m_temporaryFiles ~= Path(buildsettings.targetPath).parentPath ~ Path(f).head;
201 		}
202 	}
203 
204 	void performDirectBuild(GeneratorSettings settings, ref BuildSettings buildsettings, in Package pack, string config)
205 	{
206 		auto cwd = Path(getcwd());
207 
208 		auto generate_binary = !(buildsettings.options & BuildOptions.syntaxOnly);
209 		auto is_static_library = buildsettings.targetType == TargetType.staticLibrary || buildsettings.targetType == TargetType.library;
210 
211 		// make file paths relative to shrink the command line
212 		foreach (ref f; buildsettings.sourceFiles) {
213 			auto fp = Path(f);
214 			if( fp.absolute ) fp = fp.relativeTo(cwd);
215 			f = fp.toNativeString();
216 		}
217 
218 		logInfo("Building configuration \""~config~"\", build type "~settings.buildType);
219 
220 		prepareGeneration(buildsettings);
221 
222 		// make all target/import paths relative
223 		string makeRelative(string path) { auto p = Path(path); if (p.absolute) p = p.relativeTo(cwd); return p.toNativeString(); }
224 		buildsettings.targetPath = makeRelative(buildsettings.targetPath);
225 		foreach (ref p; buildsettings.importPaths) p = makeRelative(p);
226 		foreach (ref p; buildsettings.stringImportPaths) p = makeRelative(p);
227 
228 		Path exe_file_path;
229 		bool is_temp_target = false;
230 		if (generate_binary) {
231 			if (settings.run && !isWritableDir(Path(buildsettings.targetPath), true)) {
232 				import std.random;
233 				auto rnd = to!string(uniform(uint.min, uint.max));
234 				auto tmppath = getTempDir()~("dub/"~rnd~"/");
235 				buildsettings.targetPath = tmppath.toNativeString();
236 				m_temporaryFiles ~= tmppath;
237 				is_temp_target = true;
238 			}
239 			exe_file_path = Path(buildsettings.targetPath) ~ getTargetFileName(buildsettings, settings.platform);
240 		}
241 
242 		finalizeGeneration(buildsettings, generate_binary);
243 
244 		if( buildsettings.preBuildCommands.length ){
245 			logInfo("Running pre-build commands...");
246 			runBuildCommands(buildsettings.preBuildCommands, buildsettings);
247 		}
248 
249 		buildWithCompiler(settings, buildsettings);
250 
251 		if (is_temp_target) {
252 			m_temporaryFiles ~= exe_file_path;
253 			foreach (f; buildsettings.copyFiles)
254 				m_temporaryFiles ~= Path(buildsettings.targetPath).parentPath ~ Path(f).head;
255 		}
256 	}
257 
258 	private string computeBuildID(string config, in BuildSettings buildsettings, GeneratorSettings settings)
259 	{
260 		import std.digest.digest;
261 		import std.digest.md;
262 		MD5 hash;
263 		hash.start();
264 		void addHash(in string[] strings...) { foreach (s; strings) { hash.put(cast(ubyte[])s); hash.put(0); } hash.put(0); }
265 		addHash(buildsettings.versions);
266 		addHash(buildsettings.debugVersions);
267 		//addHash(buildsettings.versionLevel);
268 		//addHash(buildsettings.debugLevel);
269 		addHash(buildsettings.dflags);
270 		addHash(buildsettings.lflags);
271 		addHash((cast(uint)buildsettings.options).to!string);
272 		addHash(buildsettings.stringImportPaths);
273 		addHash(settings.platform.architecture);
274 		addHash(settings.platform.compiler);
275 		//addHash(settings.platform.frontendVersion);
276 		auto hashstr = hash.finish().toHexString().idup;
277 
278 		return format("%s-%s-%s-%s-%s", config, settings.buildType,
279 			settings.platform.architecture.join("."),
280 			settings.platform.compilerBinary, hashstr);
281 	}
282 
283 	private void copyTargetFile(Path build_path, BuildSettings buildsettings, BuildPlatform platform)
284 	{
285 		auto filename = getTargetFileName(buildsettings, platform);
286 		auto src = build_path ~ filename;
287 		logDiagnostic("Copying target from %s to %s", src.toNativeString(), buildsettings.targetPath);
288 		copyFile(src, Path(buildsettings.targetPath) ~ filename, true);
289 	}
290 
291 	private bool isUpToDate(Path target_path, BuildSettings buildsettings, BuildPlatform platform)
292 	{
293 		import std.datetime;
294 
295 		auto targetfile = target_path ~ getTargetFileName(buildsettings, platform);
296 		if (!existsFile(targetfile)) {
297 			logDiagnostic("Target '%s' doesn't exist, need rebuild.", targetfile.toNativeString());
298 			return false;
299 		}
300 		auto targettime = getFileInfo(targetfile).timeModified;
301 
302 		auto allfiles = appender!(string[]);
303 		allfiles ~= buildsettings.sourceFiles;
304 		allfiles ~= buildsettings.importFiles;
305 		allfiles ~= buildsettings.stringImportFiles;
306 		// TODO: add library files
307 		/*foreach (p; m_project.getTopologicalPackageList())
308 			allfiles ~= p.packageInfoFile.toNativeString();*/
309 
310 		foreach (file; allfiles.data) {
311 			auto ftime = getFileInfo(file).timeModified;
312 			if (ftime > Clock.currTime)
313 				logWarn("File '%s' was modified in the future. Please re-save.", file);
314 			if (ftime > targettime) {
315 				logDiagnostic("File '%s' modified, need rebuild.", file);
316 				return false;
317 			}
318 		}
319 		return true;
320 	}
321 
322 	void buildWithCompiler(GeneratorSettings settings, BuildSettings buildsettings)
323 	{
324 		auto generate_binary = !(buildsettings.options & BuildOptions.syntaxOnly);
325 		auto is_static_library = buildsettings.targetType == TargetType.staticLibrary || buildsettings.targetType == TargetType.library;
326 
327 		Path target_file;
328 		scope (failure) {
329 			logInfo("FAIL %s %s %s" , buildsettings.targetPath, buildsettings.targetName, buildsettings.targetType);
330 			auto tpath = Path(buildsettings.targetPath) ~ getTargetFileName(buildsettings, settings.platform);
331 			if (generate_binary && existsFile(tpath))
332 				removeFile(tpath);
333 		}
334 
335 		/*
336 			NOTE: for DMD experimental separate compile/link is used, but this is not yet implemented
337 			      on the other compilers. Later this should be integrated somehow in the build process
338 			      (either in the package.json, or using a command line flag)
339 		*/
340 		if (settings.platform.compilerBinary != "dmd" || !generate_binary || is_static_library) {
341 			// setup for command line
342 			if (generate_binary) settings.compiler.setTarget(buildsettings, settings.platform);
343 			settings.compiler.prepareBuildSettings(buildsettings, BuildSetting.commandLine);
344 
345 			// don't include symbols of dependencies (will be included by the top level target)
346 			if (is_static_library) buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(f => !f.isLinkerFile()).array;
347 
348 			// invoke the compiler
349 			logInfo("Running %s...", settings.platform.compilerBinary);
350 			settings.compiler.invoke(buildsettings, settings.platform);
351 		} else {
352 			// determine path for the temporary object file
353 			string tempobjname = buildsettings.targetName;
354 			version(Windows) tempobjname ~= ".obj";
355 			else tempobjname ~= ".o";
356 			Path tempobj = Path(buildsettings.targetPath) ~ tempobjname;
357 
358 			// setup linker command line
359 			auto lbuildsettings = buildsettings;
360 			lbuildsettings.sourceFiles = lbuildsettings.sourceFiles.filter!(f => isLinkerFile(f)).array;
361 			settings.compiler.setTarget(lbuildsettings, settings.platform);
362 			settings.compiler.prepareBuildSettings(lbuildsettings, BuildSetting.commandLineSeparate|BuildSetting.sourceFiles);
363 
364 			// setup compiler command line
365 			buildsettings.libs = null;
366 			buildsettings.lflags = null;
367 			buildsettings.addDFlags("-c", "-of"~tempobj.toNativeString());
368 			buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(f => !isLinkerFile(f)).array;
369 			settings.compiler.prepareBuildSettings(buildsettings, BuildSetting.commandLine);
370 
371 			logInfo("Compiling...");
372 			settings.compiler.invoke(buildsettings, settings.platform);
373 
374 			logInfo("Linking...");
375 			settings.compiler.invokeLinker(lbuildsettings, settings.platform, [tempobj.toNativeString()]);
376 		}
377 	}
378 
379 	void runTarget(Path exe_file_path, in BuildSettings buildsettings, string[] run_args)
380 	{
381 		if (buildsettings.targetType == TargetType.executable) {
382 			auto cwd = Path(getcwd());
383 			auto runcwd = cwd;
384 			if (buildsettings.workingDirectory.length) {
385 				runcwd = Path(buildsettings.workingDirectory);
386 				if (!runcwd.absolute) runcwd = cwd ~ runcwd;
387 				logDiagnostic("Switching to %s", runcwd.toNativeString());
388 				chdir(runcwd.toNativeString());
389 			}
390 			scope(exit) chdir(cwd.toNativeString());
391 			if (!exe_file_path.absolute) exe_file_path = cwd ~ exe_file_path;
392 			auto exe_path_string = exe_file_path.relativeTo(runcwd).toNativeString();
393 			version (Posix) { // spawnProcess on Posix systems requires an explicit path to the executable
394 				if (!exe_path_string.startsWith(".") && !exe_path_string.startsWith("/"))
395 					exe_path_string = "./" ~ exe_path_string;
396 			}
397 			logInfo("Running %s %s", exe_path_string, run_args.join(" "));
398 			auto prg_pid = spawnProcess(exe_path_string ~ run_args);
399 			auto result = prg_pid.wait();
400 			enforce(result == 0, "Program exited with code "~to!string(result));
401 		} else logInfo("Target is a library. Skipping execution.");
402 	}
403 
404 	void cleanupTemporaries()
405 	{
406 		foreach_reverse (f; m_temporaryFiles) {
407 			try {
408 				if (f.endsWithSlash) rmdir(f.toNativeString());
409 				else remove(f.toNativeString());
410 			} catch (Exception e) {
411 				logWarn("Failed to remove temporary file '%s': %s", f.toNativeString(), e.msg);
412 				logDiagnostic("Full error: %s", e.toString().sanitize);
413 			}
414 		}
415 		m_temporaryFiles = null;
416 	}
417 }
418 
419 private Path getMainSourceFile(in Package prj)
420 {
421 	foreach (f; ["source/app.d", "src/app.d", "source/"~prj.name~".d", "src/"~prj.name~".d"])
422 		if (existsFile(prj.path ~ f))
423 			return prj.path ~ f;
424 	return prj.path ~ "source/app.d";
425 }
426 
427 unittest {
428 	version (Windows) {
429 		assert(isLinkerFile("test.obj"));
430 		assert(isLinkerFile("test.lib"));
431 		assert(isLinkerFile("test.res"));
432 		assert(!isLinkerFile("test.o"));
433 		assert(!isLinkerFile("test.d"));
434 	} else {
435 		assert(isLinkerFile("test.o"));
436 		assert(isLinkerFile("test.a"));
437 		assert(isLinkerFile("test.so"));
438 		assert(isLinkerFile("test.dylib"));
439 		assert(!isLinkerFile("test.obj"));
440 		assert(!isLinkerFile("test.d"));
441 	}
442 }