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 
9 import reggae.rules.defaults;
10 import std.string: replace;
11 import std.algorithm: map;
12 import std.path: buildPath;
13 import std.typetuple: allSatisfy;
14 import std.traits: Unqual, isSomeFunction, ReturnType, arity;
15 import std.array: array, join;
16 
17 
18 Target createTargetFromTarget(in Target target) {
19     return Target(target.outputs,
20                   target._command.removeBuilddir,
21                   target.dependencies.map!(a => a.enclose(target)).array,
22                   target.implicits.map!(a => a.enclose(target)).array);
23 }
24 
25 /**
26  Contains the top-level targets.
27  */
28 struct Build {
29     const(Target)[] targets;
30 
31     this(in Target[] targets) {
32         this.targets = targets.map!createTargetFromTarget.array;
33     }
34 
35     this(T...)(in T targets) {
36         foreach(t; targets) {
37             static if(isSomeFunction!(typeof(t))) {
38                 const target = t();
39             } else {
40                 const target = t;
41             }
42 
43             this.targets ~= createTargetFromTarget(target);
44         }
45     }
46 }
47 
48 //a directory for each top-level target no avoid name clashes
49 //@trusted because of map -> buildPath -> array
50 Target enclose(in Target target, in Target topLevel) @trusted {
51     if(target.isLeaf) return Target(target.outputs.map!(a => a.removeBuilddir).array,
52                                     target._command.removeBuilddir,
53                                     target.dependencies,
54                                     target.implicits);
55 
56     immutable dirName = buildPath("objs", topLevel.outputs[0] ~ ".objs");
57     return Target(target.outputs.map!(a => realTargetPath(dirName, a)).array,
58                   target._command.removeBuilddir,
59                   target.dependencies.map!(a => a.enclose(topLevel)).array,
60                   target.implicits.map!(a => a.enclose(topLevel)).array);
61 }
62 
63 immutable gBuilddir = "$builddir";
64 
65 
66 private string realTargetPath(in string dirName, in string output) @trusted pure {
67     import std.algorithm: canFind;
68 
69     return output.canFind(gBuilddir)
70         ? output.removeBuilddir
71         : buildPath(dirName, output);
72 }
73 
74 private string removeBuilddir(in string output) @trusted pure {
75     import std.path: buildNormalizedPath;
76     import std.algorithm;
77     return output.
78         splitter.
79         map!(a => a.canFind(gBuilddir) ? a.replace(gBuilddir, ".").buildNormalizedPath : a).
80         join(" ");
81 }
82 
83 enum isTarget(alias T) = is(Unqual!(typeof(T)) == Target) ||
84     isSomeFunction!T && is(ReturnType!T == Target);
85 
86 unittest {
87     auto  t1 = Target();
88     const t2 = Target();
89     static assert(isTarget!t1);
90     static assert(isTarget!t2);
91 }
92 
93 mixin template buildImpl(targets...) if(allSatisfy!(isTarget, targets)) {
94     Build buildFunc() {
95         return Build(targets);
96     }
97 }
98 
99 /**
100  Two variations on a template mixin. When reggae is used as a library,
101  this will essentially build reggae itself as part of the build description.
102 
103  When reggae is used as a command-line tool to generate builds, it simply
104  declares the build function that will be called at run-time. The tool
105  will then compile the user's reggaefile.d with the reggae libraries,
106  resulting in a buildgen executable.
107 
108  In either case, the compile-time parameters of $(D build) are the
109  build's top-level targets.
110  */
111 version(reggaelib) {
112     mixin template build(targets...) if(allSatisfy!(isTarget, targets)) {
113         mixin reggaeGen!(targets);
114     }
115 } else {
116     alias build = buildImpl;
117 }
118 
119 package template isBuildFunction(alias T) {
120     static if(!isSomeFunction!T) {
121         enum isBuildFunction = false;
122     } else {
123         enum isBuildFunction = is(ReturnType!T == Build) && arity!T == 0;
124     }
125 }
126 
127 unittest {
128     Build myBuildFunction() { return Build(); }
129     static assert(isBuildFunction!myBuildFunction);
130     float foo;
131     static assert(!isBuildFunction!foo);
132 }
133 
134 
135 /**
136  The core of reggae's D-based DSL for describing build systems.
137  Targets contain outputs, a command to generate those outputs,
138  explicit dependencies and implicit dependencies. All dependencies
139  are themselves $(D Target) structs.
140 
141  The command is given as a string. In this string, certain words
142  have special meaning: $(D $in), $(D $out), $(D $project) and $(D builddir).
143 
144  $(D $in) gets expanded to all explicit dependencies.
145  $(D $out) gets expanded to all outputs.
146  $(D $project) gets expanded to the project directory (i.e. the directory including
147  the source files to build that was given as a command-line argument). This can be
148  useful when build outputs are to be placed in the source directory, such as
149  automatically generated source files.
150  $(D $builddir) expands to the build directory (i.e. where reggae was run from).
151  */
152 struct Target {
153     const(string)[] outputs;
154     const(Target)[] dependencies;
155     const(Target)[] implicits;
156 
157     this(in string output) @safe pure nothrow {
158         this(output, null, null);
159     }
160 
161     this(in string output, string command, in Target dependency,
162          in Target[] implicits = []) @safe pure nothrow {
163         this([output], command, [dependency], implicits);
164     }
165 
166     this(in string output, string command,
167          in Target[] dependencies, in Target[] implicits = []) @safe pure nothrow {
168         this([output], command, dependencies, implicits);
169     }
170 
171     this(in string[] outputs, string command,
172          in Target[] dependencies, in Target[] implicits = []) @safe pure nothrow {
173         this.outputs = outputs;
174         this.dependencies = dependencies;
175         this.implicits = implicits;
176         this._command = command;
177     }
178 
179     @property string dependencyFilesString(in string projectPath = "") @safe pure const nothrow {
180         return depFilesStringImpl(dependencies, projectPath);
181     }
182 
183     @property string implicitFilesString(in string projectPath = "") @safe pure const nothrow {
184         return depFilesStringImpl(implicits, projectPath);
185     }
186 
187     @property string command(in string projectPath = "") @trusted pure const nothrow {
188         //functional didn't work here, I don't know why so sticking with loops for now
189         string[] depOutputs;
190         foreach(dep; dependencies) {
191             foreach(output; dep.outputs) {
192                 //leaf objects are references to source files in the project path
193                 //those need their path built. Any other dependencies are in the
194                 //build path, so they don't need the same treatment
195                 depOutputs ~= dep.isLeaf ? buildPath(projectPath, output) : output;
196             }
197         }
198         auto replaceIn = _command.replace("$in", depOutputs.join(" "));
199         auto replaceOut = replaceIn.replace("$out", outputs.join(" "));
200         return replaceOut.replace("$project", projectPath);
201     }
202 
203     bool isLeaf() @safe pure const nothrow {
204         return dependencies is null && implicits is null;
205     }
206 
207     //@trusted because of replace
208     string rawCmdString(in string projectPath) @trusted pure nothrow const {
209         return _command.replace("$project", projectPath);
210     }
211 
212 
213     string shellCommand(in string projectPath = "") @safe pure const {
214         immutable rawCmdLine = rawCmdString(projectPath);
215         if(rawCmdLine.isDefaultCommand) {
216             return defaultCommand(projectPath, rawCmdLine);
217         } else {
218             return command(projectPath);
219         }
220     }
221 
222     string[] outputsInProjectPath(in string projectPath) @safe pure nothrow const {
223         return outputs.map!(a => isLeaf ? buildPath(projectPath, a) : a).array;
224     }
225 
226 private:
227 
228     string _command;
229 
230     //@trusted because of join
231     string depFilesStringImpl(in Target[] deps, in string projectPath) @trusted pure const nothrow {
232         import std.conv;
233         string files;
234         //join doesn't do const, resort to loops
235         foreach(i, dep; deps) {
236             files ~= text(dep.outputsInProjectPath(projectPath).join(" "));
237             if(i != deps.length - 1) files ~= " ";
238         }
239         return files;
240     }
241 
242     //this function returns a string to be run by the shell with `std.process.execute`
243     //it does 'normal' commands, not built-in rules
244     string defaultCommand(in string projectPath, in string rawCmdLine) @safe pure const {
245         import reggae.config: dCompiler, cppCompiler, cCompiler;
246 
247         immutable flags = rawCmdLine.getDefaultRuleParams("flags", []).join(" ");
248         immutable includes = rawCmdLine.getDefaultRuleParams("includes", []).join(" ");
249         immutable depfile = outputs[0] ~ ".dep";
250 
251         string ccCommand(in string compiler) {
252             import std.stdio;
253             debug writeln("ccCommand with compiler ", compiler);
254             return [compiler, flags, includes, "-MMD", "-MT", outputs[0],
255                     "-MF", depfile, "-o", outputs[0], "-c",
256                     dependencyFilesString(projectPath)].join(" ");
257         }
258 
259 
260         immutable rule = rawCmdLine.getDefaultRule;
261         import std.stdio;
262         debug writeln("rule: ", rule);
263 
264         switch(rule) {
265 
266         case "_dcompile":
267             immutable stringImports = rawCmdLine.getDefaultRuleParams("stringImports", []).join(" ");
268             immutable command = [".reggae/dcompile",
269                                  "--objFile=" ~ outputs[0],
270                                  "--depFile=" ~ depfile, dCompiler,
271                                  flags, includes, stringImports,
272                                  dependencyFilesString(projectPath),
273                 ].join(" ");
274 
275             return command;
276 
277         case "_cppcompile": return ccCommand(cppCompiler);
278         case "_ccompile":   return ccCommand(cCompiler);
279         case "_link":
280             return [dCompiler, "-of" ~ outputs[0],
281                     flags,
282                     dependencyFilesString(projectPath)].join(" ");
283         default:
284             assert(0, "Unknown default rule " ~ rule);
285         }
286     }
287 }