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 }