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 }