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