1 /**
2  * Copyright: Copyright (c) 2011 Nick Sabalausky. All rights reserved.
3  * Authors: Nick Sabalausky
4  * Version: Initial created: July 26, 2011
5  * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
6  */
7 module dvm.commands.Compile;
8 
9 import tango.core.Exception;
10 import tango.io.Stdout;
11 import tango.io.device.File;
12 import tango.net.http.HttpGet;
13 import tango.sys.Common;
14 import tango.sys.Environment;
15 import tango.sys.Process;
16 import tango.sys.win32.Types;
17 import tango.text.Util;
18 import tango.util.compress.Zip : extractArchive;
19 
20 import mambo.util.Version;
21 
22 import dvm.commands.Command;
23 import dvm.commands.DmcInstall;
24 import dvm.commands.Fetch;
25 import dvm.commands.Install;
26 import dvm.commands.Use;
27 import mambo.core._;
28 import dvm.dvm.Wrapper;
29 import dvm.dvm._;
30 import Path = dvm.io.Path;
31 import dvm.sys.Process;
32 import dvm.util.Util;
33 
34 class Compile : Fetch
35 {
36     private
37     {
38         static if (Windows)
39         {
40             string origMakefile       = "win32.mak";
41             string secondaryMakefile  = "win32.mak";
42             string patchedDMDMakefile = "win32-fixed.mak";
43         }
44 
45         else
46         {
47             string origMakefile       = "posix.mak";
48             string secondaryMakefile  = options.platform ~ ".mak";
49             string patchedDMDMakefile = "posix.mak";
50         }
51 
52         bool isGitStructure;
53         bool isD1;
54 
55         string base;
56         string dmdBase;
57         string dmdPath;
58         string druntimePath;
59         string phobosPath;
60         string toolsPath;
61 
62         string installBinName;
63         string installLibName;
64 
65         string installPath;
66         string installBin;
67         string installLib;
68 
69         string latestDMDPath;
70         string latestDMDBin;
71         string latestDMDLib;
72 
73         string phobosLibName;
74 
75         string dmdMakefile;
76         string druntimeMakefile;
77         string phobosMakefile;
78     }
79 
80     this ()
81     {
82         super("compile", "Compiles DMD and standard library.");
83     }
84 
85     override void execute ()
86     {
87         compile("", options.compileDebug);
88     }
89 
90 protected:
91 
92     void compile (string directory="", bool compileDebug=false)
93     {
94         if (directory == "")
95         {
96             if (args.any())
97                 directory = args.first;
98 
99             else
100                 directory = ".";
101         }
102 
103         base = Path.normalize(directory);
104         if (base == "")
105             base = ".";
106 
107         analyzeStructure;
108         verifyStructure;
109 
110         verbose("Project structure: ", isGitStructure? "Git-style" : "Release-style");
111 
112         version (Windows)
113             installDMC();
114 
115         if (isGitStructure)
116             installLatestDMD; // Need this for sc.ini/dmd.conf and packaged static libs
117 
118         // Save current dir
119         auto saveCwd = Environment.cwd;
120         scope(exit) Environment.cwd = saveCwd;
121 
122         // Save PATH
123         auto savePath = Environment.get("PATH");
124         scope(exit) Environment.set("PATH", savePath);
125 
126         version (Windows)
127         {
128             // Using the dmc.bat wrapper to compile DMD results in a mysterious
129             // "paths with spaces" heisenbug that I can't seem to track down.
130             if (Path.exists(Path.join(options.path.compilers, "dmc")))
131             {
132                 auto dmcPath = Path.join(options.path.compilers, "dmc", "bin");
133                 addEnvPath(dmcPath);
134             }
135         }
136 
137         compileDMD(compileDebug);
138 
139         // Add the new dmd to PATH
140         addEnvPath(installBin);
141 
142         compileDruntime;
143         compilePhobos(compileDebug);
144         compileRDMD(compileDebug);
145     }
146 
147 private:
148 
149     void analyzeStructure ()
150     {
151         auto gitDMDPath      = Path.join(base, "dmd", "src");
152         auto gitDruntimePath = Path.join(base, "druntime");
153         auto gitPhobosPath   = Path.join(base, "phobos");
154         auto gitToolsPath    = Path.join(base, "tools");
155 
156         if (Path.exists(gitDMDPath) && Path.exists(gitPhobosPath))
157         {
158             isGitStructure = true;
159             dmdBase      = Path.join(base, "dmd");
160             dmdPath      = gitDMDPath;
161             druntimePath = gitDruntimePath;
162             phobosPath   = gitPhobosPath;
163             toolsPath    = gitToolsPath;
164             installPath  = dmdBase;
165         }
166         else
167         {
168             isGitStructure = false;
169             dmdBase      = base;
170             dmdPath      = Path.join(base, "src", "dmd");
171             druntimePath = Path.join(base, "src", "druntime");
172             phobosPath   = Path.join(base, "src", "phobos");
173             //toolsPath    = ; // N/A: Releases don't include 'rdmd.d'
174             installPath  = Path.join(base, options.platform);
175         }
176 
177         dmdBase      = Environment.toAbsolute(dmdBase.toMutable).assumeUnique;
178         dmdPath      = Environment.toAbsolute(dmdPath.toMutable).assumeUnique;
179         druntimePath = Environment.toAbsolute(druntimePath.toMutable).assumeUnique;
180         phobosPath   = Environment.toAbsolute(phobosPath.toMutable).assumeUnique;
181         toolsPath    = Environment.toAbsolute(toolsPath.toMutable).assumeUnique;
182         installPath  = Environment.toAbsolute(installPath.toMutable).assumeUnique;
183 
184         installBinName = firstExisting(["bin"[], "bin"~bitsLabel], installPath, null, "bin"~bitsLabel);
185         installLibName = firstExisting(["lib"[], "lib"~bitsLabel], installPath, null, "lib"~bitsLabel);
186 
187         installBin = Path.join(installPath, installBinName);
188         installLib = Path.join(installPath, installLibName);
189 
190         dmdMakefile      = Path.exists(Path.join(dmdPath,      origMakefile))? origMakefile : secondaryMakefile;
191         druntimeMakefile = Path.exists(Path.join(druntimePath, origMakefile))? origMakefile : secondaryMakefile;
192         phobosMakefile   = Path.exists(Path.join(phobosPath,   origMakefile))? origMakefile : secondaryMakefile;
193 
194         isD1 = !Path.exists(druntimePath);
195 
196         version (Posix)
197             patchedDMDMakefile = dmdMakefile;
198 
199         version (Windows)
200             phobosLibName = "phobos";
201 
202         else
203             phobosLibName = isD1? "libphobos" : "libphobos2";
204 
205         phobosLibName = phobosLibName ~ options.path.libExtension;
206     }
207 
208     void verifyStructure ()
209     {
210         bool valid = true;
211 
212         if (!Path.exists(Path.join(dmdPath, dmdMakefile)))
213             valid = false;
214 
215         if (!isD1 && !Path.exists(Path.join(druntimePath, druntimeMakefile)))
216             valid = false;
217 
218         if (!Path.exists(Path.join(phobosPath, phobosMakefile)))
219             valid = false;
220 
221         if (!valid)
222         {
223             throw new DvmException(
224                 "Unexpected DMD project structure in '"~base~"'\n\n"~
225                 "Make sure the path you give (or the current path) is either the\n"~
226                 "top-level of an extracted DMD release or a directory containing\n"~
227                 "Git checkouts of dmd, druntime, and phobos.",
228             __FILE__, __LINE__);
229         }
230     }
231 
232     version (Windows)
233         void installDMC ()
234         {
235             // Only install if not on PATH
236             auto path = Environment.exePath("dmc.exe".toMutable);
237             if(path)
238                 return;
239 
240             path = Environment.exePath("dmc.bat".toMutable);
241             if(path)
242                 return;
243 
244             auto dmcInstall = new DmcInstall();
245             dmcInstall.execute();
246         }
247 
248     void installLatestDMD ()
249     {
250         // Only install if missing
251         auto ver = "2." ~ getLatestDMDVersion("2");
252         latestDMDPath = Path.join(options.path.compilers, "dmd-" ~ ver);
253 
254         latestDMDLib = Path.join(latestDMDPath, "lib");
255         latestDMDBin = Path.join(latestDMDPath, "bin");
256 
257         if (!Path.exists(latestDMDPath))
258         {
259             auto install = new Install();
260             install.install(ver);
261         }
262     }
263 
264     void compileDMD (bool compileDebug)
265     {
266         Environment.cwd = dmdPath;
267 
268         string targetName;
269         version (Windows)
270             targetName = compileDebug? "debdmd" : "release";
271 
272         version (Windows)
273             patchDMDMake;
274 
275         // Build dmd executable
276         verbose("Building DMD: ", dmdPath);
277         auto result = system(("make -f" ~ patchedDMDMakefile ~ " " ~ targetName).toMutable);
278 
279         if (result.status != 0)
280             throw new DvmException("Error building DMD's executable", __FILE__, __LINE__);
281 
282         auto dmdExeName = "dmd" ~ options.path.executableExtension;
283 
284         // Copy dmd executable
285         Path.copy(Path.join(dmdPath, dmdExeName), Path.join(installBin, dmdExeName));
286 
287         // Set executable permissions
288         version (Posix)
289         {
290             Path.permission(Path.join(dmdPath, dmdExeName), "+x");
291             Path.permission(Path.join(installBin, dmdExeName), "+x");
292         }
293 
294         // Copy needed files from lib/bin directories
295         if (isGitStructure)
296         {
297             verbose("Copying lib/bin directories: ");
298 
299             auto fileSets = ["lib"[]: Path.children(latestDMDLib), "bin": Path.children(latestDMDBin)];
300 
301             foreach (srcSubDir, fileSet; fileSets)
302             foreach (info; fileSet)
303             if (!info.folder)
304             if (info.name != dmdExeName && info.name != phobosLibName)
305             {
306                 auto targetSubDir = (srcSubDir ~ bitsLabel).assumeUnique;
307 
308                 auto sourcePath = Path.join(latestDMDPath, srcSubDir, info.name);
309                 auto targetPath = Path.join(installPath, targetSubDir, info.name);
310 
311                 if (!Path.exists(targetPath))
312                     Path.copy(sourcePath, targetPath);
313 
314                 else if (info.name == options.path.confName)
315                 {
316                     Path.copy(targetPath, targetPath ~ ".dvm-bak");
317                     Path.copy(sourcePath, targetPath);
318                 }
319             }
320 
321             patchDmdConf();
322         }
323     }
324 
325     void compileDruntime ()
326     {
327         if (isD1)
328             return;
329 
330         verbose("Building druntime: ", druntimePath);
331 
332         Environment.cwd = druntimePath;
333         auto result = system(("make -f" ~ druntimeMakefile).toMutable);
334 
335         if (result.status != 0)
336             throw new DvmException("Error building druntime", __FILE__, __LINE__);
337     }
338 
339     void compilePhobos (bool compileDebug)
340     {
341         verbose("Building phobos: ", phobosPath);
342 
343         // Rebuilding minit.obj should never be necessary, and doing so requires
344         // an assembler not included in DMC, so force it to never be rebuilt.
345         version (Windows)
346         {
347             auto minitSearchPaths = [phobosPath, druntimePath, Path.join(druntimePath, "src", "rt")];
348             auto minitPath = firstExisting(minitSearchPaths, null, "minit.obj");
349             touch(Path.join(minitPath, "minit.obj"));
350         }
351 
352         string targetName;
353         version (Posix)
354         {
355             if (!isD1)
356                 targetName = compileDebug? "debug" : "release";
357         }
358 
359         string dirDef;
360         string dmdDef;
361         version (Windows)
362         {
363             if (isD1)
364             {
365                 dirDef = " " ~ quote("DIR=" ~ dmdBase);
366                 dmdDef = " " ~ quote("DMD=" ~ installBin ~ `\dmd`);
367             }
368         }
369 
370         auto druntimeDef = " " ~ quote("DRUNTIME="~druntimePath);
371 
372         Environment.cwd = phobosPath;
373         auto result = system(("make -f" ~ phobosMakefile ~ " " ~ targetName ~ druntimeDef ~ dirDef ~ dmdDef).toMutable);
374 
375         if (result.status != 0)
376             throw new DvmException("Error building phobos", __FILE__, __LINE__);
377 
378         // Find phobos lib
379         auto generatedDir = Path.join(phobosPath, "generated", options.platform, targetName);
380         auto generatedBitsDir = Path.join(generatedDir, bitsLabel);
381         auto searchPaths = [generatedDir, generatedBitsDir, Path.join(phobosPath, "lib"), Path.join(phobosPath, "lib"~bitsLabel), phobosPath];
382         auto sourcePath = firstExisting(searchPaths, null, phobosLibName);
383         sourcePath = Path.join(sourcePath, phobosLibName);
384 
385         // Copy phobos lib
386         auto targetPath = Path.join(installLib, phobosLibName);
387         Path.copy(sourcePath, targetPath);
388     }
389 
390     void compileRDMD (bool compileDebug)
391     {
392         auto rdmdSrc = Path.join(toolsPath, "rdmd.d");
393         if (!isGitStructure || !Path.exists(rdmdSrc))
394             return;
395 
396         verbose("Building RDMD: ", rdmdSrc);
397 
398         Environment.cwd = toolsPath;
399         auto args = compileDebug? "-debug -gc" : "-release -inline -O";
400         auto result = system("dmd rdmd.d -wi " ~ args);
401 
402         if (result.status != 0)
403             throw new DvmException("Error building RDMD", __FILE__, __LINE__);
404 
405         auto rdmdExeName = "rdmd" ~ options.path.executableExtension;
406         Path.copy(Path.join(toolsPath, rdmdExeName), Path.join(installBin, rdmdExeName));
407     }
408 
409     void patchDmdConf ()
410     {
411         auto patchedFile = Path.join(installBin, options.path.confName);
412         verbose("Patching: ", patchedFile);
413 
414         auto content = cast(string) File.get(patchedFile);
415 
416         auto newPath = isGitStructure? "%@P%/../.." : "%@P%/../../src";
417         content = content.slashSafeSubstitute("%@P%/../src", newPath);
418         content = content.slashSafeSubstitute("%@P%/../lib", "%@P%/../"~installLibName);
419         content = content.slashSafeSubstitute("%@P%/../lib3264", "%@P%/../lib64");
420         content = content.slashSafeSubstitute("%@P%/../lib6464", "%@P%/../lib64");
421 
422         File.set(patchedFile, content);
423     }
424 
425     void patchDMDMake ()
426     {
427         auto srcPath  = Path.join(dmdPath, dmdMakefile);
428         auto destPath = Path.join(dmdPath, patchedDMDMakefile);
429 
430         verbose("Patching:");
431         verbose(options.indentation, "source: ", srcPath);
432         verbose(options.indentation, "destination: ", destPath, '\n');
433 
434         auto content = cast(string) File.get(srcPath);
435 
436         content = content.substitute(`CC=\dm\bin\dmc`, `CC=dmc`).assumeUnique;
437         content = content.substitute(dmdMakefile, patchedDMDMakefile).assumeUnique;
438 
439         File.set(destPath, content);
440     }
441 
442     string bitsLabel ()
443     {
444         return options.is64bit? "64" : "32";
445     }
446 
447     string quote (string str)
448     {
449         version (Windows)
450             return format(`"{}"`, str);
451 
452         else
453             return format(`'{}'`, str);
454     }
455 
456     void addEnvPath (string path)
457     {
458         Environment.set("PATH", path ~ options.path.pathSeparator ~ Environment.get("PATH"));
459     }
460 
461     void touch(string filename)
462     {
463         // Merely opening the file for append and closing doesn't appear to work
464         auto data = File.get(filename);
465         File.set(filename, data);
466     }
467 
468     // Returns the first element of paths for which 'pre/path/post' exists,
469     // or defaultPath if none.
470     string firstExisting(string[] paths, string pre = null, string post = null, string defaultPath = null)
471     {
472         foreach (string path; paths)
473         {
474             auto testPath = path;
475             testPath = pre is null? testPath : Path.join(pre, testPath);
476             testPath = post is null? testPath : Path.join(testPath, post);
477 
478             if (Path.exists(testPath))
479                 return path;
480         }
481 
482         return defaultPath;
483     }
484 }