1 module reggae.backend.make;
2 
3 
4 struct Makefile {
5 
6     import reggae.build: Build, Target;
7     import reggae.options: Options;
8 
9     Build build;
10     const(Options) options;
11     string projectPath;
12 
13     this(Build build, in Options options) @safe pure {
14         this.build = build;
15         this.options = options;
16     }
17 
18     string fileName() @safe pure nothrow const {
19         return "Makefile";
20     }
21 
22     //only the main targets
23     string simpleOutput() @safe {
24 
25         import reggae.options: banner;
26         import reggae.build: CommandType;
27         import std.conv: text;
28         import std.array: join;
29 
30         auto ret = banner;
31         ret ~= text("all: ", build.defaultTargetsString(options.projectPath), "\n");
32         ret ~= ".SUFFIXES:\n"; //disable default rules
33         ret ~= options.compilerVariables.join("\n") ~ "\n";
34 
35         ret ~= "$(VERBOSE).SILENT:\n"; // Do not display executed commands
36 
37         foreach(target; build.range) {
38 
39             mkDir(target);
40 
41             immutable output = target.expandOutputs(options.projectPath).join(" ");
42             if(target.getCommandType == CommandType.phony) {
43                 ret ~= ".PHONY: " ~ output ~ "\n";
44             }
45             ret ~= output ~  ": ";
46 
47             const deps =
48                 target.dependenciesInProjectPath(options.projectPath)
49                 ~ target.implicitsInProjectPath(options.projectPath)
50                 ;
51             ret ~= deps.join(" ");
52 
53             ret ~= " " ~ fileName() ~ "\n";
54             ret ~= "\t@echo [make] Building " ~ output ~ "\n";
55             ret ~= "\t" ~ command(target) ~ "\n";
56         }
57 
58         return ret;
59     }
60 
61     private static string replaceEnvVars(in string str) @safe {
62         import std.regex: regex, matchAll;
63         import std.algorithm: _sort = sort, uniq, map;
64         import std.array: array, replace;
65         import std.process: environment;
66 
67         auto re = regex(`\$(\w+)`);
68         auto envVars = str.matchAll(re).map!(a => a.hit).array._sort.uniq;
69         string ret = str;
70 
71         foreach(var; envVars) {
72             ret = ret.replace(var, environment.get(var[1..$], ""));
73         }
74 
75         return ret;
76     }
77 
78     //includes rerunning reggae
79     string output() @safe {
80 
81         import std.array: join;
82 
83         auto ret = simpleOutput;
84 
85         if(options.export_) {
86             ret = options.eraseProjectPath(ret);
87         } else {
88             // add a dependency on the Makefile to reggae itself and the build description,
89             // but only if not exporting a build
90             ret ~= fileName() ~ ": " ~ options.reggaeFileDependencies.join(" ") ~ "\n";
91             ret ~= "\t" ~ options.rerunArgs.join(" ") ~ "\n";
92         }
93 
94         return replaceEnvVars(ret);
95     }
96 
97     void writeBuild() @safe {
98         import std.stdio: File;
99         import std.path: buildPath;
100 
101         auto output = output();
102         auto file = File(buildPath(options.workingDir, fileName), "w");
103         file.write(output);
104     }
105 
106     //the only reason this is needed is to add auto dependency
107     //tracking
108     string command(Target target) @safe const {
109         import reggae.build: CommandType, replaceConcreteCompilersWithVars;
110 
111         immutable cmdType = target.getCommandType;
112         if(cmdType == CommandType.code)
113             throw new Exception("Command type 'code' not supported for make backend");
114 
115         immutable cmd = target.shellCommand(options).replaceConcreteCompilersWithVars(options);
116         immutable depfile = target.expandOutputs(options.projectPath)[0] ~ ".dep";
117         if(target.hasDefaultCommand) {
118             return cmdType == CommandType.link ? cmd : cmd ~ makeAutoDeps(depfile);
119         } else {
120             return cmd;
121         }
122     }
123 
124     private void mkDir(Target target) @trusted const {
125         import std.path: dirName;
126         import std.file: exists, mkdirRecurse;
127 
128         foreach(output; target.expandOutputs(options.projectPath)) {
129             import std.file;
130             if(!output.dirName.exists) mkdirRecurse(output.dirName);
131         }
132     }
133 }
134 
135 
136 //For explanation of the crazy Makefile commands, see:
137 //http://stackoverflow.com/questions/8025766/makefile-auto-dependency-generation
138 //http://make.mad-scientist.net/papers/advanced-auto-dependency-generation/
139 private string makeAutoDeps(in string depfile) @safe pure nothrow {
140     immutable pFile = depfile ~ ".P";
141     return "\n\t@cp " ~ depfile ~ " " ~ pFile ~ "; \\\n" ~
142         "    sed -e 's/#.*//' -e 's/^[^:]*: *//' -e 's/ *\\$$//' \\\n" ~
143         "        -e '/^$$/ d' -e 's/$$/ :/' < " ~ depfile ~ " >> " ~ pFile ~"; \\\n" ~
144         "    rm -f " ~ depfile ~ "\n\n" ~
145         "-include " ~ pFile ~ "\n\n";
146 }