1 module reggae.build;
2 
3 import std.string: replace;
4 import std.algorithm: map;
5 import std.path: buildPath;
6 import std.typetuple: allSatisfy;
7 import std.traits: Unqual, isSomeFunction, ReturnType, arity;
8 import std.array: array, join;
9 
10 
11 Target createTargetFromTarget(in Target target) {
12     return Target(target.outputs,
13                   target._command.removeBuilddir,
14                   target.dependencies.map!(a => a.enclose(target)).array,
15                   target.implicits.map!(a => a.enclose(target)).array);
16 }
17 
18 
19 struct Build {
20     const(Target)[] targets;
21 
22     this(in Target[] targets) {
23         this.targets = targets.map!createTargetFromTarget.array;
24     }
25 
26     this(T...)(in T targets) {
27         foreach(t; targets) {
28             static if(isSomeFunction!(typeof(t))) {
29                 const target = t();
30             } else {
31                 const target = t;
32             }
33 
34             this.targets ~= createTargetFromTarget(target);
35         }
36     }
37 }
38 
39 //a directory for each top-level target no avoid name clashes
40 //@trusted because of map -> buildPath -> array
41 Target enclose(in Target target, in Target topLevel) @trusted {
42     if(target.isLeaf) return Target(target.outputs.map!(a => a.removeBuilddir).array,
43                                     target._command.removeBuilddir,
44                                     target.dependencies,
45                                     target.implicits);
46 
47     immutable dirName = buildPath("objs", topLevel.outputs[0] ~ ".objs");
48     return Target(target.outputs.map!(a => realTargetPath(dirName, a)).array,
49                   target._command.removeBuilddir,
50                   target.dependencies.map!(a => a.enclose(topLevel)).array,
51                   target.implicits.map!(a => a.enclose(topLevel)).array);
52 }
53 
54 immutable gBuilddir = "$builddir";
55 
56 
57 private string realTargetPath(in string dirName, in string output) @trusted pure {
58     import std.algorithm: canFind;
59 
60     return output.canFind(gBuilddir)
61         ? output.removeBuilddir
62         : buildPath(dirName, output);
63 }
64 
65 private string removeBuilddir(in string output) @trusted pure {
66     import std.path: buildNormalizedPath;
67     import std.algorithm;
68     return output.
69         splitter.
70         map!(a => a.canFind(gBuilddir) ? a.replace(gBuilddir, ".").buildNormalizedPath : a).
71         join(" ");
72 }
73 
74 enum isTarget(alias T) = is(Unqual!(typeof(T)) == Target) ||
75     isSomeFunction!T && is(ReturnType!T == Target);
76 
77 unittest {
78     auto  t1 = Target();
79     const t2 = Target();
80     static assert(isTarget!t1);
81     static assert(isTarget!t2);
82 }
83 
84 mixin template build(T...) if(allSatisfy!(isTarget, T)) {
85     Build buildFunc() {
86         return Build(T);
87     }
88 }
89 
90 
91 package template isBuildFunction(alias T) {
92     static if(!isSomeFunction!T) {
93         enum isBuildFunction = false;
94     } else {
95         enum isBuildFunction = is(ReturnType!T == Build) && arity!T == 0;
96     }
97 }
98 
99 unittest {
100     Build myBuildFunction() { return Build(); }
101     static assert(isBuildFunction!myBuildFunction);
102     float foo;
103     static assert(!isBuildFunction!foo);
104 }
105 
106 
107 struct Target {
108     const(string)[] outputs;
109     const(Target)[] dependencies;
110     const(Target)[] implicits;
111 
112     this(in string output) @safe pure nothrow {
113         this(output, null, null);
114     }
115 
116     this(in string output, string command, in Target dependency,
117          in Target[] implicits = []) @safe pure nothrow {
118         this([output], command, [dependency], implicits);
119     }
120 
121     this(in string output, string command,
122          in Target[] dependencies, in Target[] implicits = []) @safe pure nothrow {
123         this([output], command, dependencies, implicits);
124     }
125 
126     this(in string[] outputs, string command,
127          in Target[] dependencies, in Target[] implicits = []) @safe pure nothrow {
128         this.outputs = outputs;
129         this.dependencies = dependencies;
130         this.implicits = implicits;
131         this._command = command;
132     }
133 
134     @property string dependencyFiles(in string projectPath = "") @safe const nothrow {
135         return depFilesImpl(dependencies, projectPath);
136     }
137 
138     @property string implicitFiles(in string projectPath = "") @safe const nothrow {
139         return depFilesImpl(implicits, projectPath);
140     }
141 
142     @property string command(in string projectPath = "") @trusted pure const nothrow {
143         //functional didn't work here, I don't know why so sticking with loops for now
144         string[] depOutputs;
145         foreach(dep; dependencies) {
146             foreach(output; dep.outputs) {
147                 depOutputs ~= dep.isLeaf ? buildPath(projectPath, output) : output;
148             }
149         }
150         auto replaceIn = _command.replace("$in", depOutputs.join(" "));
151         auto replaceOut = replaceIn.replace("$out", outputs.join(" "));
152         return replaceOut.replace("$project", projectPath);
153     }
154 
155     bool isLeaf() @safe pure const nothrow {
156         return dependencies is null && implicits is null;
157     }
158 
159     //@trusted because of replace
160     package string inOutCommand(in string projectPath = "") @trusted pure nothrow const {
161         return _command.replace("$project", projectPath);
162     }
163 
164 private:
165 
166     string _command;
167 
168     //@trusted because of join
169     string depFilesImpl(in Target[] deps, in string projectPath) @trusted const nothrow {
170         import std.conv;
171         string files;
172         //join doesn't do const, resort to loops
173         foreach(i, dep; deps) {
174             files ~= text(dep.outputs.map!(a => dep.isLeaf ? buildPath(projectPath, a) : a).join(" "));
175             if(i != deps.length - 1) files ~= " ";
176         }
177         return files;
178     }
179 }