1 module reggae.dub.json;
2 
3 import reggae.dub.info;
4 import reggae.build;
5 import std.json;
6 import std.algorithm: map, filter;
7 
8 
9 DubInfo getDubInfo(in string origString) @trusted {
10 
11     import std.string: indexOf;
12     import std.array;
13     import std.range: iota;
14     import core.exception: RangeError;
15 
16     string nextOpenCurly(string str) {
17         return str[str.indexOf("{") .. $];
18     }
19 
20     try {
21 
22         // the output might contain non-JSON at the beginning in stderr
23         auto jsonString = nextOpenCurly(origString);
24 
25         for(; ; jsonString = nextOpenCurly(jsonString[1..$])) {
26             auto json = parseJSON(jsonString);
27 
28             bool hasKey(in string key) {
29                 try
30                     return (key in json.object) !is null;
31                 catch(JSONException ex)
32                     return false;
33             }
34 
35             if(!hasKey("packages") || !hasKey("targets")) {
36                 continue;
37             }
38 
39             auto packages = json.byKey("packages").array;
40             auto targets = json.byKey("targets").array;
41 
42             // gets the package from `packages` corresponding to target `i`
43             auto packageForTarget(JSONValue target) {
44                 import std.algorithm: find;
45                 return packages.find!(a => a["name"] == target.object["rootPackage"]).front;
46             }
47 
48             // unfortunately there's a hybrid approach going on here.
49             // dub seems to put most of the important information in `targets`
50             // but unfortunately under that `sourceFiles` contains object files
51             // from every package.
52             // So we take our information from targets mostly, except for the
53             // source files
54             auto info = DubInfo(targets
55                                 .map!((target) {
56 
57                                     auto dubPackage = packageForTarget(target);
58                                     auto bs = target.object["buildSettings"];
59 
60                                     return DubPackage(
61                                         bs.byKey("targetName").str,
62                                         dubPackage.byKey("path").str,
63                                         bs.getOptional("mainSourceFile"),
64                                         bs.getOptional("targetName"),
65                                         bs.byKey("dflags").jsonValueToStrings,
66                                         bs.byKey("lflags").jsonValueToStrings,
67                                         bs.byKey("importPaths").jsonValueToStrings,
68                                         bs.byKey("stringImportPaths").jsonValueToStrings,
69                                         bs.byKey("sourceFiles").jsonValueToStrings,
70                                         bs.getOptionalEnum!TargetType("targetType"),
71                                         bs.getOptionalList("versions"),
72                                         target.getOptionalList("dependencies"),
73                                         bs.getOptionalList("libs"),
74                                         true, // backwards compatibility (active)
75                                         bs.getOptionalList("preBuildCommands"),
76                                         bs.getOptionalList("postBuildCommands"),
77                                     );
78                                 })
79                                 .filter!(a => a.active)
80                                 .array);
81             info = info.cleanObjectSourceFiles;
82 
83             // in dub.json/dub.sdl, $PACKAGE_DIR is a variable that refers to the root
84             // of the dub package
85             void resolvePackageDir(in DubPackage dubPackage, ref string str) {
86                 str = str.replace("$PACKAGE_DIR", dubPackage.path);
87             }
88 
89             foreach(ref dubPackage; info.packages) {
90                 foreach(ref member; dubPackage.tupleof) {
91 
92                     static if(is(typeof(member) == string)) {
93                         resolvePackageDir(dubPackage, member);
94                     } else static if(is(typeof(member) == string[])) {
95                         foreach(ref elt; member)
96                             resolvePackageDir(dubPackage, elt);
97                     }
98                 }
99             }
100 
101             return info;
102         }
103     } catch(RangeError e) {
104         import std.stdio;
105         stderr.writeln("Could not parse the output of dub describe:\n", origString);
106         throw e;
107     }
108 }
109 
110 
111 private string[] jsonValueToFiles(JSONValue files) @trusted {
112     import std.array;
113 
114     return files.array.
115         filter!(a => ("type" in a && a.byKey("type").str == "source") ||
116                      ("role" in a && a.byKey("role").str == "source") ||
117                      ("type" !in a && "role" !in a)).
118         map!(a => a.byKey("path").str).
119         array;
120 }
121 
122 private string[] jsonValueToStrings(JSONValue json) @trusted {
123     import std.array;
124     return json.array.map!(a => a.str).array;
125 }
126 
127 
128 private JSONValue byKey(JSONValue json, in string key) @trusted {
129     import core.exception: RangeError;
130     try
131         return json.object[key];
132     catch(RangeError e) {
133         throw new Exception("Could not find key " ~ key);
134     }
135 }
136 
137 private bool byOptionalKey(JSONValue json, in string key, bool def) {
138     import std.conv: to;
139     auto value = json.object;
140     return key in value ? value[key].boolean : def;
141 }
142 
143 //std.json has no conversion to bool
144 private bool boolean(JSONValue json) @trusted {
145     import std.exception: enforce;
146     enforce!JSONException(json.type == JSON_TYPE.TRUE || json.type == JSON_TYPE.FALSE,
147                           "JSONValue is not a boolean");
148     return json.type == JSON_TYPE.TRUE;
149 }
150 
151 private string getOptional(JSONValue json, in string key) @trusted {
152     auto aa = json.object;
153     return key in aa ? aa[key].str : "";
154 }
155 
156 private T getOptionalEnum(T)(JSONValue json, in string key) @trusted {
157     auto aa = json.object;
158     return key in aa ? cast(T)aa[key].integer : T.init;
159 }
160 
161 private string[] getOptionalList(JSONValue json, in string key) @trusted {
162     auto aa = json.object;
163     return key in aa ? aa[key].jsonValueToStrings : [];
164 }
165 
166 // dub describe, due to a bug, ends up including object files listed as sources
167 // in a dependent package in the `sourceFiles` part of `target.buildSettings`
168 // for the main package. We clean it up here.
169 DubInfo cleanObjectSourceFiles(in DubInfo info) {
170     import std.algorithm: find, uniq, joiner, canFind, remove;
171     import std.array: array;
172 
173     auto ret = info.dup;
174 
175     bool isObjectFile(in string fileName) {
176         import std.path: extension;
177         return fileName.extension == ".o";
178     }
179 
180     auto sourceObjectFiles = info.packages
181         .map!(a => a.files.dup)
182         .joiner
183         .filter!isObjectFile
184         .uniq;
185 
186     foreach(sourceObjectFile; sourceObjectFiles) {
187         const packagesWithFile = ret.packages.filter!(a => a.files.canFind(sourceObjectFile)).array;
188         foreach(ref dubPackage; ret.packages) {
189             // all but the last package get scrubbed
190             if(dubPackage.files.canFind(sourceObjectFile) && dubPackage != packagesWithFile[$-1]) {
191                 dubPackage.files = dubPackage.files.filter!(a => a != sourceObjectFile).array;
192             }
193         }
194     }
195 
196     return ret;
197 }