1 /**
2  * Code to build DMD/Phobos/Druntime.
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <vladimir@thecybershadow.net>
12  */
13 
14 module ae.sys.d.builder;
15 
16 import std.algorithm;
17 import std.array;
18 import std.datetime;
19 import std.exception;
20 import std.file;
21 import std.path;
22 import std.process;
23 import std.string;
24 
25 import ae.sys.file;
26 
27 /// Class which builds D from source.
28 class DBuilder
29 {
30 	/// DBuilder configuration.
31 	struct Config
32 	{
33 		/// Build configuration.
34 		struct Build
35 		{
36 			version (Windows)
37 				enum defaultModel = "32";
38 			else
39 			version (D_LP64)
40 				enum defaultModel = "64";
41 			else
42 				enum defaultModel = "32";
43 
44 			string model = defaultModel; /// Target model ("32" or "64").
45 			bool debugDMD = false;       /// Whether to build a debug DMD.
46 			                             /// Debug builds are faster to build,
47 			                             /// but run slower. Windows only.
48 
49 			/// Returns a string representation of this build configuration
50 			/// usable for a cache directory name. Must reflect all fields.
51 			string toString() const
52 			{
53 				import std.conv : text;
54 				string buildID = text(model);
55 				if (debugDMD)
56 					buildID ~= "-debug";
57 				return buildID;
58 			}
59 		}
60 		Build build; /// ditto
61 
62 		/// Local configuration.
63 		struct Local
64 		{
65 			string repoDir;  /// D source location (as checked out from GitHub).
66 			string buildDir; /// Build target directory.
67 			string dmcDir;   /// Where dmc.zip is unpacked.
68 			string vsDir;    /// Where Visual Studio is installed
69 			string sdkDir;   /// Where the Windows SDK is installed
70 
71 			/// D build environment.
72 			string[string] env;
73 		}
74 		Local local; /// ditto
75 	}
76 	Config config;
77 
78 	/// Build everything.
79 	void build()
80 	{
81 		buildDMD();
82 		buildPhobosIncludes();
83 		buildDruntime();
84 		buildPhobos();
85 		buildTools();
86 	}
87 
88 	void buildDMD()
89 	{
90 		{
91 			auto owd = pushd(buildPath(config.local.repoDir, "dmd", "src"));
92 			string[] targets = config.build.debugDMD ? [] : ["dmd"];
93 			run(["make", "-f", makeFileName, "MODEL=" ~ config.build.model] ~ targets);
94 		}
95 
96 		install(
97 			buildPath(config.local.repoDir, "dmd", "src", "dmd" ~ binExt),
98 			buildPath(config.local.buildDir, "bin", "dmd" ~ binExt),
99 		);
100 
101 		version (Windows)
102 		{
103 			// TODO: either properly detect where VS and the SDK are installed,
104 			// or obtain and create a portable install with only the necessary components,
105 			// as done here: https://github.com/CyberShadow/FarCI
106 			auto ini = q"EOS
107 [Environment]
108 LIB="%@P%\..\lib"
109 DFLAGS="-I%@P%\..\import"
110 DMC=__DMC__
111 LINKCMD=%DMC%\link.exe
112 [Environment64]
113 LIB="%@P%\..\lib"
114 DFLAGS=%DFLAGS% -L/OPT:NOICF
115 VSINSTALLDIR=__VS__\
116 VCINSTALLDIR=%VSINSTALLDIR%VC\
117 PATH=%PATH%;%VCINSTALLDIR%\bin\amd64
118 WindowsSdkDir=__SDK__
119 LINKCMD=%VCINSTALLDIR%\bin\amd64\link.exe
120 LIB=%LIB%;"%VCINSTALLDIR%\lib\amd64"
121 LIB=%LIB%;"%WindowsSdkDir%\Lib\winv6.3\um\x64"
122 LIB=%LIB%;"%WindowsSdkDir%\Lib\win8\um\x64"
123 LIB=%LIB%;"%WindowsSdkDir%\Lib\x64"
124 EOS";
125 			ini = ini.replace("__DMC__", config.local.dmcDir.buildPath(`bin`).absolutePath());
126 			ini = ini.replace("__VS__" , config.local.vsDir .absolutePath());
127 			ini = ini.replace("__SDK__", config.local.sdkDir.absolutePath());
128 
129 			buildPath(config.local.buildDir, "bin", "sc.ini").write(ini);
130 		}
131 		else
132 		{
133 			auto ini = q"EOS
134 [Environment]
135 DFLAGS="-I%@P%/../import" "-L-L%@P%/../lib"
136 EOS";
137 			buildPath(config.local.buildDir, "bin", "dmd.conf").write(ini);
138 		}
139 
140 		log("DMD OK!");
141 	}
142 
143 	@property string[] platformMakeVars()
144 	{
145 		string[] args;
146 
147 		args ~= "MODEL=" ~ config.build.model;
148 
149 		version (Windows)
150 			if (config.build.model == "64")
151 			{
152 				args ~= "VCDIR="  ~ config.local.vsDir .absolutePath() ~ `\VC`;
153 				args ~= "SDKDIR=" ~ config.local.sdkDir.absolutePath();
154 			}
155 
156 		return args;
157 	}
158 
159 	void buildDruntime()
160 	{
161 		{
162 			auto owd = pushd(buildPath(config.local.repoDir, "druntime"));
163 
164 			mkdirRecurse("import");
165 			mkdirRecurse("lib");
166 
167 			setTimes(buildPath("src", "rt", "minit.obj"), Clock.currTime(), Clock.currTime());
168 
169 			run(["make", "-f", makeFileNameModel] ~ platformMakeVars);
170 		}
171 
172 		install(
173 			buildPath(config.local.repoDir, "druntime", "import"),
174 			buildPath(config.local.buildDir, "import"),
175 		);
176 
177 
178 		log("Druntime OK!");
179 	}
180 
181 	void buildPhobosIncludes()
182 	{
183 		// In older versions of D, Druntime depended on Phobos modules.
184 		foreach (f; ["std", "etc", "crc32.d"])
185 			if (buildPath(config.local.repoDir, "phobos", f).exists)
186 				install(
187 					buildPath(config.local.repoDir, "phobos", f),
188 					buildPath(config.local.buildDir, "import", f),
189 				);
190 	}
191 
192 	void buildPhobos()
193 	{
194 		string[] targets;
195 
196 		{
197 			auto owd = pushd(buildPath(config.local.repoDir, "phobos"));
198 			version (Windows)
199 			{
200 				auto lib = "phobos%s.lib".format(modelSuffix);
201 				run(["make", "-f", makeFileNameModel, lib] ~ platformMakeVars);
202 				enforce(lib.exists);
203 				targets = [lib];
204 			}
205 			else
206 			{
207 				run(["make", "-f", makeFileNameModel] ~ platformMakeVars);
208 				targets = "generated".dirEntries(SpanMode.depth).filter!(de => de.name.endsWith(".a")).map!(de => de.name).array();
209 			}
210 		}
211 
212 		foreach (lib; targets)
213 			install(
214 				buildPath(config.local.repoDir, "phobos", lib),
215 				buildPath(config.local.buildDir, "lib", lib.baseName()),
216 			);
217 
218 		log("Phobos OK!");
219 	}
220 
221 	void buildTools()
222 	{
223 		// Just build rdmd
224 		{
225 			auto owd = pushd(buildPath(config.local.repoDir, "tools"));
226 			run(["dmd", "-m" ~ config.build.model, "rdmd"]);
227 		}
228 		install(
229 			buildPath(config.local.repoDir, "tools", "rdmd" ~ binExt),
230 			buildPath(config.local.buildDir, "bin", "rdmd" ~ binExt),
231 		);
232 
233 		log("Tools OK!");
234 	}
235 
236 protected:
237 	@property string modelSuffix() { return config.build.model == config.init.build.model ? "" : config.build.model; }
238 	version (Windows)
239 	{
240 		enum string makeFileName = "win32.mak";
241 		@property string makeFileNameModel() { return "win"~config.build.model~".mak"; }
242 		enum string binExt = ".exe";
243 	}
244 	else
245 	{
246 		enum string makeFileName = "posix.mak";
247 		enum string makeFileNameModel = "posix.mak";
248 		enum string binExt = "";
249 	}
250 
251 	void run(string[] args, ref string[string] newEnv)
252 	{
253 		if (newEnv is null) newEnv = environment.toAA();
254 		string oldPath = environment["PATH"];
255 		scope(exit) environment["PATH"] = oldPath;
256 		environment["PATH"] = newEnv["PATH"];
257 		log("PATH=" ~ newEnv["PATH"]);
258 
259 		auto status = spawnProcess(args, newEnv, .Config.newEnv).wait();
260 		enforce(status == 0, "Command %s failed with status %d".format(args, status));
261 	}
262 
263 	void run(string[] args...)
264 	{
265 		run(args, config.local.env);
266 	}
267 
268 	void install(string src, string dst)
269 	{
270 		ensurePathExists(dst);
271 		if (src.isDir)
272 		{
273 			if (!dst.exists)
274 				dst.mkdirRecurse();
275 			foreach (de; src.dirEntries(SpanMode.shallow))
276 				install(de.name, dst.buildPath(de.name.baseName));
277 		}
278 		else
279 		{
280 			debug log(src ~ " -> " ~ dst);
281 			try
282 				hardLink(src, dst);
283 			catch (FileException e)
284 				copy(src, dst);
285 		}
286 	}
287 
288 	/// Override to add logging.
289 	void log(string line)
290 	{
291 	}
292 }