1 /*
2 Copyright (c) 2013-2014 Timur Gafarov 
3 
4 Boost Software License - Version 1.0 - August 17th, 2003
5 
6 Permission is hereby granted, free of charge, to any person or organization
7 obtaining a copy of the software and accompanying documentation covered by
8 this license (the "Software") to use, reproduce, display, distribute,
9 execute, and transmit the Software, and to prepare derivative works of the
10 Software, and to permit third-parties to whom the Software is furnished to
11 do so, all subject to the following:
12 
13 The copyright notices in the Software and this entire statement, including
14 the above license grant, this restriction and the following disclaimer,
15 must be included in all copies of the Software, in whole or in part, and
16 all derivative works of the Software, unless such copies or derivative
17 works are solely in the form of machine-executable object code generated by
18 a source language processor.
19 
20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
23 SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
24 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
25 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
26 DEALINGS IN THE SOFTWARE.
27 */
28 
29 module session;
30 
31 import std.stdio;
32 import std.path;
33 import std.file;
34 import std..string;
35 import std.datetime;
36 import std.conv;
37 import std.digest.crc;
38 import std.process: executeShell;
39 import std.algorithm;
40 static import std.process;
41 import core.stdc.stdlib;
42 
43 import cmdopt;
44 import conf;
45 import lexer;
46 import dmodule;
47 import project;
48 import exdep;
49 
50 class BuildSession
51 {
52     CmdOptions ops;
53     Config config;
54     string[] versionIds;
55     string[] debugIds;
56     string[] externals;
57     bool[string] scanned;
58 
59     // Quit at any time without throwing an exception
60     void quit(int code, string message = "")
61     {
62         if (message.length > 0)
63             writefln("Cook session failed: %s", message);
64 
65         if (ops._debug_)
66             printConfig();
67 
68         version(Windows) 
69             executeShell("pause");
70         core.stdc.stdlib.exit(code);
71     }
72 
73     void printConfig()
74     {
75         writeln("Configuration:");
76         string[] confContents;
77         foreach(i, v; config.data)
78         {
79             string f = formatPattern(v, config, '%');
80             confContents ~= std..string.format(" %s: %s", i, f);
81         }
82         confContents.sort;
83         foreach(i, v; confContents)
84             writeln(v);
85     }
86 
87     this(CmdOptions cmdops, Config conf)
88     {
89         ops = cmdops;
90         config = conf;
91         prepare();
92     }
93 
94     void prepare()
95     {
96         // Set default configuration keys
97         string tempTarget = "main";
98         config.set("profile", "default", false);
99         config.set("prephase", "", false);
100         config.set("postphase", "", false);
101         config.set("source.language", "d", false);
102         config.set("source.ext", ".d", false);
103         config.set("compiler", "dmd", false);
104         config.set("linker", "dmd", false);
105         version(Windows) config.set("librarian", "lib", false);
106         version(linux) config.set("librarian", "dmd", false);
107         config.set("cflags", "", false); 
108         config.set("lflags", "", false);
109         //config.set("modules", ""); // TODO?
110         config.set("obj.path", "", false);
111         config.set("obj.path.use", "true", false);
112         config.set("obj.ext", "", false);
113         config.set("modules.main", "main", false);
114         config.set("modules.forced", "", false);
115         config.set("target", "", false);
116         config.set("rc", "", false);
117         config.set("modules.cache", "main.cache", false);
118         
119         version(Windows)
120         {
121             config.set("obj.path", "o_windows/");
122             config.set("obj.ext", ".obj");
123         }
124         version(linux)
125         {
126             config.set("obj.path", "o_linux/");
127             config.set("obj.ext", ".o");
128             config.append("lflags", "-L-rpath -L\".\" -L-ldl ");
129         }
130 
131         config.set("modules.maindir", ".");
132 
133         if (ops.targets.length)
134         {
135             // FIXME: currently we use only first given target
136             string mainModule = ops.targets[0];
137             config.set("modules.main", mainModule);
138 
139             // strip extension, if any
140             if (extension(config.get("modules.main")) == ".d") 
141                 config.set("modules.main", config.get("modules.main")[0..$-2]);
142                             
143             string maindir = dirName(config.get("modules.main"));
144             config.set("modules.maindir", maindir);
145         }
146         
147         // Set cache filename
148         string cacheFilePostfix = "-" ~ config.get("profile");
149         version(Windows)
150         {
151             cacheFilePostfix ~= "-windows.cache";
152         }
153         version(linux)
154         {
155             cacheFilePostfix ~= "-linux.cache";
156         }
157         tempTarget = baseName(config.get("modules.main"));
158         config.set("modules.cache", config.get("modules.main") ~ cacheFilePostfix);
159         
160         // Set responce file name
161         string rspFilePostfix = "-" ~ config.get("profile");
162         version(Windows)
163         {
164             rspFilePostfix ~= "-windows.rsp";
165         }
166         version(linux)
167         {
168             rspFilePostfix ~= "-linux.rsp";
169         }         
170         config.set("modules.rsp", config.get("modules.main") ~ rspFilePostfix);
171         
172         config.set("project.compile", "%compiler% %cflags% -c %source% -of%object%", false);
173         if (ops.rsp)
174             config.set("project.link", "%linker% %lflags% -of%target% %packages% @" ~ config.get("modules.rsp"), false);
175         else
176             config.set("project.link", "%linker% %lflags% -of%target% %objects% %packages%", false);
177             
178         if (ops.rsp)
179         {
180             version(Windows) config.set("project.linklib", "%librarian% %lflags% -c -p32 -of%target% @" ~ config.get("modules.rsp"), false);
181             version(linux) config.set("project.linklib", "%librarian% %lflags% -lib -of%target% @" ~ config.get("modules.rsp"), false);
182             version(Windows) config.set("project.linkpkg", "%librarian% %lflags% -c -p32 -of%package% @" ~ config.get("modules.rsp"), false);
183             version(linux) config.set("project.linkpkg", "%librarian% %lflags% -lib -of%package% @" ~ config.get("modules.rsp"), false);
184         }
185         else
186         {
187             version(Windows) config.set("project.linklib", "%librarian% %lflags% -c -p32 -of%target% %objects%", false);
188             version(linux) config.set("project.linklib", "%librarian% %lflags% -lib -of%target% %objects%", false);
189             version(Windows) config.set("project.linkpkg", "%librarian% %lflags% -c -p32 -of%package% %objects%", false);
190             version(linux) config.set("project.linkpkg", "%librarian% %lflags% -lib -of%package% %objects%", false);
191         }
192         
193         version(linux) config.set("project.run", "./%target%", false);
194         version(Windows) config.set("project.run", "%target%", false);
195         config.set("project.packages", "", false);
196 
197         if (ops.output.length)
198         {
199             tempTarget = ops.output;
200         }
201 
202         if (ops.clean)
203         {
204             if (exists(config.get("modules.cache"))) 
205                 std.file.remove(config.get("modules.cache"));
206             if (exists(config.get("modules.rsp"))) 
207                 std.file.remove(config.get("modules.rsp"));
208             // FIXME: use config for these
209             if (exists("o_linux")) rmdirRecurse("o_linux");
210             if (exists("o_windows")) rmdirRecurse("o_windows");
211             quit(0);
212         }
213 
214         if (ops.cache.length) 
215             config.set("modules.cache", ops.cache);
216 
217         if (ops.rc.length) 
218             config.set("rc", ops.rc);
219 
220         if (ops.cflags.length) 
221             config.append("cflags", ops.cflags ~ " ");
222 
223         if (ops.lflags.length) 
224             config.append("lflags", ops.lflags ~ " ");
225 
226         if ("./" ~ tempTarget != ops.program)
227             config.set("target", tempTarget);
228         else 
229             quit(1, "illegal target name: \"" ~ tempTarget ~ "\" (conflicts with Cook executable)");
230 
231         version(Windows)
232         {
233             if (ops.noconsole) 
234                 config.append("lflags", "-L/exet:nt/su:windows ");
235         }
236 
237         // Read user-wide default configuration
238         string userConfigFilename = home(".cook/default.conf", ops);
239         if (exists(userConfigFilename))
240         {
241             readConfiguration(config, userConfigFilename);
242         }
243 
244         // Read default configuration
245         string defaultConfigFilename = "default.conf";
246         if (exists(defaultConfigFilename))
247         {
248             readConfiguration(config, defaultConfigFilename);
249         }
250 
251         // Read project configuration
252         if (ops.conf.length) 
253         {
254             if (exists(ops.conf))
255                 readConfiguration(config, ops.conf);
256         }
257         else
258         {
259             string configFilename = "./" ~ config.get("target") ~ ".conf";
260             if (exists(configFilename))
261                 readConfiguration(config, configFilename);
262         }
263 
264         config.append("cflags", " -I%modules.maindir% ");
265 
266         if (ops.release)
267             config.append("cflags", " -release -O -inline -noboundscheck ");
268     }
269 
270     void build(Project proj)
271     {
272         string externalsStr = config.get("project.external");
273         if (externalsStr.length)
274             externals = split(externalsStr);
275 
276         setVersionIds(proj);
277         
278         if (ops.rebuild)
279         {
280             writeln("Do you really want to rebuild the project? (Y/N)");
281             bool rebuild = false;
282             bool confirmed = false;
283             while(!confirmed)
284             {
285                 string input = readln();
286                 if (input.length)
287                 {
288                     if (input[0] == 'Y' || input[0] == 'y')
289                     {
290                         rebuild = true;
291                         confirmed = true;
292                     }
293                     else if (input[0] == 'N' || input[0] == 'n')
294                     {
295                         confirmed = true;
296                     }
297                 }
298                 
299                 if (!confirmed)
300                     writeln("Please, write Y or N");
301             }
302             
303             if (!rebuild)
304                 ops.rebuild = false;
305         }
306         
307         if (!ops.rebuild)
308             readCache(proj);
309 
310         scanProjectHierarchy(proj);
311         traceBackwardDependencies(proj);
312         addForcedModules(proj);
313         writeCache(proj);
314 
315         if (ops.dump.length)
316         {
317             if (ops.dump in proj.modules)
318             {
319                 auto m = proj.modules[ops.dump];
320                 writefln("Filename: %s", m.filename);
321                 writefln("Package name: %s", m.packageName);
322                 writefln("Last modified: %s", m.lastModified);
323                 writefln("Imports: %s", m.importedFiles);
324                 writefln("Version ids: %s", m.versionIds);
325                 writefln("Debug ids: %s", m.debugIds);
326             }
327         }
328 
329         doPrephase();
330         compileAndLink(proj);
331         strip();
332         run();
333         if (ops._debug_)
334             printConfig();
335     }
336 
337     void setVersionIds(Project proj)
338     {
339         versionIds = split(config.get("version"));
340         debugIds = split(config.get("debug"));
341 
342         foreach(v; versionIds)
343         {
344             proj.versionIds[v] = 1;
345             if (v == "Wine")
346                 proj.versionIds["Windows"] = 1;
347         }
348 
349         foreach(v; debugIds)
350             proj.debugIds[v] = 1;
351 
352         string ver;
353         foreach(i, v; versionIds)
354         {
355             ver ~= " -version=" ~ v;
356         }
357         if (ver.length)
358             config.append("cflags", ver);
359 
360         string deb;
361         foreach(i, v; debugIds)
362         {
363             deb ~= " -debug=" ~ v;
364         }
365         if (deb.length)
366             config.append("cflags", deb);
367     }
368 
369     void readCache(Project proj)
370     {
371         string cacheFile = config.get("modules.cache");
372         if (exists(cacheFile) && !ops.nocache)
373         {
374             string cache = readText(cacheFile);
375             foreach (line; splitLines(cache))
376             {
377                 auto tokens = split(line);
378                 auto m = proj.addModule(tokens[0]);
379                 m.lastModified = SysTime.fromISOExtString(tokens[1]);
380                 m.globalFile = cast(bool)to!uint(tokens[2]);
381                 foreach (i; tokens[3..$])
382                     m.addImportFile(i);        
383             }
384         }
385     }
386 
387     void scanProjectHierarchy(Project proj)
388     {
389         proj.mainModuleFilename = config.get("modules.main") ~ config.get("source.ext");
390         if (exists(proj.mainModuleFilename))
391         {
392             scanDependencies(proj, proj.mainModuleFilename);
393         }
394         else
395             quit(1, "no main module found");
396 
397         if (proj.modules.length == 0) 
398             quit(1, "no source files found");
399     }
400 
401     void scanDependencies(Project proj, string fileName, bool globalFile = false)
402     {
403         scanned[fileName] = true;
404 
405         DModule m;
406 
407         if (!exists(fileName))
408             return;
409         
410         // if it is a new module
411         if (!(fileName in proj.modules))
412         {
413             if (!ops.quiet) writefln("Analyzing \"%s\"...", fileName);
414             m = proj.addModule(fileName);
415             m.globalFile = globalFile;
416             m.lastModified = timeLastModified(fileName);
417             if (!m.buildDependencyList())
418                 quit(1, "cannot build proper dependency list");
419             m.packageName = pathToModule(dirName(fileName));           
420             scanModule(proj, m);
421         }
422         else // if we already have it
423         {
424             m = proj.modules[fileName];
425 
426             //TODO: cache this
427             m.packageName = pathToModule(dirName(fileName));
428 
429             immutable auto lm = timeLastModified(fileName);
430             if (lm > m.lastModified)
431             {
432                 if (!ops.quiet) writefln("Analyzing \"%s\"...", fileName);
433                 m.lastModified = lm;
434                 if (!m.buildDependencyList())
435                     quit(1, "cannot build proper dependency list");
436                 m.forceRebuild = true;
437                 scanModule(proj, m);
438             }    
439         }
440         
441         foreach (mName; m.importedFiles)
442         if (!(mName in scanned))
443         {
444             scanDependencies(proj, mName);
445         }
446     }
447 
448     bool existsInExternals(
449         string filename, 
450         ref string externalFilename)
451     {
452         foreach(e; externals)
453         {
454             string f = e ~ "/" ~ filename;
455             if (exists(f))
456             {
457                 externalFilename = f;
458                 return true;
459             }
460         }
461         return false;
462     }
463 
464     void scanModule(Project proj, DModule m)
465     {
466         foreach(importedFile; m.importedFiles)
467         {
468             string subprojectFile = importedFile;
469             
470             if (config.get("modules.maindir") != ".")
471                 subprojectFile = 
472                     config.get("modules.maindir") ~ "/" ~ importedFile;
473 
474             string externalFile;
475             
476             if (exists(importedFile))
477                 scanDependencies(proj, importedFile);
478             else if (exists(subprojectFile))
479                 scanDependencies(proj, subprojectFile);
480             else if (existsInExternals(subprojectFile, externalFile))
481                 scanDependencies(proj, externalFile, true);
482             else
483             {
484                 // Treat it as package import (<importedFile>/package.d)
485                 string pkgFile = 
486                     stripExtension(importedFile) ~ "/"
487                   ~ moduleToPath("package", config.get("source.ext"));
488                   
489                 string subprojectPkgFile = 
490                     config.get("modules.maindir") ~ "/"
491                   ~ pkgFile;
492                   
493                 if (exists(pkgFile))
494                     scanDependencies(proj, pkgFile);
495                 else if (exists(subprojectPkgFile))
496                     scanDependencies(proj, subprojectPkgFile);
497                 else if (existsInExternals(pkgFile, externalFile))
498                     scanDependencies(proj, externalFile, true);
499             }
500         }
501     }
502 
503     void traceBackwardDependencies(Project proj)
504     {
505         if (!ops.nobacktrace)
506         {
507             foreach(modulei, modulev; proj.modules)
508             {
509                 foreach (mName; modulev.importedFiles)
510                 {
511                     if (mName in proj.modules)
512                     {
513                         auto imModule = proj.modules[mName];
514                         imModule.backdeps[modulei] = modulev;
515                     }
516                 }
517             }
518             foreach(mName, m; proj.modules)
519             {
520                 foreach(i, v; m.backdeps)
521                 {
522                     v.forceRebuild = v.forceRebuild || m.forceRebuild;
523                 }
524             }
525         }
526     }
527 
528     void addForcedModules(Project proj)
529     {
530         foreach(fileName; split(config.get("modules.forced")))
531         {
532             if (exists(fileName))
533             {
534                 DModule m = proj.addModule(fileName);
535                 m.lastModified = timeLastModified(fileName);
536 
537                 if (!m.buildDependencyList())
538                     quit(1, "cannot build proper dependency list");
539 
540                 foreach(importedFile; m.importedFiles)
541                 {
542                     if (exists(importedFile))
543                         scanDependencies(proj, importedFile);
544                 }
545             }
546         }
547     }
548 
549     void doPrephase()
550     {
551         uint retcode;
552         string prephase = formatPattern(config.get("prephase"), config, '%');
553         if (prephase != "")
554         {
555             if (!ops.quiet)
556                 writeln(prephase);
557             if (!ops.emulate)
558             {
559                 retcode = executeShell(prephase).status;
560                 if (retcode)
561                     quit(1, "Prephase error");
562             }
563         }
564     }
565 
566     void writeCache(Project proj)
567     {
568         string cache;
569         foreach (i, m; proj.modules)
570         {
571             //TODO: cache module's package also
572             cache ~= i ~ " " ~ m.toString() ~ "\n";
573         }
574 
575         if (!ops.emulate) 
576             if (!ops.nocache)
577                 std.file.write(config.get("modules.cache"), cache);
578     }
579     
580     void writeResponceFile(string rsp)
581     {
582         if (!ops.emulate) 
583             if (ops.rsp)
584                 std.file.write(config.get("modules.rsp"), rsp);
585     }
586 
587     void compileAndLink(Project proj)
588     {
589         string[] pkgList = split(config.get("project.packages"));
590 
591         string linkList;
592 
593         // Compile modules
594         if (config.get("obj.path") != "")
595             if (!exists(config.get("obj.path"))) 
596                 mkdir(config.get("obj.path"));
597         bool terminate = false;
598         foreach (i, v; proj.modules)
599         {
600             if (!terminate && exists(i))
601             {
602                 string targetObjectName = i;
603                 string tobjext = extension(targetObjectName);
604                 targetObjectName = targetObjectName[0..$-tobjext.length] ~ config.get("obj.ext");
605                 string targetObject;
606                 if (v.globalFile)
607                 {
608                     string hash = crc32Of(targetObjectName).crcHexString;
609                     targetObject = home(format(".cook/obj/%s/%s%s", 
610                         config.get("profile"), 
611                         hash, 
612                         config.get("obj.ext")), ops);
613                 }
614                 else
615                     targetObject = config.get("obj.path") ~ "/" ~ targetObjectName;
616 
617                 if ((timeLastModified(i) > timeLastModified(targetObject, SysTime.min)) 
618                     || v.forceRebuild
619                     || ops.rebuild)
620                 {
621                     if (config.get("obj.path.use") == "false")
622                         targetObject = targetObjectName;
623 
624                     config.set("source", i);
625                     config.set("object", targetObject);
626                     string command = formatPattern(config.get("project.compile"), config, '%');
627                     if (!ops.quiet)
628                         writeln(command);
629                     if (!ops.emulate)
630                     {
631                         immutable auto retcode = executeShell(command).status;
632                         if (retcode)
633                             terminate = true;
634                     }
635                 }
636 
637                 if (config.get("obj.path.use") == "false")
638                     targetObject = targetObjectName;
639 
640                 if (!matches(v.packageName, pkgList))
641                     linkList ~= targetObject ~ " ";
642             }
643         }
644 
645         // If compilation error occured
646         if (terminate)
647         {
648             quit(1, "compilation error");
649         }
650 
651         // Compile resource file, if any
652         version(Windows)
653         {
654             if (config.get("rc").length > 0)
655             {
656                 string res = stripExtension(config.get("rc")) ~ ".res ";
657                 string command = "windres -i " ~ config.get("rc") ~ " -o " ~ res ~ "-O res";
658                 if (!ops.quiet)
659                     writeln(command);
660                 if (!ops.emulate)
661                 {
662                     immutable auto retcode = executeShell(command).status;
663                     if (retcode)
664                         quit(1);
665                 }
666                 config.append("lflags", res); 
667             }
668         }
669 
670         // Link packages, if any
671         // WARNING: alpha stage, needs work!
672         // TODO: do not relink a package, if it is unchanged
673         // TODO: do not create a package, if it is empty
674         string pkgLibList;
675         foreach(pkg; pkgList)
676         {
677             string pkgLinkList;
678             foreach(i, m; proj.modules)
679             {
680                 if (m.packageName == pkg)
681                 {
682                     string targetObjectName = i;
683                     string tobjext = extension(targetObjectName);
684                     targetObjectName = targetObjectName[0..$-tobjext.length] ~ config.get("obj.ext");
685 
686                     string targetObject = config.get("obj.path") ~ targetObjectName;
687                     pkgLinkList ~= targetObject ~ " ";
688                 }
689             }
690 
691             //TODO: pkgext should be a configuration key
692             string pkgExt;
693             version(Windows) pkgExt = ".lib";
694             version(linux) pkgExt = ".a";
695 
696             config.set("objects", pkgLinkList);
697             config.set("package", pkg ~ pkgExt);
698             string command = formatPattern(config.get("project.linkpkg"), config, '%');
699             
700             writeResponceFile(pkgLinkList);
701             
702             if (!ops.quiet)
703                 writeln(command);
704             if (!ops.emulate)
705             {
706                 immutable auto retcode = executeShell(command).status;
707                 if (retcode)
708                     quit(1, "package linking error");
709             }
710 
711             pkgLibList ~= config.get("package") ~ " ";
712         }
713 
714         // Link
715         config.set("objects", linkList);           
716         config.set("packages", pkgLibList);
717         if (ops.lib)
718         {
719             version(Windows) config.append("target", ".lib");
720             version(linux)   config.append("target", ".a");
721             
722             writeResponceFile(linkList);
723 
724             string command = formatPattern(config.get("project.linklib"), config, '%');
725             if (!ops.quiet)
726                 writeln(command);
727             if (!ops.emulate)
728             {
729                 immutable auto retcode = executeShell(command).status;
730                 if (retcode)
731                     quit(1, "linking error");
732             }
733         }
734         else
735         {
736             version(Windows) config.append("target", ".exe"); 
737            
738             writeResponceFile(linkList);
739 
740             string command = formatPattern(config.get("project.link"), config, '%');
741             if (!ops.quiet)
742                 writeln(command);
743             if (!ops.emulate)
744             {
745                 immutable auto retcode = executeShell(command).status;
746                 if (retcode)
747                     quit(1, "linking error");
748             }
749         }
750     }
751 
752     void strip()
753     {
754         if (ops.strip)
755         {
756             version(linux)
757             {
758                 string command = "strip " ~ config.get("target");
759                 if (!ops.quiet)
760                     writeln(command);
761                 if (!ops.emulate)
762                     std.process.system(command);
763             }
764         }
765     }
766 
767     void run()
768     {
769         if (ops.run && !ops.lib) 
770         {
771             if (!ops.emulate)
772             {
773                 string command = formatPattern(config.get("project.run"), config, '%');
774                 if (!ops.quiet)
775                     writeln(command);
776                 if (!ops.emulate)
777                     executeShell(command);
778             }
779         }
780     }
781 }
782