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, getLanguage(), 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).
230             map!(a => a.replace("$project", projectPath)).array;
231     }
232 
233     @property const(Command) command() @safe const pure nothrow { return _command; }
234 
235     Language getLanguage() @safe pure nothrow const {
236         import reggae.range: Leaves;
237         const leaves = () @trusted { return Leaves(this).array; }();
238         foreach(language; [Language.D, Language.Cplusplus, Language.C]) {
239             if(leaves.any!(a => reggae.rules.common.getLanguage(a.outputs[0]) == language)) return language;
240         }
241 
242         return Language.unknown;
243     }
244 
245     void execute(in string projectPath = "") @safe const {
246         _command.execute(projectPath, getLanguage(), outputs, inputs(projectPath));
247     }
248 
249 
250 private:
251 
252     //@trusted because of join
253     string depFilesStringImpl(in Target[] deps, in string projectPath) @trusted pure const nothrow {
254         import std.conv;
255         string files;
256         //join doesn't do const, resort to loops
257         foreach(i, dep; deps) {
258             files ~= text(dep.outputsInProjectPath(projectPath).join(" "));
259             if(i != deps.length - 1) files ~= " ";
260         }
261         return files;
262     }
263 
264     string[] inputs(in string projectPath) @safe pure nothrow const {
265         //functional didn't work here, I don't know why so sticking with loops for now
266         string[] inputs;
267         foreach(dep; dependencies) {
268             foreach(output; dep.outputs) {
269                 //leaf objects are references to source files in the project path,
270                 //those need their path built. Any other dependencies are in the
271                 //build path, so they don't need the same treatment
272                 inputs ~= dep.isLeaf ? buildPath(projectPath, output) : output;
273             }
274         }
275         return inputs;
276     }
277 }
278 
279 
280 enum CommandType {
281     shell,
282     compile,
283     link,
284     code,
285 }
286 
287 alias CommandFunction = void function(in string[], in string[]);
288 
289 /**
290  A command to be execute to produce a targets outputs from its inputs.
291  In general this will be a shell command, but the high-level rules
292  use commands with known semantics (compilation, linking, etc)
293 */
294 struct Command {
295     alias Params = AssocList!(string, string[]);
296 
297     private string command;
298     private CommandType type;
299     private Params params;
300     private CommandFunction func;
301 
302     ///If constructed with a string, it's a shell command
303     this(string shellCommand) @safe pure nothrow {
304         command = shellCommand;
305         type = CommandType.shell;
306     }
307 
308     /**Explicitly request a command of this type with these parameters
309        In general to create one of the builtin high level rules*/
310     this(CommandType type, Params params) @safe pure {
311         if(type == CommandType.shell) throw new Exception("Command rule cannot be shell");
312         this.type = type;
313         this.params = params;
314     }
315 
316     ///A D function call command
317     this(CommandFunction func) @safe pure nothrow {
318         type = CommandType.code;
319         this.func = func;
320     }
321 
322     const(string)[] paramNames() @safe pure nothrow const {
323         return params.keys;
324     }
325 
326     CommandType getType() @safe pure const {
327         return type;
328     }
329 
330     bool isDefaultCommand() @safe pure const {
331         return type != CommandType.shell;
332     }
333 
334     string[] getParams(in string projectPath, in string key, string[] ifNotFound) @safe pure const {
335         return getParams(projectPath, key, true, ifNotFound);
336     }
337 
338     const(Command) expandBuildDir() @safe pure const {
339         switch(type) with(CommandType) {
340         case shell:
341             auto cmd = Command(_expandBuildDir(command));
342             cmd.type = this.type;
343             return cmd;
344         default:
345             return this;
346         }
347     }
348 
349     ///Replace $in, $out, $project with values
350     string expand(in string projectPath, in string[] outputs, in string[] inputs) @safe pure nothrow const {
351         return expandCmd(command, projectPath, outputs, inputs);
352     }
353 
354     private static string expandCmd(in string cmd, in string projectPath,
355                                     in string[] outputs, in string[] inputs) @safe pure nothrow {
356         auto replaceIn = cmd.dup.replace("$in", inputs.join(" "));
357         auto replaceOut = replaceIn.replace("$out", outputs.join(" "));
358         return replaceOut.replace("$project", projectPath);
359     }
360 
361     //@trusted because of replace
362     string rawCmdString(in string projectPath) @trusted pure nothrow const {
363         return command.replace("$project", projectPath);
364     }
365 
366     //@trusted because of replace
367     private string[] getParams(in string projectPath, in string key,
368                                bool useIfNotFound, string[] ifNotFound = []) @safe pure const {
369         return params.get(key, ifNotFound).map!(a => a.replace("$project", projectPath)).array;
370     }
371 
372     static string builtinTemplate(CommandType type,
373                                   Language language,
374                                   Flag!"dependencies" deps = Yes.dependencies) @safe pure {
375         import reggae.config: dCompiler, cppCompiler, cCompiler;
376 
377         final switch(type) with(CommandType) {
378             case shell:
379                 assert(0, "builtinTemplate cannot be shell");
380 
381             case link:
382                 final switch(language) with(Language) {
383                         import std.stdio;
384                         debug writeln("builtinTemplate called with language ", language);
385                     case D:
386                     case unknown:
387                         return dCompiler ~ " -of$out $flags $in";
388                     case Cplusplus:
389                         return cppCompiler ~ " -o $out $flags $in";
390                     case C:
391                         return cCompiler ~ " -o $out $flags $in";
392                 }
393 
394             case code:
395                 throw new Exception("Command type 'code' has no built-in template");
396 
397             case compile:
398                 immutable ccParams = deps
399                     ? " $flags $includes -MMD -MT $out -MF $DEPFILE -o $out -c $in"
400                     : " $flags $includes -o $out -c $in";
401 
402                 final switch(language) with(Language) {
403                     case D:
404                         return deps
405                             ? ".reggae/dcompile --objFile=$out --depFile=$DEPFILE " ~
406                             dCompiler ~ " $flags $includes $stringImports $in"
407                             : dCompiler ~ " $flags $includes $stringImports -of$out -c $in";
408                     case Cplusplus:
409                         return cppCompiler ~ ccParams;
410                     case C:
411                         return cCompiler ~ ccParams;
412                     case unknown:
413                         throw new Exception("Unsupported language for compiling");
414                 }
415         }
416     }
417 
418     string defaultCommand(in string projectPath,
419                           in Language language,
420                           in string[] outputs,
421                           in string[] inputs,
422                           Flag!"dependencies" deps = Yes.dependencies) @safe pure const {
423         assert(isDefaultCommand, text("This command is not a default command: ", this));
424         auto cmd = builtinTemplate(type, language, deps);
425         foreach(key; params.keys) {
426             immutable var = "$" ~ key;
427             immutable value = getParams(projectPath, key, []).join(" ");
428             cmd = cmd.replace(var, value);
429         }
430         return expandCmd(cmd, projectPath, outputs, inputs);
431     }
432 
433     ///returns a command string to be run by the shell
434     string shellCommand(in string projectPath,
435                         in Language language,
436                         in string[] outputs,
437                         in string[] inputs,
438                         Flag!"dependencies" deps = Yes.dependencies) @safe pure const {
439         return isDefaultCommand
440             ? defaultCommand(projectPath, language, outputs, inputs, deps)
441             : expand(projectPath, outputs, inputs);
442     }
443 
444 
445     void execute(in string projectPath, in Language language,
446                  in string[] outputs, in string[] inputs) const @trusted {
447         import std.process;
448         import std.stdio;
449 
450         final switch(type) with(CommandType) {
451             case shell:
452             case compile:
453             case link:
454                 immutable cmd = shellCommand(projectPath, language, outputs, inputs);
455                 writeln("[build] " ~ cmd);
456                 immutable res = executeShell(cmd);
457                 enforce(res.status == 0, "Could not execute" ~ cmd ~ ":\n" ~ res.output);
458                 break;
459             case code:
460                 assert(func !is null, "Command of type code with null function");
461                 func(inputs, outputs);
462                 break;
463         }
464     }
465 
466 }