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 }