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