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