1 /**
2  This module is responsible for the output of a build system
3  from a JSON description
4  */
5 
6 module reggae.json_build;
7 
8 
9 import reggae.build;
10 import reggae.ctaa;
11 import reggae.rules.common;
12 import reggae.options;
13 
14 import std.json;
15 import std.algorithm;
16 import std.array;
17 import std.conv;
18 import std.traits;
19 
20 
21 enum JsonTargetType {
22     fixed,
23     dynamic,
24 }
25 
26 enum JsonCommandType {
27     shell,
28     link,
29 }
30 
31 
32 enum JsonDependencyType {
33     fixed,
34     dynamic,
35 }
36 
37 
38 enum JsonDepsFuncName {
39     objectFiles,
40     staticLibrary,
41     targetConcat,
42     executable,
43 }
44 
45 Build jsonToBuild(in string projectPath, in string jsonString) {
46     return tryJson(jsonString, jsonToBuildImpl(projectPath, jsonString));
47 }
48 
49 private auto tryJson(E)(in string jsonString, lazy E expr) {
50     import core.exception;
51     try {
52         return expr();
53     } catch(JSONException e) {
54         rethrow(jsonString, e);
55     } catch(RangeError e) {
56         import std.stdio;
57         stderr.writeln(e.toString);
58         rethrow(jsonString, e);
59     }
60 
61     assert(0);
62 }
63 
64 private void rethrow(E)(in string jsonString, E e) {
65     throw new Exception("Wrong JSON description for:\n" ~ jsonString ~ "\n" ~ e.msg, e, e.file, e.line);
66 }
67 
68 private struct Version0 {
69 
70     static Build jsonToBuild(in string projectPath, in JSONValue json) {
71         Build.TopLevelTarget maybeOptional(in JSONValue json, Target target) {
72             immutable optional = ("optional" in json.object) !is null;
73             return createTopLevelTarget(target, optional);
74         }
75 
76         auto targets = json.array.
77             filter!(a => a.object["type"].str != "defaultOptions").
78             map!(a => maybeOptional(a, jsonToTarget(projectPath, a))).
79             array;
80 
81         return Build(targets);
82     }
83 
84     static const(Options) jsonToOptions(in Options options, in JSONValue json) {
85         //first, find the JSON object we want
86         auto defaultOptionsRange = json.array.filter!(a => a.object["type"].str == "defaultOptions");
87         return defaultOptionsRange.empty
88             ? options
89             : jsonToOptionsImpl(options, defaultOptionsRange.front);
90     }
91 }
92 
93 private struct Version1 {
94 
95     static Build jsonToBuild(in string projectPath, in JSONValue json) {
96         return Version0.jsonToBuild(projectPath, json.object["build"]);
97     }
98 
99     static const(Options) jsonToOptions(in Options options, in JSONValue json) {
100         return jsonToOptionsImpl(options, json.object["defaultOptions"], json.object["dependencies"]);
101     }
102 }
103 
104 
105 private Build jsonToBuildImpl(in string projectPath, in string jsonString) {
106     import std.exception;
107 
108     auto json = parseJSON(jsonString);
109     immutable version_ = version_(json);
110 
111     enforce(version_ == 0 || version_ == 1, "Unknown JSON build version");
112 
113     return version_ == 1
114         ? Version1.jsonToBuild(projectPath, json)
115         : Version0.jsonToBuild(projectPath, json);
116 }
117 
118 private long version_(in JSONValue json) {
119     return json.type == JSONType.object
120         ? json.object["version"].integer
121         : 0;
122 }
123 
124 
125 private Target jsonToTarget(in string projectPath, JSONValue json) {
126     if(json.object["type"].str.to!JsonTargetType == JsonTargetType.dynamic)
127         return callTargetFunc(projectPath, json);
128 
129     auto dependencies = getDeps(projectPath, json.object["dependencies"]);
130     auto implicits = getDeps(projectPath, json.object["implicits"]);
131 
132     if(isLeaf(json)) {
133         return Target(json.object["outputs"].array.map!(a => a.str).array,
134                       "",
135                       []);
136     }
137 
138     return Target(json.object["outputs"].array.map!(a => a.str).array,
139                   jsonToCommand(json.object["command"]),
140                   dependencies,
141                   implicits);
142 }
143 
144 private bool isLeaf(in JSONValue json) pure {
145     return json.object["dependencies"].object["type"].str.to!JsonDependencyType == JsonDependencyType.fixed &&
146         json.object["dependencies"].object["targets"].array.empty &&
147         json.object["implicits"].object["type"].str.to!JsonDependencyType == JsonDependencyType.fixed &&
148         json.object["implicits"].object["targets"].array.empty;
149 }
150 
151 
152 private Command jsonToCommand(in JSONValue json) pure {
153     immutable type = json.object["type"].str.to!JsonCommandType;
154     final switch(type) with(JsonCommandType) {
155         case shell:
156             return Command(json.object["cmd"].str);
157         case link:
158             return Command(CommandType.link,
159                            assocList([assocEntry("flags", json.object["flags"].str.splitter.array)]));
160     }
161 }
162 
163 
164 private Target[] getDeps(in string projectPath, in JSONValue json) {
165     import core.exception;
166     immutable type = json.object["type"].str.to!JsonDependencyType;
167 
168     if(type == JsonDependencyType.fixed && "targets" in json.object && json.object["targets"].array.length == 0) {
169         return [];
170     }
171     try {
172         return type == JsonDependencyType.fixed
173             ? fixedDeps(projectPath, json)
174             : callDepsFunc(projectPath, json);
175     } catch(RangeError e) {
176         import std.stdio;
177         stderr.writeln(e.toString);
178         throw new JSONException("Could not get dependencies from JSON object" ~
179                                 json.to!string);
180     } catch(JSONException e) {
181         throw new JSONException(e.msg ~ ": object was " ~ json.to!string);
182     }
183 }
184 
185 private Target[] fixedDeps(in string projectPath, in JSONValue json) {
186     return "targets" in json.object
187            ? json.object["targets"].array.map!(a => jsonToTarget(projectPath, a)).array
188            :  [Target(json.object["outputs"].array.map!(a => a.str.dup.to!string),
189                       "",
190                       getDeps(projectPath, json.object["dependencies"]),
191                       getDeps(projectPath, json.object["implicits"]))];
192 
193 }
194 
195 private Target[] callDepsFunc(in string projectPath, in JSONValue json) {
196     immutable func = json.object["func"].str.to!JsonDepsFuncName;
197     final switch(func) {
198     case JsonDepsFuncName.objectFiles:
199         return objectFiles(projectPath,
200                            strings(json, "src_dirs"),
201                            strings(json, "exclude_dirs"),
202                            strings(json, "src_files"),
203                            strings(json, "exclude_files"),
204                            stringVal(json, "flags"),
205                            strings(json, "includes"),
206                            strings(json, "string_imports"));
207     case JsonDepsFuncName.staticLibrary:
208         return staticLibrary(projectPath,
209                              stringVal(json, "name"),
210                              strings(json, "src_dirs"),
211                              strings(json, "exclude_dirs"),
212                              strings(json, "src_files"),
213                              strings(json, "exclude_files"),
214                              stringVal(json, "flags"),
215                              strings(json, "includes"),
216                              strings(json, "string_imports"));
217     case JsonDepsFuncName.executable:
218         return [executable(projectPath,
219                           stringVal(json, "name"),
220                           strings(json, "src_dirs"),
221                           strings(json, "exclude_dirs"),
222                           strings(json, "src_files"),
223                           strings(json, "exclude_files"),
224                           stringVal(json, "compiler_flags"),
225                           stringVal(json, "linker_flags"),
226                           strings(json, "includes"),
227                           strings(json, "string_imports"))];
228     case JsonDepsFuncName.targetConcat:
229         return json.object["dependencies"].array.
230             map!(a => getDeps(projectPath, a)).join;
231     }
232 }
233 
234 private const(string)[] strings(in JSONValue json, in string key) {
235     return json.object[key].array.map!(a => a.str).array;
236 }
237 
238 private const(string) stringVal(in JSONValue json, in string key) {
239     return json.object[key].str;
240 }
241 
242 
243 private Target callTargetFunc(in string projectPath, in JSONValue json) {
244     import std.exception;
245     import reggae.rules.d;
246     import reggae.types;
247 
248     enforce(json.object["func"].str == "scriptlike",
249             "scriptlike is the only JSON function supported for Targets");
250 
251     auto srcFile = SourceFileName(stringVal(json, "src_name"));
252     auto app = json.object["exe_name"].isNull
253         ? App(srcFile)
254         : App(srcFile, BinaryFileName(stringVal(json, "exe_name")));
255 
256 
257     return scriptlike(projectPath, app,
258                       Flags(stringVal(json, "flags")),
259                       const ImportPaths(strings(json, "includes")),
260                       const StringImportPaths(strings(json, "string_imports")),
261                       getDeps(projectPath, json["link_with"]));
262 }
263 
264 
265 const(Options) jsonToOptions(in Options options, in string jsonString) {
266     return tryJson(jsonString, jsonToOptions(options, parseJSON(jsonString)));
267 }
268 
269 //get "real" options based on what was passed in via the command line
270 //and a json object.
271 //This is needed so that scripting language build descriptions can specify
272 //default values for the options
273 //First the command-line parses the options, then the json can override the defaults
274 const(Options) jsonToOptions(in Options options, in JSONValue json) {
275     return version_(json) == 1
276         ? Version1.jsonToOptions(options, json)
277         : Version0.jsonToOptions(options, json);
278 }
279 
280 
281 private const(Options) jsonToOptionsImpl(in Options options,
282                                          in JSONValue defaultOptionsObj,
283                                          in JSONValue dependencies = parseJSON(`[]`)) {
284     import std.exception;
285     import std.conv;
286 
287     assert(defaultOptionsObj.type == JSONType.object,
288            text("jsonToOptions requires an object, not ", defaultOptionsObj.type));
289 
290     Options defaultOptions;
291 
292     //statically loop over members of Options
293     foreach(member; __traits(allMembers, Options)) {
294 
295         static if(member[0] != '_') {
296 
297             //type alias for the current member
298             mixin(`alias T = typeof(defaultOptions.` ~ member ~ `);`);
299 
300             //don't bother with functions or with these member variables
301             static if(member != "args" && member != "userVars" && !isSomeFunction!T) {
302                 if(member in defaultOptionsObj) {
303                     static if(is(T == bool)) {
304                         mixin(`immutable type = defaultOptionsObj.object["` ~ member ~ `"].type;`);
305                         if(type == JSONType.true_)
306                             mixin("defaultOptions." ~ member ~ ` = true;`);
307                         else if(type == JSONType.false_)
308                             mixin("defaultOptions." ~ member ~ ` = false;`);
309                     }
310                     else
311                         mixin("defaultOptions." ~ member ~ ` = defaultOptionsObj.object["` ~ member ~ `"].str.to!T;`);
312                 }
313             }
314         }
315     }
316 
317     defaultOptions.dependencies = dependencies.array.map!(a => cast(string)a.str.dup).array;
318 
319     return getOptions(defaultOptions, options.args.dup);
320 }