1 module reggae.build;
2 
3 import reggae.rules.defaults;
4 import std.string: replace;
5 import std.algorithm: map;
6 import std.path: buildPath;
7 import std.typetuple: allSatisfy;
8 import std.traits: Unqual, isSomeFunction, ReturnType, arity;
9 import std.array: array, join;
10 
11 
12 Target createTargetFromTarget(in Target target) {
13     return Target(target.outputs,
14                   target._command.removeBuilddir,
15                   target.dependencies.map!(a => a.enclose(target)).array,
16                   target.implicits.map!(a => a.enclose(target)).array);
17 }
18 
19 
20 struct Build {
21     const(Target)[] targets;
22 
23     this(in Target[] targets) {
24         this.targets = targets.map!createTargetFromTarget.array;
25     }
26 
27     this(T...)(in T targets) {
28         foreach(t; targets) {
29             static if(isSomeFunction!(typeof(t))) {
30                 const target = t();
31             } else {
32                 const target = t;
33             }
34 
35             this.targets ~= createTargetFromTarget(target);
36         }
37     }
38 }
39 
40 //a directory for each top-level target no avoid name clashes
41 //@trusted because of map -> buildPath -> array
42 Target enclose(in Target target, in Target topLevel) @trusted {
43     if(target.isLeaf) return Target(target.outputs.map!(a => a.removeBuilddir).array,
44                                     target._command.removeBuilddir,
45                                     target.dependencies,
46                                     target.implicits);
47 
48     immutable dirName = buildPath("objs", topLevel.outputs[0] ~ ".objs");
49     return Target(target.outputs.map!(a => realTargetPath(dirName, a)).array,
50                   target._command.removeBuilddir,
51                   target.dependencies.map!(a => a.enclose(topLevel)).array,
52                   target.implicits.map!(a => a.enclose(topLevel)).array);
53 }
54 
55 immutable gBuilddir = "$builddir";
56 
57 
58 private string realTargetPath(in string dirName, in string output) @trusted pure {
59     import std.algorithm: canFind;
60 
61     return output.canFind(gBuilddir)
62         ? output.removeBuilddir
63         : buildPath(dirName, output);
64 }
65 
66 private string removeBuilddir(in string output) @trusted pure {
67     import std.path: buildNormalizedPath;
68     import std.algorithm;
69     return output.
70         splitter.
71         map!(a => a.canFind(gBuilddir) ? a.replace(gBuilddir, ".").buildNormalizedPath : a).
72         join(" ");
73 }
74 
75 enum isTarget(alias T) = is(Unqual!(typeof(T)) == Target) ||
76     isSomeFunction!T && is(ReturnType!T == Target);
77 
78 unittest {
79     auto  t1 = Target();
80     const t2 = Target();
81     static assert(isTarget!t1);
82     static assert(isTarget!t2);
83 }
84 
85 mixin template build(T...) if(allSatisfy!(isTarget, T)) {
86     Build buildFunc() {
87         return Build(T);
88     }
89 }
90 
91 
92 package template isBuildFunction(alias T) {
93     static if(!isSomeFunction!T) {
94         enum isBuildFunction = false;
95     } else {
96         enum isBuildFunction = is(ReturnType!T == Build) && arity!T == 0;
97     }
98 }
99 
100 unittest {
101     Build myBuildFunction() { return Build(); }
102     static assert(isBuildFunction!myBuildFunction);
103     float foo;
104     static assert(!isBuildFunction!foo);
105 }
106 
107 
108 struct Target {
109     const(string)[] outputs;
110     const(Target)[] dependencies;
111     const(Target)[] implicits;
112 
113     this(in string output) @safe pure nothrow {
114         this(output, null, null);
115     }
116 
117     this(in string output, string command, in Target dependency,
118          in Target[] implicits = []) @safe pure nothrow {
119         this([output], command, [dependency], implicits);
120     }
121 
122     this(in string output, string command,
123          in Target[] dependencies, in Target[] implicits = []) @safe pure nothrow {
124         this([output], command, dependencies, implicits);
125     }
126 
127     this(in string[] outputs, string command,
128          in Target[] dependencies, in Target[] implicits = []) @safe pure nothrow {
129         this.outputs = outputs;
130         this.dependencies = dependencies;
131         this.implicits = implicits;
132         this._command = command;
133     }
134 
135     @property string dependencyFilesString(in string projectPath = "") @safe pure const nothrow {
136         return depFilesStringImpl(dependencies, projectPath);
137     }
138 
139     @property string implicitFilesString(in string projectPath = "") @safe pure const nothrow {
140         return depFilesStringImpl(implicits, projectPath);
141     }
142 
143     @property string command(in string projectPath = "") @trusted pure const nothrow {
144         //functional didn't work here, I don't know why so sticking with loops for now
145         string[] depOutputs;
146         foreach(dep; dependencies) {
147             foreach(output; dep.outputs) {
148                 //leaf objects are references to source files in the project path
149                 //those need their path built. Any other dependencies are in the
150                 //build path, so they don't need the same treatment
151                 depOutputs ~= dep.isLeaf ? buildPath(projectPath, output) : output;
152             }
153         }
154         auto replaceIn = _command.replace("$in", depOutputs.join(" "));
155         auto replaceOut = replaceIn.replace("$out", outputs.join(" "));
156         return replaceOut.replace("$project", projectPath);
157     }
158 
159     bool isLeaf() @safe pure const nothrow {
160         return dependencies is null && implicits is null;
161     }
162 
163     //@trusted because of replace
164     string rawCmdString(in string projectPath) @trusted pure nothrow const {
165         return _command.replace("$project", projectPath);
166     }
167 
168 
169     string shellCommand(in string projectPath = "") @safe pure const {
170         immutable rawCmdLine = rawCmdString(projectPath);
171         if(rawCmdLine.isDefaultCommand) {
172             return defaultCommand(projectPath, rawCmdLine);
173         } else {
174             return command(projectPath);
175         }
176     }
177 
178     string[] outputsInProjectPath(in string projectPath) @safe pure nothrow const {
179         return outputs.map!(a => isLeaf ? buildPath(projectPath, a) : a).array;
180     }
181 
182 private:
183 
184     string _command;
185 
186     //@trusted because of join
187     string depFilesStringImpl(in Target[] deps, in string projectPath) @trusted pure const nothrow {
188         import std.conv;
189         string files;
190         //join doesn't do const, resort to loops
191         foreach(i, dep; deps) {
192             files ~= text(dep.outputsInProjectPath(projectPath).join(" "));
193             if(i != deps.length - 1) files ~= " ";
194         }
195         return files;
196     }
197 
198     //this function returns a string to be run by the shell with `std.process.execute`
199     //it does 'normal' commands, not built-in rules
200     string defaultCommand(in string projectPath, in string rawCmdLine) @safe pure const {
201         import reggae.config: dCompiler, cppCompiler, cCompiler;
202 
203         immutable flags = rawCmdLine.getDefaultRuleParams("flags", []).join(" ");
204         immutable includes = rawCmdLine.getDefaultRuleParams("includes", []).join(" ");
205         immutable depfile = outputs[0] ~ ".dep";
206 
207         string ccCommand(in string compiler) {
208             import std.stdio;
209             debug writeln("ccCommand with compiler ", compiler);
210             return [compiler, flags, includes, "-MMD", "-MT", outputs[0],
211                     "-MF", depfile, "-o", outputs[0], "-c",
212                     dependencyFilesString(projectPath)].join(" ");
213         }
214 
215 
216         immutable rule = rawCmdLine.getDefaultRule;
217         import std.stdio;
218         debug writeln("rule: ", rule);
219 
220         switch(rule) {
221 
222         case "_dcompile":
223             immutable stringImports = rawCmdLine.getDefaultRuleParams("stringImports", []).join(" ");
224             immutable command = [".reggae/reggaebin",
225                                  "--objFile=" ~ outputs[0],
226                                  "--depFile=" ~ depfile, dCompiler,
227                                  flags, includes, stringImports,
228                                  dependencyFilesString(projectPath),
229                 ].join(" ");
230 
231             return command;
232 
233         case "_cppcompile": return ccCommand(cppCompiler);
234         case "_ccompile":   return ccCommand(cCompiler);
235         case "_dlink":
236             return [dCompiler, "-of" ~ outputs[0],
237                     flags,
238                     dependencyFilesString(projectPath)].join(" ");
239         default:
240             assert(0, "Unknown default rule " ~ rule);
241         }
242     }
243 }