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, dirSeparator;
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 import std.range;
21 import std.typecons;
22 
23 
24 /**
25  Contains the top-level targets.
26  */
27 struct Build {
28     static struct TopLevelTarget {
29         Target target;
30         bool optional;
31     }
32 
33     private const(TopLevelTarget)[] _targets;
34 
35     this(in Target[] targets) {
36         _targets = targets.map!createTopLevelTarget.array;
37     }
38 
39     this(T...)(in T targets) {
40         foreach(t; targets) {
41             //the constructor needs to go from Target to TopLevelTarget
42             //and accepts functions that return a parameter as well as parameters themselves
43             //if a function, call it, if not, take the value
44             //if the value is Target, call createTopLevelTarget, if not, take it as is
45             static if(isSomeFunction!(typeof(t)) && is(ReturnType!(typeof(t))) == Target) {
46                 _targets ~= createTopLevelTarget(t());
47             } else static if(is(Unqual!(typeof(t)) == TopLevelTarget)) {
48                 _targets ~= t;
49             } else {
50                 _targets ~= createTopLevelTarget(t);
51             }
52         }
53     }
54 
55     auto targets() @trusted pure nothrow const {
56         return _targets.map!(a => a.target);
57     }
58 
59     auto defaultTargets() @trusted pure nothrow const {
60         return _targets.filter!(a => !a.optional).map!(a => a.target);
61     }
62 
63     string defaultTargetsString(in string projectPath) @trusted pure const {
64         return defaultTargets.map!(a => a.outputsInProjectPath(projectPath).join(" ")).join(" ");
65     }
66 
67     auto range() @safe pure const {
68         import reggae.range;
69         return UniqueDepthFirst(this);
70     }
71 }
72 
73 
74 /**
75  Designate a target as optional so it won't be built by default.
76  "Compile-time" version that can be aliased
77  */
78 Build.TopLevelTarget optional(alias targetFunc)() {
79     auto target = targetFunc();
80     return createTopLevelTarget(target, true);
81 }
82 
83 /**
84  Designate a target as optional so it won't be built by default.
85  */
86 Build.TopLevelTarget optional(in Target target) {
87     return Build.TopLevelTarget(target, true);
88 }
89 
90 Build.TopLevelTarget createTopLevelTarget(in Target target, bool optional = false) {
91     return Build.TopLevelTarget(target.inTopLevelObjDirOf(topLevelDirName(target), Yes.topLevel),
92                                 optional);
93 }
94 
95 
96 immutable gBuilddir = "$builddir";
97 immutable gProjdir  = "$project";
98 
99 //a directory for each top-level target no avoid name clashes
100 //@trusted because of map -> buildPath -> array
101 Target inTopLevelObjDirOf(in Target target, string dirName, Flag!"topLevel" isTopLevel = No.topLevel) @trusted {
102     //leaf targets only get the $builddir expansion, nothing else
103     //this is because leaf targets are by definition in the project path
104 
105     //every other non-top-level target gets its outputs placed in a directory
106     //specific to its top-level parent
107 
108     if(target.outputs.any!(a => a.startsWith(gBuilddir) || a.startsWith(gProjdir))) {
109          dirName = topLevelDirName(target);
110     }
111 
112     const outputs = isTopLevel
113         ? target.outputs.map!(a => expandBuildDir(a)).array
114         : target.outputs.map!(a => realTargetPath(dirName, target, a)).array;
115 
116     return Target(outputs,
117                   target._command.expandVariables,
118                   target.dependencies.map!(a => a.inTopLevelObjDirOf(dirName)).array,
119                   target.implicits.map!(a => a.inTopLevelObjDirOf(dirName)).array);
120 }
121 
122 
123 string topLevelDirName(in Target target) @safe pure {
124     return buildPath("objs", target.outputs[0].expandBuildDir ~ ".objs");
125 }
126 
127 //targets that have outputs with $builddir or $project in them want to be placed
128 //in a specific place. Those don't get touched. Other targets get
129 //placed in their top-level parent's object directory
130 string realTargetPath(in string dirName, in Target target, in string output) @trusted pure {
131     return target.isLeaf
132         ? expandBuildDir(output)
133         : realTargetPath(dirName, output);
134 }
135 
136 
137 //targets that have outputs with $builddir or $project in them want to be placed
138 //in a specific place. Those don't get touched. Other targets get
139 //placed in their top-level parent's object directory
140 string realTargetPath(in string dirName, in string output) @trusted pure {
141     import std.algorithm: canFind;
142 
143     if(output.startsWith(gProjdir)) return output;
144 
145     return output.canFind(gBuilddir)
146         ? output.expandBuildDir
147         : buildPath(dirName, output);
148 }
149 
150 //replace $builddir with the current directory
151 string expandBuildDir(in string output) @trusted pure {
152     import std.path: buildNormalizedPath;
153     import std.algorithm;
154     return output.
155         splitter.
156         map!(a => a.canFind(gBuilddir) ? a.replace(gBuilddir, ".").buildNormalizedPath : a).
157         join(" ");
158 }
159 
160  enum isTarget(alias T) =
161      is(Unqual!(typeof(T)) == Target) ||
162      is(Unqual!(typeof(T)) == Build.TopLevelTarget) ||
163      isSomeFunction!T && is(ReturnType!T == Target) ||
164      isSomeFunction!T && is(ReturnType!T == Build.TopLevelTarget);
165 
166 unittest {
167     auto  t1 = Target();
168     const t2 = Target();
169     static assert(isTarget!t1);
170     static assert(isTarget!t2);
171     const t3 = Build.TopLevelTarget(Target());
172     static assert(isTarget!t3);
173 }
174 
175 mixin template buildImpl(targets...) if(allSatisfy!(isTarget, targets)) {
176     Build buildFunc() {
177         return Build(targets);
178     }
179 }
180 
181 /**
182  Two variations on a template mixin. When reggae is used as a library,
183  this will essentially build reggae itself as part of the build description.
184 
185  When reggae is used as a command-line tool to generate builds, it simply
186  declares the build function that will be called at run-time. The tool
187  will then compile the user's reggaefile.d with the reggae libraries,
188  resulting in a buildgen executable.
189 
190  In either case, the compile-time parameters of $(D build) are the
191  build's top-level targets.
192  */
193 version(reggaelib) {
194     mixin template build(targets...) if(allSatisfy!(isTarget, targets)) {
195         mixin reggaeGen!(targets);
196     }
197 } else {
198     alias build = buildImpl;
199 }
200 
201 package template isBuildFunction(alias T) {
202     static if(!isSomeFunction!T) {
203         enum isBuildFunction = false;
204     } else {
205         enum isBuildFunction = is(ReturnType!T == Build) && arity!T == 0;
206     }
207 }
208 
209 unittest {
210     Build myBuildFunction() { return Build(); }
211     static assert(isBuildFunction!myBuildFunction);
212     float foo;
213     static assert(!isBuildFunction!foo);
214 }
215 
216 
217 /**
218  The core of reggae's D-based DSL for describing build systems.
219  Targets contain outputs, a command to generate those outputs,
220  explicit dependencies and implicit dependencies. All dependencies
221  are themselves $(D Target) structs.
222 
223  The command is given as a string. In this string, certain words
224  have special meaning: $(D $in), $(D $out), $(D $project) and $(D builddir).
225 
226  $(D $in) gets expanded to all explicit dependencies.
227  $(D $out) gets expanded to all outputs.
228  $(D $project) gets expanded to the project directory (i.e. the directory including
229  the source files to build that was given as a command-line argument). This can be
230  useful when build outputs are to be placed in the source directory, such as
231  automatically generated source files.
232  $(D $builddir) expands to the build directory (i.e. where reggae was run from).
233  */
234 struct Target {
235     const(string)[] outputs;
236     private const(Command) _command; ///see $(D Command) struct
237     const(Target)[] dependencies;
238     const(Target)[] implicits;
239 
240     this(in string output) @safe pure nothrow {
241         this(output, "", null);
242     }
243 
244     this(C)(in string output,
245             in C command,
246             in Target dependency,
247             in Target[] implicits = []) @safe pure nothrow {
248         this([output], command, [dependency], implicits);
249     }
250 
251     this(C)(in string output,
252             in C command,
253             in Target[] dependencies,
254             in Target[] implicits = []) @safe pure nothrow {
255         this([output], command, dependencies, implicits);
256     }
257 
258     this(C)(in string[] outputs,
259             in C command,
260             in Target[] dependencies,
261             in Target[] implicits = []) @safe pure nothrow {
262 
263         this.outputs = outputs;
264         this.dependencies = dependencies;
265         this.implicits = implicits;
266 
267         static if(is(C == Command))
268             this._command = command;
269         else
270             this._command = Command(command);
271     }
272 
273     @property string dependencyFilesString(in string projectPath = "") @safe pure const {
274         return depFilesStringImpl(dependencies, projectPath);
275     }
276 
277     @property string implicitFilesString(in string projectPath = "") @safe pure const {
278         return depFilesStringImpl(implicits, projectPath);
279     }
280 
281     bool isLeaf() @safe pure const nothrow {
282         return dependencies is null && implicits is null;
283     }
284 
285     string[] outputsInProjectPath(in string projectPath) @safe pure const {
286         string inProjectPath(in string path) {
287             return path.startsWith(gProjdir)
288                 ? path
289                 : path.startsWith(gBuilddir)
290                     ? path.replace(gBuilddir ~ dirSeparator, "")
291                     : buildPath(projectPath, path);
292         }
293 
294         return outputs.map!(a => isLeaf ? inProjectPath(a) : a).
295             map!(a => a.replace("$project", projectPath)).array;
296     }
297 
298     Language getLanguage() @safe pure nothrow const {
299         import reggae.range: Leaves;
300         const leaves = () @trusted { return Leaves(this).array; }();
301         foreach(language; [Language.D, Language.Cplusplus, Language.C]) {
302             if(leaves.any!(a => reggae.rules.common.getLanguage(a.outputs[0]) == language)) return language;
303         }
304 
305         return Language.unknown;
306     }
307 
308     ///Replace special variables and return a list of outputs thus modified
309     auto expandOutputs(in string projectPath) @safe pure const {
310         return outputsInProjectPath(projectPath).map!(a => a.replace(gBuilddir ~ dirSeparator, ""));
311     }
312 
313     ///replace all special variables with their expansion
314     @property string expandCommand(in string projectPath = "") @trusted pure const {
315         return _command.expand(projectPath, outputs, inputs(projectPath));
316     }
317 
318     //@trusted because of replace
319     string rawCmdString(in string projectPath = "") @trusted pure const {
320         return _command.rawCmdString(projectPath);
321     }
322 
323     ///returns a command string to be run by the shell
324     string shellCommand(in string projectPath = "",
325                         Flag!"dependencies" deps = Yes.dependencies) @safe pure const {
326         return _command.shellCommand(projectPath, getLanguage(), outputs, inputs(projectPath), deps);
327     }
328 
329     string[] execute(in string projectPath = "") @safe const {
330         return _command.execute(projectPath, getLanguage(), outputs, inputs(projectPath));
331     }
332 
333     bool hasDefaultCommand() @safe const pure {
334         return _command.isDefaultCommand;
335     }
336 
337     CommandType getCommandType() @safe pure const nothrow {
338         return _command.getType;
339     }
340 
341     string[] getCommandParams(in string projectPath, in string key, string[] ifNotFound) @safe pure const {
342         return _command.getParams(projectPath, key, ifNotFound);
343     }
344 
345     const(string)[] commandParamNames() @safe pure nothrow const {
346         return _command.paramNames;
347     }
348 
349     static Target phony(in string output, in string shellCommand,
350                         in Target[] dependencies = [], in Target[] implicits = []) @safe pure {
351         return Target(output, Command.phony(shellCommand), dependencies, implicits);
352     }
353 
354     string toString() const pure nothrow {
355         try {
356             if(isLeaf) return outputs[0];
357             immutable outputs = outputs.length == 1 ? `"` ~ outputs[0] ~ `"` : text(outputs);
358             immutable depsStr = dependencies.length == 0 ? "" : text(dependencies);
359             immutable impsStr = implicits.length == 0 ? "" : text(implicits);
360             auto parts = [text(outputs), `"` ~ _command.command ~ `"`];
361             if(depsStr != "") parts ~= depsStr;
362             if(impsStr != "") parts ~= impsStr;
363             return text("Target(", parts.join(", "), ")");
364         } catch(Exception) {
365             assert(0);
366         }
367     }
368 
369 
370 private:
371 
372 
373     //@trusted because of join
374     string depFilesStringImpl(in Target[] deps, in string projectPath) @trusted pure const {
375         string files;
376         //join doesn't do const, resort to loops
377         foreach(i, dep; deps) {
378             files ~= text(dep.outputsInProjectPath(projectPath).join(" "));
379             if(i != deps.length - 1) files ~= " ";
380         }
381         return files;
382     }
383 
384     string[] inputs(in string projectPath) @safe pure nothrow const {
385         //functional didn't work here, I don't know why so sticking with loops for now
386         string[] inputs;
387         foreach(dep; dependencies) {
388             foreach(output; dep.outputs) {
389                 //leaf objects are references to source files in the project path,
390                 //those need their path built. Any other dependencies are in the
391                 //build path, so they don't need the same treatment
392                 inputs ~= dep.isLeaf ? inProjectPath(projectPath, output) : output;
393             }
394         }
395         return inputs;
396     }
397 }
398 
399 string inProjectPath(in string projectPath, in string name) @safe pure nothrow {
400     if(name.startsWith(gBuilddir)) return name;
401     return buildPath(projectPath, name);
402 }
403 
404 
405 enum CommandType {
406     shell,
407     compile,
408     link,
409     compileAndLink,
410     code,
411     phony,
412 }
413 
414 alias CommandFunction = void function(in string[], in string[]);
415 
416 /**
417  A command to be execute to produce a targets outputs from its inputs.
418  In general this will be a shell command, but the high-level rules
419  use commands with known semantics (compilation, linking, etc)
420 */
421 struct Command {
422     alias Params = AssocList!(string, string[]);
423 
424     private string command;
425     private CommandType type;
426     private Params params;
427     private CommandFunction func;
428 
429     ///If constructed with a string, it's a shell command
430     this(string shellCommand) @safe pure nothrow {
431         command = shellCommand;
432         type = CommandType.shell;
433     }
434 
435     /**Explicitly request a command of this type with these parameters
436        In general to create one of the builtin high level rules*/
437     this(CommandType type, Params params = Params()) @safe pure {
438         if(type == CommandType.shell) throw new Exception("Command rule cannot be shell");
439         this.type = type;
440         this.params = params;
441     }
442 
443     ///A D function call command
444     this(CommandFunction func) @safe pure nothrow {
445         type = CommandType.code;
446         this.func = func;
447     }
448 
449     static Command phony(in string shellCommand) @safe pure nothrow {
450         Command cmd;
451         cmd.type = CommandType.phony;
452         cmd.command = shellCommand;
453         return cmd;
454     }
455 
456     const(string)[] paramNames() @safe pure nothrow const {
457         return params.keys;
458     }
459 
460     CommandType getType() @safe pure const nothrow {
461         return type;
462     }
463 
464     bool isDefaultCommand() @safe pure const {
465         return type == CommandType.compile || type == CommandType.link || type == CommandType.compileAndLink;
466     }
467 
468     string[] getParams(in string projectPath, in string key, string[] ifNotFound) @safe pure const {
469         return getParams(projectPath, key, true, ifNotFound);
470     }
471 
472     const(Command) expandVariables() @safe pure const {
473         switch(type) with(CommandType) {
474         case shell:
475             auto cmd = Command(expandBuildDir(command));
476             cmd.type = this.type;
477             return cmd;
478         default:
479             return this;
480         }
481     }
482 
483     ///Replace $in, $out, $project with values
484     string expand(in string projectPath, in string[] outputs, in string[] inputs) @safe pure const {
485         return expandCmd(command, projectPath, outputs, inputs);
486     }
487 
488     private static string expandCmd(in string cmd, in string projectPath,
489                                     in string[] outputs, in string[] inputs) @safe pure {
490         auto replaceIn = cmd.dup.replace("$in", inputs.join(" "));
491         auto replaceOut = replaceIn.replace("$out", outputs.join(" "));
492         return replaceOut.replace("$project", projectPath).replace(gBuilddir ~ dirSeparator, "");
493     }
494 
495     //@trusted because of replace
496     string rawCmdString(in string projectPath) @trusted pure const {
497         if(getType != CommandType.shell)
498             throw new Exception("Command type 'code' not supported for ninja backend");
499         return command.replace("$project", projectPath);
500     }
501 
502     //@trusted because of replace
503     private string[] getParams(in string projectPath, in string key,
504                                bool useIfNotFound, string[] ifNotFound = []) @safe pure const {
505         return params.get(key, ifNotFound).map!(a => a.replace("$project", projectPath)).array;
506     }
507 
508     static string builtinTemplate(CommandType type,
509                                   Language language,
510                                   Flag!"dependencies" deps = Yes.dependencies) @safe pure {
511         import reggae.config: options;
512 
513         final switch(type) with(CommandType) {
514             case phony:
515                 assert(0, "builtinTemplate cannot be phony");
516 
517             case shell:
518                 assert(0, "builtinTemplate cannot be shell");
519 
520             case link:
521                 final switch(language) with(Language) {
522                     case D:
523                     case unknown:
524                         return options.dCompiler ~ " -of$out $flags $in";
525                     case Cplusplus:
526                         return options.cppCompiler ~ " -o $out $flags $in";
527                     case C:
528                         return options.cCompiler ~ " -o $out $flags $in";
529                 }
530 
531             case code:
532                 throw new Exception("Command type 'code' has no built-in template");
533 
534             case compile:
535                 return compileTemplate(type, language, deps).replace("$out $in", "$out -c $in");
536 
537             case compileAndLink:
538                 return compileTemplate(type, language, deps);
539         }
540     }
541 
542     private static string compileTemplate(CommandType type,
543                                           Language language,
544                                           Flag!"dependencies" deps = Yes.dependencies) @safe pure {
545         import reggae.config: options;
546 
547         immutable ccParams = deps
548             ? " $flags $includes -MMD -MT $out -MF $out.dep -o $out $in"
549             : " $flags $includes -o $out $in";
550 
551         final switch(language) with(Language) {
552             case D:
553                 return deps
554                     ? ".reggae/dcompile --objFile=$out --depFile=$out.dep " ~
555                     options.dCompiler ~ " $flags $includes $stringImports $in"
556                     : options.dCompiler ~ " $flags $includes $stringImports -of$out $in";
557             case Cplusplus:
558                 return options.cppCompiler ~ ccParams;
559             case C:
560                 return options.cCompiler ~ ccParams;
561             case unknown:
562                 throw new Exception("Unsupported language for compiling");
563         }
564     }
565 
566     string defaultCommand(in string projectPath,
567                           in Language language,
568                           in string[] outputs,
569                           in string[] inputs,
570                           Flag!"dependencies" deps = Yes.dependencies) @safe pure const {
571         assert(isDefaultCommand, text("This command is not a default command: ", this));
572         auto cmd = builtinTemplate(type, language, deps);
573         foreach(key; params.keys) {
574             immutable var = "$" ~ key;
575             immutable value = getParams(projectPath, key, []).join(" ");
576             cmd = cmd.replace(var, value);
577         }
578         return expandCmd(cmd, projectPath, outputs, inputs);
579     }
580 
581     ///returns a command string to be run by the shell
582     string shellCommand(in string projectPath,
583                         in Language language,
584                         in string[] outputs,
585                         in string[] inputs,
586                         Flag!"dependencies" deps = Yes.dependencies) @safe pure const {
587         return isDefaultCommand
588             ? defaultCommand(projectPath, language, outputs, inputs, deps)
589             : expand(projectPath, outputs, inputs);
590     }
591 
592 
593     string[] execute(in string projectPath, in Language language,
594                      in string[] outputs, in string[] inputs) const @trusted {
595         import std.process;
596 
597         final switch(type) with(CommandType) {
598             case shell:
599             case compile:
600             case link:
601             case compileAndLink:
602             case phony:
603                 immutable cmd = shellCommand(projectPath, language, outputs, inputs);
604                 immutable res = executeShell(cmd);
605                 enforce(res.status == 0, "Could not execute phony " ~ cmd ~ ":\n" ~ res.output);
606                 return [cmd, res.output];
607             case code:
608                 assert(func !is null, "Command of type code with null function");
609                 func(inputs, outputs);
610                 return [];
611         }
612     }
613 
614 }