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