1 /** 2 JSON format support for PackageRecipe 3 4 Copyright: © 2012-2014 rejectedsoftware e.K. 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Sönke Ludwig, Matthias Dondorff 7 */ 8 module dub.recipe.json; 9 10 import dub.compilers.compiler; 11 import dub.dependency; 12 import dub.recipe.packagerecipe; 13 14 import dub.internal.vibecompat.data.json; 15 16 import std.algorithm : canFind, startsWith; 17 import std.conv : to; 18 import std.exception : enforce; 19 import std.range; 20 import std.string : format, indexOf; 21 import std.traits : EnumMembers; 22 23 deprecated("Use the overload that takes a `PackageName` as 3rd argument") 24 void parseJson(ref PackageRecipe recipe, Json json, string parent) 25 { 26 const PackageName pname = parent ? PackageName(parent) : PackageName.init; 27 parseJson(recipe, json, pname); 28 } 29 30 void parseJson(ref PackageRecipe recipe, Json json, in PackageName parent = PackageName.init) 31 { 32 foreach (string field, value; json) { 33 switch (field) { 34 default: break; 35 case "name": recipe.name = value.get!string; break; 36 case "version": recipe.version_ = value.get!string; break; 37 case "description": recipe.description = value.get!string; break; 38 case "homepage": recipe.homepage = value.get!string; break; 39 case "authors": recipe.authors = deserializeJson!(string[])(value); break; 40 case "copyright": recipe.copyright = value.get!string; break; 41 case "license": recipe.license = value.get!string; break; 42 case "configurations": break; // handled below, after the global settings have been parsed 43 case "buildTypes": 44 foreach (string name, settings; value) { 45 BuildSettingsTemplate bs; 46 bs.parseJson(settings, PackageName.init); 47 recipe.buildTypes[name] = bs; 48 } 49 break; 50 case "toolchainRequirements": 51 recipe.toolchainRequirements.parseJson(value); 52 break; 53 case "-ddoxFilterArgs": recipe.ddoxFilterArgs = deserializeJson!(string[])(value); break; 54 case "-ddoxTool": recipe.ddoxTool = value.get!string; break; 55 } 56 } 57 58 enforce(recipe.name.length > 0, "The package \"name\" field is missing or empty."); 59 60 const fullname = parent.toString().length 61 ? PackageName(parent.toString() ~ ":" ~ recipe.name) 62 : PackageName(recipe.name); 63 64 // parse build settings 65 recipe.buildSettings.parseJson(json, fullname); 66 67 if (auto pv = "configurations" in json) { 68 foreach (settings; *pv) { 69 ConfigurationInfo ci; 70 ci.parseJson(settings, fullname); 71 recipe.configurations ~= ci; 72 } 73 } 74 75 // parse any sub packages after the main package has been fully parsed 76 if (auto ps = "subPackages" in json) 77 recipe.parseSubPackages(fullname, ps.opt!(Json[])); 78 } 79 80 Json toJson(const scope ref PackageRecipe recipe) 81 { 82 auto ret = recipe.buildSettings.toJson(); 83 ret["name"] = recipe.name; 84 if (!recipe.version_.empty) ret["version"] = recipe.version_; 85 if (!recipe.description.empty) ret["description"] = recipe.description; 86 if (!recipe.homepage.empty) ret["homepage"] = recipe.homepage; 87 if (!recipe.authors.empty) ret["authors"] = serializeToJson(recipe.authors); 88 if (!recipe.copyright.empty) ret["copyright"] = recipe.copyright; 89 if (!recipe.license.empty) ret["license"] = recipe.license; 90 if (!recipe.subPackages.empty) { 91 Json[] jsonSubPackages = new Json[recipe.subPackages.length]; 92 foreach (i, subPackage; recipe.subPackages) { 93 if (subPackage.path !is null) { 94 jsonSubPackages[i] = Json(subPackage.path); 95 } else { 96 jsonSubPackages[i] = subPackage.recipe.toJson(); 97 } 98 } 99 ret["subPackages"] = jsonSubPackages; 100 } 101 if (recipe.configurations.length) { 102 Json[] configs; 103 foreach(config; recipe.configurations) 104 configs ~= config.toJson(); 105 ret["configurations"] = configs; 106 } 107 if (recipe.buildTypes.length) { 108 Json[string] types; 109 foreach (name, settings; recipe.buildTypes) 110 types[name] = settings.toJson(); 111 ret["buildTypes"] = types; 112 } 113 if (!recipe.toolchainRequirements.empty) { 114 ret["toolchainRequirements"] = recipe.toolchainRequirements.toJson(); 115 } 116 if (!recipe.ddoxFilterArgs.empty) ret["-ddoxFilterArgs"] = recipe.ddoxFilterArgs.serializeToJson(); 117 if (!recipe.ddoxTool.empty) ret["-ddoxTool"] = recipe.ddoxTool; 118 return ret; 119 } 120 121 private void parseSubPackages(ref PackageRecipe recipe, in PackageName parent, Json[] subPackagesJson) 122 { 123 enforce(!parent.sub, format("'subPackages' found in '%s'. This is only supported in the main package file for '%s'.", 124 parent, parent.main)); 125 126 recipe.subPackages = new SubPackage[subPackagesJson.length]; 127 foreach (i, subPackageJson; subPackagesJson) { 128 // Handle referenced Packages 129 if(subPackageJson.type == Json.Type..string) { 130 string subpath = subPackageJson.get!string; 131 recipe.subPackages[i] = SubPackage(subpath, PackageRecipe.init); 132 } else { 133 PackageRecipe subinfo; 134 subinfo.parseJson(subPackageJson, parent); 135 recipe.subPackages[i] = SubPackage(null, subinfo); 136 } 137 } 138 } 139 140 private void parseJson(ref ConfigurationInfo config, Json json, in PackageName pname) 141 { 142 foreach (string name, value; json) { 143 switch (name) { 144 default: break; 145 case "name": 146 config.name = value.get!string; 147 enforce(!config.name.empty, "Configurations must have a non-empty name."); 148 break; 149 case "platforms": config.platforms = deserializeJson!(string[])(value); break; 150 } 151 } 152 153 enforce(!config.name.empty, "Configuration is missing a name."); 154 config.buildSettings.parseJson(json, pname); 155 } 156 157 private Json toJson(const scope ref ConfigurationInfo config) 158 { 159 auto ret = config.buildSettings.toJson(); 160 ret["name"] = config.name; 161 if (config.platforms.length) ret["platforms"] = serializeToJson(config.platforms); 162 return ret; 163 } 164 165 private void parseJson(ref BuildSettingsTemplate bs, Json json, in PackageName pname) 166 { 167 foreach(string name, value; json) 168 { 169 auto idx = indexOf(name, "-"); 170 string basename, suffix; 171 if( idx >= 0 ) { basename = name[0 .. idx]; suffix = name[idx + 1 .. $]; } 172 else basename = name; 173 switch(basename){ 174 default: break; 175 case "dependencies": 176 foreach (string pkg, verspec; value) { 177 if (pkg.startsWith(":")) { 178 enforce(!pname.sub.length, 179 "Short-hand packages syntax not allowed within " ~ 180 "sub packages: %s -> %s".format(pname, pkg)); 181 pkg = pname.toString() ~ pkg; 182 } 183 enforce(pkg !in bs.dependencies, "The dependency '"~pkg~"' is specified more than once." ); 184 bs.dependencies[pkg] = Dependency.fromJson(verspec); 185 if (verspec.type == Json.Type.object) 186 bs.dependencies[pkg].settings.parseJson(verspec, pname); 187 } 188 break; 189 case "systemDependencies": 190 bs.systemDependencies = value.get!string; 191 break; 192 case "targetType": 193 enforce(suffix.empty, "targetType does not support platform customization."); 194 bs.targetType = value.get!string.to!TargetType; 195 break; 196 case "targetPath": 197 enforce(suffix.empty, "targetPath does not support platform customization."); 198 bs.targetPath = value.get!string; 199 break; 200 case "targetName": 201 enforce(suffix.empty, "targetName does not support platform customization."); 202 bs.targetName = value.get!string; 203 break; 204 case "workingDirectory": 205 enforce(suffix.empty, "workingDirectory does not support platform customization."); 206 bs.workingDirectory = value.get!string; 207 break; 208 case "mainSourceFile": 209 enforce(suffix.empty, "mainSourceFile does not support platform customization."); 210 bs.mainSourceFile = value.get!string; 211 break; 212 case "subConfigurations": 213 enforce(suffix.empty, "subConfigurations does not support platform customization."); 214 bs.subConfigurations = deserializeJson!(string[string])(value); 215 break; 216 case "dflags": bs.dflags[suffix] = deserializeJson!(string[])(value); break; 217 case "lflags": bs.lflags[suffix] = deserializeJson!(string[])(value); break; 218 case "libs": bs.libs[suffix] = deserializeJson!(string[])(value); break; 219 case "files": 220 case "sourceFiles": bs.sourceFiles[suffix] = deserializeJson!(string[])(value); break; 221 case "sourcePaths": bs.sourcePaths[suffix] = deserializeJson!(string[])(value); break; 222 case "cSourcePaths": bs.cSourcePaths[suffix] = deserializeJson!(string[])(value); break; 223 case "sourcePath": bs.sourcePaths[suffix] ~= [value.get!string]; break; // deprecated 224 case "excludedSourceFiles": bs.excludedSourceFiles[suffix] = deserializeJson!(string[])(value); break; 225 case "injectSourceFiles": bs.injectSourceFiles[suffix] = deserializeJson!(string[])(value); break; 226 case "copyFiles": bs.copyFiles[suffix] = deserializeJson!(string[])(value); break; 227 case "extraDependencyFiles": bs.extraDependencyFiles[suffix] = deserializeJson!(string[])(value); break; 228 case "versions": bs.versions[suffix] = deserializeJson!(string[])(value); break; 229 case "debugVersions": bs.debugVersions[suffix] = deserializeJson!(string[])(value); break; 230 case "-versionFilters": bs.versionFilters[suffix] = deserializeJson!(string[])(value); break; 231 case "-debugVersionFilters": bs.debugVersionFilters[suffix] = deserializeJson!(string[])(value); break; 232 case "importPaths": bs.importPaths[suffix] = deserializeJson!(string[])(value); break; 233 case "cImportPaths": bs.cImportPaths[suffix] = deserializeJson!(string[])(value); break; 234 case "stringImportPaths": bs.stringImportPaths[suffix] = deserializeJson!(string[])(value); break; 235 case "preGenerateCommands": bs.preGenerateCommands[suffix] = deserializeJson!(string[])(value); break; 236 case "postGenerateCommands": bs.postGenerateCommands[suffix] = deserializeJson!(string[])(value); break; 237 case "preBuildCommands": bs.preBuildCommands[suffix] = deserializeJson!(string[])(value); break; 238 case "postBuildCommands": bs.postBuildCommands[suffix] = deserializeJson!(string[])(value); break; 239 case "preRunCommands": bs.preRunCommands[suffix] = deserializeJson!(string[])(value); break; 240 case "postRunCommands": bs.postRunCommands[suffix] = deserializeJson!(string[])(value); break; 241 case "environments": bs.environments[suffix] = deserializeJson!(string[string])(value); break; 242 case "buildEnvironments": bs.buildEnvironments[suffix] = deserializeJson!(string[string])(value); break; 243 case "runEnvironments": bs.runEnvironments[suffix] = deserializeJson!(string[string])(value); break; 244 case "preGenerateEnvironments": bs.preGenerateEnvironments[suffix] = deserializeJson!(string[string])(value); break; 245 case "postGenerateEnvironments": bs.postGenerateEnvironments[suffix] = deserializeJson!(string[string])(value); break; 246 case "preBuildEnvironments": bs.preBuildEnvironments[suffix] = deserializeJson!(string[string])(value); break; 247 case "postBuildEnvironments": bs.postBuildEnvironments[suffix] = deserializeJson!(string[string])(value); break; 248 case "preRunEnvironments": bs.preRunEnvironments[suffix] = deserializeJson!(string[string])(value); break; 249 case "postRunEnvironments": bs.postRunEnvironments[suffix] = deserializeJson!(string[string])(value); break; 250 case "buildRequirements": 251 Flags!BuildRequirement reqs; 252 foreach (req; deserializeJson!(string[])(value)) 253 reqs |= to!BuildRequirement(req); 254 bs.buildRequirements[suffix] = reqs; 255 break; 256 case "buildOptions": 257 Flags!BuildOption options; 258 foreach (opt; deserializeJson!(string[])(value)) 259 options |= to!BuildOption(opt); 260 bs.buildOptions[suffix] = options; 261 break; 262 } 263 } 264 } 265 266 private Json toJson(const scope ref BuildSettingsTemplate bs) 267 { 268 static string withSuffix (string pre, string post) 269 { 270 if (!post.length) 271 return pre; 272 return pre ~ "-" ~ post; 273 } 274 275 auto ret = Json.emptyObject; 276 if( bs.dependencies !is null ){ 277 auto deps = Json.emptyObject; 278 foreach( pack, d; bs.dependencies ) 279 deps[pack] = d.toJson(); 280 ret["dependencies"] = deps; 281 } 282 if (bs.systemDependencies !is null) ret["systemDependencies"] = bs.systemDependencies; 283 if (bs.targetType != TargetType.autodetect) ret["targetType"] = bs.targetType.to!string(); 284 if (!bs.targetPath.empty) ret["targetPath"] = bs.targetPath; 285 if (!bs.targetName.empty) ret["targetName"] = bs.targetName; 286 if (!bs.workingDirectory.empty) ret["workingDirectory"] = bs.workingDirectory; 287 if (!bs.mainSourceFile.empty) ret["mainSourceFile"] = bs.mainSourceFile; 288 if (bs.subConfigurations.length > 0) ret["subConfigurations"] = serializeToJson(bs.subConfigurations); 289 foreach (suffix, arr; bs.dflags) ret[withSuffix("dflags", suffix)] = serializeToJson(arr); 290 foreach (suffix, arr; bs.lflags) ret[withSuffix("lflags", suffix)] = serializeToJson(arr); 291 foreach (suffix, arr; bs.libs) ret[withSuffix("libs", suffix)] = serializeToJson(arr); 292 foreach (suffix, arr; bs.sourceFiles) ret[withSuffix("sourceFiles", suffix)] = serializeToJson(arr); 293 foreach (suffix, arr; bs.sourcePaths) ret[withSuffix("sourcePaths", suffix)] = serializeToJson(arr); 294 foreach (suffix, arr; bs.cSourcePaths) ret[withSuffix("cSourcePaths", suffix)] = serializeToJson(arr); 295 foreach (suffix, arr; bs.excludedSourceFiles) ret[withSuffix("excludedSourceFiles", suffix)] = serializeToJson(arr); 296 foreach (suffix, arr; bs.injectSourceFiles) ret[withSuffix("injectSourceFiles", suffix)] = serializeToJson(arr); 297 foreach (suffix, arr; bs.copyFiles) ret[withSuffix("copyFiles", suffix)] = serializeToJson(arr); 298 foreach (suffix, arr; bs.extraDependencyFiles) ret[withSuffix("extraDependencyFiles", suffix)] = serializeToJson(arr); 299 foreach (suffix, arr; bs.versions) ret[withSuffix("versions", suffix)] = serializeToJson(arr); 300 foreach (suffix, arr; bs.debugVersions) ret[withSuffix("debugVersions", suffix)] = serializeToJson(arr); 301 foreach (suffix, arr; bs.versionFilters) ret[withSuffix("-versionFilters", suffix)] = serializeToJson(arr); 302 foreach (suffix, arr; bs.debugVersionFilters) ret[withSuffix("-debugVersionFilters", suffix)] = serializeToJson(arr); 303 foreach (suffix, arr; bs.importPaths) ret[withSuffix("importPaths", suffix)] = serializeToJson(arr); 304 foreach (suffix, arr; bs.cImportPaths) ret[withSuffix("cImportPaths", suffix)] = serializeToJson(arr); 305 foreach (suffix, arr; bs.stringImportPaths) ret[withSuffix("stringImportPaths", suffix)] = serializeToJson(arr); 306 foreach (suffix, arr; bs.preGenerateCommands) ret[withSuffix("preGenerateCommands", suffix)] = serializeToJson(arr); 307 foreach (suffix, arr; bs.postGenerateCommands) ret[withSuffix("postGenerateCommands", suffix)] = serializeToJson(arr); 308 foreach (suffix, arr; bs.preBuildCommands) ret[withSuffix("preBuildCommands", suffix)] = serializeToJson(arr); 309 foreach (suffix, arr; bs.postBuildCommands) ret[withSuffix("postBuildCommands", suffix)] = serializeToJson(arr); 310 foreach (suffix, arr; bs.preRunCommands) ret[withSuffix("preRunCommands", suffix)] = serializeToJson(arr); 311 foreach (suffix, arr; bs.postRunCommands) ret[withSuffix("postRunCommands", suffix)] = serializeToJson(arr); 312 foreach (suffix, aa; bs.environments) ret[withSuffix("environments", suffix)] = serializeToJson(aa); 313 foreach (suffix, aa; bs.buildEnvironments) ret[withSuffix("buildEnvironments", suffix)] = serializeToJson(aa); 314 foreach (suffix, aa; bs.runEnvironments) ret[withSuffix("runEnvironments", suffix)] = serializeToJson(aa); 315 foreach (suffix, aa; bs.preGenerateEnvironments) ret[withSuffix("preGenerateEnvironments", suffix)] = serializeToJson(aa); 316 foreach (suffix, aa; bs.postGenerateEnvironments) ret[withSuffix("postGenerateEnvironments", suffix)] = serializeToJson(aa); 317 foreach (suffix, aa; bs.preBuildEnvironments) ret[withSuffix("preBuildEnvironments", suffix)] = serializeToJson(aa); 318 foreach (suffix, aa; bs.postBuildEnvironments) ret[withSuffix("postBuildEnvironments", suffix)] = serializeToJson(aa); 319 foreach (suffix, aa; bs.preRunEnvironments) ret[withSuffix("preRunEnvironments", suffix)] = serializeToJson(aa); 320 foreach (suffix, aa; bs.postRunEnvironments) ret[withSuffix("postRunEnvironments", suffix)] = serializeToJson(aa); 321 foreach (suffix, arr; bs.buildRequirements) { 322 string[] val; 323 foreach (i; [EnumMembers!BuildRequirement]) 324 if (arr & i) val ~= to!string(i); 325 ret[withSuffix("buildRequirements", suffix)] = serializeToJson(val); 326 } 327 foreach (suffix, arr; bs.buildOptions) { 328 string[] val; 329 foreach (i; [EnumMembers!BuildOption]) 330 if (arr & i) val ~= to!string(i); 331 ret[withSuffix("buildOptions", suffix)] = serializeToJson(val); 332 } 333 return ret; 334 } 335 336 private void parseJson(ref ToolchainRequirements tr, Json json) 337 { 338 foreach (string name, value; json) 339 tr.addRequirement(name, value.get!string); 340 } 341 342 private Json toJson(const scope ref ToolchainRequirements tr) 343 { 344 auto ret = Json.emptyObject; 345 if (tr.dub != VersionRange.Any) ret["dub"] = serializeToJson(tr.dub); 346 if (tr.frontend != VersionRange.Any) ret["frontend"] = serializeToJson(tr.frontend); 347 if (tr.dmd != VersionRange.Any) ret["dmd"] = serializeToJson(tr.dmd); 348 if (tr.ldc != VersionRange.Any) ret["ldc"] = serializeToJson(tr.ldc); 349 if (tr.gdc != VersionRange.Any) ret["gdc"] = serializeToJson(tr.gdc); 350 return ret; 351 } 352 353 unittest { 354 import std.string: strip, outdent; 355 static immutable json = ` 356 { 357 "name": "projectname", 358 "environments": { 359 "Var1": "env" 360 }, 361 "buildEnvironments": { 362 "Var2": "buildEnv" 363 }, 364 "runEnvironments": { 365 "Var3": "runEnv" 366 }, 367 "preGenerateEnvironments": { 368 "Var4": "preGenEnv" 369 }, 370 "postGenerateEnvironments": { 371 "Var5": "postGenEnv" 372 }, 373 "preBuildEnvironments": { 374 "Var6": "preBuildEnv" 375 }, 376 "postBuildEnvironments": { 377 "Var7": "postBuildEnv" 378 }, 379 "preRunEnvironments": { 380 "Var8": "preRunEnv" 381 }, 382 "postRunEnvironments": { 383 "Var9": "postRunEnv" 384 } 385 } 386 `.strip.outdent; 387 auto jsonValue = parseJsonString(json); 388 PackageRecipe rec1; 389 parseJson(rec1, jsonValue); 390 PackageRecipe rec; 391 // verify that all fields are serialized properly 392 parseJson(rec, rec1.toJson()); 393 394 assert(rec.name == "projectname"); 395 assert(rec.buildSettings.environments == ["": ["Var1": "env"]]); 396 assert(rec.buildSettings.buildEnvironments == ["": ["Var2": "buildEnv"]]); 397 assert(rec.buildSettings.runEnvironments == ["": ["Var3": "runEnv"]]); 398 assert(rec.buildSettings.preGenerateEnvironments == ["": ["Var4": "preGenEnv"]]); 399 assert(rec.buildSettings.postGenerateEnvironments == ["": ["Var5": "postGenEnv"]]); 400 assert(rec.buildSettings.preBuildEnvironments == ["": ["Var6": "preBuildEnv"]]); 401 assert(rec.buildSettings.postBuildEnvironments == ["": ["Var7": "postBuildEnv"]]); 402 assert(rec.buildSettings.preRunEnvironments == ["": ["Var8": "preRunEnv"]]); 403 assert(rec.buildSettings.postRunEnvironments == ["": ["Var9": "postRunEnv"]]); 404 }