1 /**
2  This module contains the core data definitions that allow a build
3  to be expressed in. $(D Build) is a container struct for top-level
4  targets, $(D Target) is the heart of the system.
5  */
6 
7 module reggae.build;
8 import reggae.ctaa;
9 import reggae.rules.common: Language, getLanguage;
10 
11 import std.string: replace;
12 import std.algorithm;
13 import std.path: buildPath;
14 import std.typetuple: allSatisfy;
15 import std.traits: Unqual, isSomeFunction, ReturnType, arity;
16 import std.array: array, join;
17 import std.conv;
18 import std.exception;
19 
20 Target createTopLevelTarget(in Target target) {
21     return Target(target.outputs,
22                   target._command.expandBuildDir,
23                   target.dependencies.map!(a => a.enclose(target)).array,
24                   target.implicits.map!(a => a.enclose(target)).array);
25 }
26 
27 /**
28  Contains the top-level targets.
29  */
30 struct Build {
31     const(Target)[] targets;
32 
33     this(in Target[] targets) {
34         this.targets = targets.map!createTopLevelTarget.array;
35     }
36 
37     this(T...)(in T targets) {
38         foreach(t; targets) {
39             static if(isSomeFunction!(typeof(t))) {
40                 const target = t();
41             } else {
42                 const target = t;
43             }
44 
45             this.targets ~= createTopLevelTarget(target);
46         }
47     }
48 }
49 
50 //a directory for each top-level target no avoid name clashes
51 //@trusted because of map -> buildPath -> array
52 Target enclose(in Target target, in Target topLevel) @trusted {
53     //leaf targets only get the $builddir expansion, nothing else
54     if(target.isLeaf) return Target(target.outputs.map!(a => a._expandBuildDir).array,
55                                     target._command.expandBuildDir,
56                                     target.dependencies,
57                                     target.implicits);
58 
59     //every other non-top-level target gets its outputs placed in a directory
60     //specific to its top-level parent
61     immutable dirName = buildPath("objs", topLevel.outputs[0] ~ ".objs");
62     return Target(target.outputs.map!(a => realTargetPath(dirName, a)).array,
63                   target._command.expandBuildDir,
64                   target.dependencies.map!(a => a.enclose(topLevel)).array,
65                   target.implicits.map!(a => a.enclose(topLevel)).array);
66 }
67 
68 immutable gBuilddir = "$builddir";
69 
70 
71 //targets that have outputs with $builddir in them want to be placed
72 //in a specific place. Those don't get touched. Other targets get
73 //placed in their top-level parent's object directory
74 private string realTargetPath(in string dirName, in string output) @trusted pure {
75     import std.algorithm: canFind;
76 
77     return output.canFind(gBuilddir)
78         ? output._expandBuildDir
79         : buildPath(dirName, output);
80 }
81 
82 private string _expandBuildDir(in string output) @trusted pure {
83     import std.path: buildNormalizedPath;
84     import std.algorithm;
85     return output.
86         splitter.
87         map!(a => a.canFind(gBuilddir) ? a.replace(gBuilddir, ".").buildNormalizedPath : a).
88         join(" ");
89 }
90 
91 enum isTarget(alias T) = is(Unqual!(typeof(T)) == Target) ||
92     isSomeFunction!T && is(ReturnType!T == Target);
93 
94 unittest {
95     auto  t1 = Target();
96     const t2 = Target();
97     static assert(isTarget!t1);
98     static assert(isTarget!t2);
99 }
100 
101 mixin template buildImpl(targets...) if(allSatisfy!(isTarget, targets)) {
102     Build buildFunc() {
103         return Build(targets);
104     }
105 }
106 
107 /**
108  Two variations on a template mixin. When reggae is used as a library,
109  this will essentially build reggae itself as part of the build description.
110 
111  When reggae is used as a command-line tool to generate builds, it simply
112  declares the build function that will be called at run-time. The tool
113  will then compile the user's reggaefile.d with the reggae libraries,
114  resulting in a buildgen executable.
115 
116  In either case, the compile-time parameters of $(D build) are the
117  build's top-level targets.
118  */
119 version(reggaelib) {
120     mixin template build(targets...) if(allSatisfy!(isTarget, targets)) {
121         mixin reggaeGen!(targets);
122     }
123 } else {
124     alias build = buildImpl;
125 }
126 
127 package template isBuildFunction(alias T) {
128     static if(!isSomeFunction!T) {
129         enum isBuildFunction = false;
130     } else {
131         enum isBuildFunction = is(ReturnType!T == Build) && arity!T == 0;
132     }
133 }
134 
135 unittest {
136     Build myBuildFunction() { return Build(); }
137     static assert(isBuildFunction!myBuildFunction);
138     float foo;
139     static assert(!isBuildFunction!foo);
140 }
141 
142 
143 /**
144  The core of reggae's D-based DSL for describing build systems.
145  Targets contain outputs, a command to generate those outputs,
146  explicit dependencies and implicit dependencies. All dependencies
147  are themselves $(D Target) structs.
148 
149  The command is given as a string. In this string, certain words
150  have special meaning: $(D $in), $(D $out), $(D $project) and $(D builddir).
151 
152  $(D $in) gets expanded to all explicit dependencies.
153  $(D $out) gets expanded to all outputs.
154  $(D $project) gets expanded to the project directory (i.e. the directory including
155  the source files to build that was given as a command-line argument). This can be
156  useful when build outputs are to be placed in the source directory, such as
157  automatically generated source files.
158  $(D $builddir) expands to the build directory (i.e. where reggae was run from).
159  */
160 struct Target {
161     const(string)[] outputs;
162     const(Target)[] dependencies;
163     const(Target)[] implicits;
164 
165     this(in string output) @safe pure nothrow {
166         this(output, "", null);
167     }
168 
169     this(C)(in string output,
170             in C command,
171             in Target dependency,
172             in Target[] implicits = []) @safe pure nothrow {
173         this([output], command, [dependency], implicits);
174     }
175 
176     this(C)(in string output,
177             in C command,
178             in Target[] dependencies,
179             in Target[] implicits = []) @safe pure nothrow {
180         this([output], command, dependencies, implicits);
181     }
182 
183     this(C)(in string[] outputs,
184             in C command,
185             in Target[] dependencies,
186             in Target[] implicits = []) @safe pure nothrow {
187 
188         this.outputs = outputs;
189         this.dependencies = dependencies;
190         this.implicits = implicits;
191 
192         static if(is(C == Command))
193             this._command = command;
194         else
195             this._command = Command(command);
196     }
197 
198     @property string dependencyFilesString(in string projectPath = "") @safe pure const nothrow {
199         return depFilesStringImpl(dependencies, projectPath);
200     }
201 
202     @property string implicitFilesString(in string projectPath = "") @safe pure const nothrow {
203         return depFilesStringImpl(implicits, projectPath);
204     }
205 
206     ///replace all special variables with their expansion
207     @property string expandCommand(in string projectPath = "") @trusted pure const nothrow {
208         return _command.expand(projectPath, outputs, inputs(projectPath));
209     }
210 
211     bool isLeaf() @safe pure const nothrow {
212         return dependencies is null && implicits is null;
213     }
214 
215     //@trusted because of replace
216     string rawCmdString(in string projectPath) @trusted pure nothrow const {
217         return _command.rawCmdString(projectPath);
218     }
219 
220     ///returns a command string to be run by the shell
221     string shellCommand(in string projectPath = "") @safe pure const {
222         return _command.shellCommand(projectPath, outputs, inputs(projectPath));
223     }
224 
225     string[] outputsInProjectPath(in string projectPath) @safe pure nothrow const {
226         return outputs.map!(a => isLeaf ? buildPath(projectPath, a) : a).array;
227     }
228 
229     @property const(Command) command() @safe const pure nothrow { return _command; }
230 
231     Language getLanguage() @safe pure nothrow const {
232         return reggae.rules.common.getLanguage(inputs("")[0]);
233     }
234 
235     void execute(in string projectPath = "") @safe const {
236         _command.execute(projectPath, outputs, inputs(projectPath));
237     }
238 
239 
240 private:
241 
242     const(Command) _command;
243 
244     //@trusted because of join
245     string depFilesStringImpl(in Target[] deps, in string projectPath) @trusted pure const nothrow {
246         import std.conv;
247         string files;
248         //join doesn't do const, resort to loops
249         foreach(i, dep; deps) {
250             files ~= text(dep.outputsInProjectPath(projectPath).join(" "));
251             if(i != deps.length - 1) files ~= " ";
252         }
253         return files;
254     }
255 
256     string[] inputs(in string projectPath) @safe pure nothrow const {
257         //functional didn't work here, I don't know why so sticking with loops for now
258         string[] inputs;
259         foreach(dep; dependencies) {
260             foreach(output; dep.outputs) {
261                 //leaf objects are references to source files in the project path,
262                 //those need their path built. Any other dependencies are in the
263                 //build path, so they don't need the same treatment
264                 inputs ~= dep.isLeaf ? buildPath(projectPath, output) : output;
265             }
266         }
267         return inputs;
268     }
269 }
270 
271 
272 enum CommandType {
273     shell,
274     compile,
275     link,
276     code,
277 }
278 
279 alias CommandFunction = void function(in string[], in string[]);
280 
281 /**
282  A command to be execute to produce a targets outputs from its inputs.
283  In general this will be a shell command, but the high-level rules
284  use commands with known semantics (compilation, linking, etc)
285 */
286 struct Command {
287     alias Params = AssocList!(string, string[]);
288 
289     private string command;
290     private CommandType type;
291     private Params params;
292     private CommandFunction func;
293 
294     this(string shellCommand) @safe pure nothrow {
295         command = shellCommand;
296         type = CommandType.shell;
297     }
298 
299     this(CommandType type, Params params) @safe pure {
300         if(type == CommandType.shell) throw new Exception("Command rule cannot be shell");
301         this.type = type;
302         this.params = params;
303     }
304 
305     this(CommandFunction func) @safe pure nothrow {
306         type = CommandType.code;
307         this.func = func;
308     }
309 
310     const(string)[] paramNames() @safe pure nothrow const {
311         return params.keys;
312     }
313 
314     CommandType getType() @safe pure const {
315         return type;
316     }
317 
318     bool isDefaultCommand() @safe pure const {
319         return type != CommandType.shell;
320     }
321 
322     string[] getParams(in string projectPath, in string key, string[] ifNotFound) @safe pure const {
323         return getParams(projectPath, key, true, ifNotFound);
324     }
325 
326     const(Command) expandBuildDir() @safe pure const {
327         switch(type) with(CommandType) {
328         case shell:
329             auto cmd = Command(_expandBuildDir(command));
330             cmd.type = this.type;
331             return cmd;
332         default:
333             return this;
334         }
335     }
336 
337     ///Replace $in, $out, $project with values
338     string expand(in string projectPath, in string[] outputs, in string[] inputs) @safe pure nothrow const {
339         return expandCmd(command, projectPath, outputs, inputs);
340     }
341 
342     private static string expandCmd(in string cmd, in string projectPath,
343                                     in string[] outputs, in string[] inputs) @safe pure nothrow {
344         auto replaceIn = cmd.dup.replace("$in", inputs.join(" "));
345         auto replaceOut = replaceIn.replace("$out", outputs.join(" "));
346         return replaceOut.replace("$project", projectPath);
347     }
348 
349     //@trusted because of replace
350     string rawCmdString(in string projectPath) @trusted pure nothrow const {
351         return command.replace("$project", projectPath);
352     }
353 
354     //@trusted because of replace
355     private string[] getParams(in string projectPath, in string key,
356                                bool useIfNotFound, string[] ifNotFound = []) @safe pure const {
357         return params.get(key, ifNotFound).map!(a => a.replace("$project", projectPath)).array;
358     }
359 
360     static string builtinTemplate(CommandType type, Language language) @safe pure {
361         import reggae.config: dCompiler, cppCompiler, cCompiler;
362 
363         immutable ccParams = " $flags $includes -MMD -MT $out -MF $DEPFILE -o $out -c $in";
364 
365         final switch(type) with(CommandType) {
366             case shell:
367                 assert(0, "builtinTemplate cannot be shell");
368 
369             case link:
370                 return dCompiler ~ " -of$out $flags $in";
371 
372             case code:
373                 throw new Exception("Command type 'code' has no built-in template");
374 
375             case compile:
376                 final switch(language) with(Language) {
377                     case D:
378                         return ".reggae/dcompile --objFile=$out --depFile=$DEPFILE " ~
379                             dCompiler ~ " $flags $includes $stringImports $in";
380                     case Cplusplus:
381                         return cppCompiler ~ ccParams;
382                     case C:
383                         return cCompiler ~ ccParams;
384                     case unknown:
385                         throw new Exception("Unsupported language");
386                 }
387         }
388     }
389 
390     string defaultCommand(in string projectPath, in string[] outputs, in string[] inputs) @safe pure const {
391         assert(isDefaultCommand, text("This command is not a default command: ", this));
392         immutable language = getLanguage(inputs[0]);
393         auto cmd = builtinTemplate(type, language);
394         foreach(key; params.keys) {
395             immutable var = "$" ~ key;
396             immutable value = getParams(projectPath, key, []).join(" ");
397             cmd = cmd.replace(var, value);
398         }
399         return expandCmd(cmd, projectPath, outputs, inputs);
400     }
401 
402     ///returns a command string to be run by the shell
403     string shellCommand(in string projectPath, in string[] outputs, in string[] inputs) @safe pure const {
404         return isDefaultCommand
405             ? defaultCommand(projectPath, outputs, inputs)
406             : expand(projectPath, outputs, inputs);
407     }
408 
409 
410     void execute(in string projectPath, in string[] outputs, in string[] inputs) const @trusted {
411         import std.process;
412         import std.stdio;
413 
414         final switch(type) with(CommandType) {
415             case shell:
416             case compile:
417             case link:
418                 immutable cmd = shellCommand(projectPath, outputs, inputs);
419                 writeln("[build] " ~ cmd);
420                 immutable res = executeShell(cmd);
421                 enforce(res.status == 0, "Could not execute" ~ cmd ~ ":\n" ~ res.output);
422                 break;
423             case code:
424                 assert(func !is null, "Command of type code with null function");
425                 func(inputs, outputs);
426                 break;
427         }
428     }
429 
430 }