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 "-ddoxFilterArgs": recipe.ddoxFilterArgs = deserializeJson!(string[])(value); break;
45 			case "-ddoxTool": recipe.ddoxTool = value.get!string; break;
46 		}
47 	}
48 
49 	enforce(recipe.name.length > 0, "The package \"name\" field is missing or empty.");
50 
51 	auto fullname = parent_name.length ? parent_name ~ ":" ~ recipe.name : recipe.name;
52 
53 	// parse build settings
54 	recipe.buildSettings.parseJson(json, fullname);
55 
56 	if (auto pv = "configurations" in json) {
57 		TargetType deftargettp = TargetType.library;
58 		if (recipe.buildSettings.targetType != TargetType.autodetect)
59 			deftargettp = recipe.buildSettings.targetType;
60 
61 		foreach (settings; *pv) {
62 			ConfigurationInfo ci;
63 			ci.parseJson(settings, recipe.name, deftargettp);
64 			recipe.configurations ~= ci;
65 		}
66 	}
67 
68 	// parse any sub packages after the main package has been fully parsed
69 	if (auto ps = "subPackages" in json)
70 		recipe.parseSubPackages(fullname, ps.opt!(Json[]));
71 }
72 
73 Json toJson(in ref PackageRecipe recipe)
74 {
75 	auto ret = recipe.buildSettings.toJson();
76 	ret["name"] = recipe.name;
77 	if (!recipe.version_.empty) ret["version"] = recipe.version_;
78 	if (!recipe.description.empty) ret["description"] = recipe.description;
79 	if (!recipe.homepage.empty) ret["homepage"] = recipe.homepage;
80 	if (!recipe.authors.empty) ret["authors"] = serializeToJson(recipe.authors);
81 	if (!recipe.copyright.empty) ret["copyright"] = recipe.copyright;
82 	if (!recipe.license.empty) ret["license"] = recipe.license;
83 	if (!recipe.subPackages.empty) {
84 		Json[] jsonSubPackages = new Json[recipe.subPackages.length];
85 		foreach (i, subPackage; recipe.subPackages) {
86 			if (subPackage.path !is null) {
87 				jsonSubPackages[i] = Json(subPackage.path);
88 			} else {
89 				jsonSubPackages[i] = subPackage.recipe.toJson();
90 			}
91 		}
92 		ret["subPackages"] = jsonSubPackages;
93 	}
94 	if (recipe.configurations.length) {
95 		Json[] configs;
96 		foreach(config; recipe.configurations)
97 			configs ~= config.toJson();
98 		ret["configurations"] = configs;
99 	}
100 	if (recipe.buildTypes.length) {
101 		Json[string] types;
102 		foreach (name, settings; recipe.buildTypes)
103 			types[name] = settings.toJson();
104 		ret["buildTypes"] = types;
105 	}
106 	if (!recipe.ddoxFilterArgs.empty) ret["-ddoxFilterArgs"] = recipe.ddoxFilterArgs.serializeToJson();
107 	if (!recipe.ddoxTool.empty) ret["-ddoxTool"] = recipe.ddoxTool;
108 	return ret;
109 }
110 
111 private void parseSubPackages(ref PackageRecipe recipe, string parent_package_name, Json[] subPackagesJson)
112 {
113 	enforce(!parent_package_name.canFind(":"), format("'subPackages' found in '%s'. This is only supported in the main package file for '%s'.",
114 		parent_package_name, getBasePackageName(parent_package_name)));
115 
116 	recipe.subPackages = new SubPackage[subPackagesJson.length];
117 	foreach (i, subPackageJson; subPackagesJson) {
118 		// Handle referenced Packages
119 		if(subPackageJson.type == Json.Type..string) {
120 			string subpath = subPackageJson.get!string;
121 			recipe.subPackages[i] = SubPackage(subpath, PackageRecipe.init);
122 		} else {
123 			PackageRecipe subinfo;
124 			subinfo.parseJson(subPackageJson, parent_package_name);
125 			recipe.subPackages[i] = SubPackage(null, subinfo);
126 		}
127 	}
128 }
129 
130 private void parseJson(ref ConfigurationInfo config, Json json, string package_name, TargetType default_target_type = TargetType.library)
131 {
132 	config.buildSettings.targetType = default_target_type;
133 
134 	foreach (string name, value; json) {
135 		switch (name) {
136 			default: break;
137 			case "name":
138 				config.name = value.get!string;
139 				enforce(!config.name.empty, "Configurations must have a non-empty name.");
140 				break;
141 			case "platforms": config.platforms = deserializeJson!(string[])(value); break;
142 		}
143 	}
144 
145 	enforce(!config.name.empty, "Configuration is missing a name.");
146 
147 	BuildSettingsTemplate bs;
148 	config.buildSettings.parseJson(json, package_name);
149 }
150 
151 private Json toJson(in ref ConfigurationInfo config)
152 {
153 	auto ret = config.buildSettings.toJson();
154 	ret["name"] = config.name;
155 	if (config.platforms.length) ret["platforms"] = serializeToJson(config.platforms);
156 	return ret;
157 }
158 
159 private void parseJson(ref BuildSettingsTemplate bs, Json json, string package_name)
160 {
161 	foreach(string name, value; json)
162 	{
163 		auto idx = indexOf(name, "-");
164 		string basename, suffix;
165 		if( idx >= 0 ) { basename = name[0 .. idx]; suffix = name[idx .. $]; }
166 		else basename = name;
167 		switch(basename){
168 			default: break;
169 			case "dependencies":
170 				foreach (string pkg, verspec; value) {
171 					if (pkg.startsWith(":")) {
172 						enforce(!package_name.canFind(':'), format("Short-hand packages syntax not allowed within sub packages: %s -> %s", package_name, pkg));
173 						pkg = package_name ~ pkg;
174 					}
175 					enforce(pkg !in bs.dependencies, "The dependency '"~pkg~"' is specified more than once." );
176 					bs.dependencies[pkg] = deserializeJson!Dependency(verspec);
177 				}
178 				break;
179 			case "systemDependencies":
180 				bs.systemDependencies = value.get!string;
181 				break;
182 			case "targetType":
183 				enforce(suffix.empty, "targetType does not support platform customization.");
184 				bs.targetType = value.get!string.to!TargetType;
185 				break;
186 			case "targetPath":
187 				enforce(suffix.empty, "targetPath does not support platform customization.");
188 				bs.targetPath = value.get!string;
189 				break;
190 			case "targetName":
191 				enforce(suffix.empty, "targetName does not support platform customization.");
192 				bs.targetName = value.get!string;
193 				break;
194 			case "workingDirectory":
195 				enforce(suffix.empty, "workingDirectory does not support platform customization.");
196 				bs.workingDirectory = value.get!string;
197 				break;
198 			case "mainSourceFile":
199 				enforce(suffix.empty, "mainSourceFile does not support platform customization.");
200 				bs.mainSourceFile = value.get!string;
201 				break;
202 			case "subConfigurations":
203 				enforce(suffix.empty, "subConfigurations does not support platform customization.");
204 				bs.subConfigurations = deserializeJson!(string[string])(value);
205 				break;
206 			case "dflags": bs.dflags[suffix] = deserializeJson!(string[])(value); break;
207 			case "lflags": bs.lflags[suffix] = deserializeJson!(string[])(value); break;
208 			case "libs": bs.libs[suffix] = deserializeJson!(string[])(value); break;
209 			case "files":
210 			case "sourceFiles": bs.sourceFiles[suffix] = deserializeJson!(string[])(value); break;
211 			case "sourcePaths": bs.sourcePaths[suffix] = deserializeJson!(string[])(value); break;
212 			case "sourcePath": bs.sourcePaths[suffix] ~= [value.get!string]; break; // deprecated
213 			case "excludedSourceFiles": bs.excludedSourceFiles[suffix] = deserializeJson!(string[])(value); break;
214 			case "copyFiles": bs.copyFiles[suffix] = deserializeJson!(string[])(value); break;
215 			case "versions": bs.versions[suffix] = deserializeJson!(string[])(value); break;
216 			case "debugVersions": bs.debugVersions[suffix] = deserializeJson!(string[])(value); break;
217 			case "importPaths": bs.importPaths[suffix] = deserializeJson!(string[])(value); break;
218 			case "stringImportPaths": bs.stringImportPaths[suffix] = deserializeJson!(string[])(value); break;
219 			case "preGenerateCommands": bs.preGenerateCommands[suffix] = deserializeJson!(string[])(value); break;
220 			case "postGenerateCommands": bs.postGenerateCommands[suffix] = deserializeJson!(string[])(value); break;
221 			case "preBuildCommands": bs.preBuildCommands[suffix] = deserializeJson!(string[])(value); break;
222 			case "postBuildCommands": bs.postBuildCommands[suffix] = deserializeJson!(string[])(value); break;
223 			case "buildRequirements":
224 				BuildRequirements reqs;
225 				foreach (req; deserializeJson!(string[])(value))
226 					reqs |= to!BuildRequirement(req);
227 				bs.buildRequirements[suffix] = reqs;
228 				break;
229 			case "buildOptions":
230 				BuildOptions options;
231 				foreach (opt; deserializeJson!(string[])(value))
232 					options |= to!BuildOption(opt);
233 				bs.buildOptions[suffix] = options;
234 				break;
235 		}
236 	}
237 }
238 
239 private Json toJson(in ref BuildSettingsTemplate bs)
240 {
241 	auto ret = Json.emptyObject;
242 	if( bs.dependencies !is null ){
243 		auto deps = Json.emptyObject;
244 		foreach( pack, d; bs.dependencies )
245 			deps[pack] = serializeToJson(d);
246 		ret["dependencies"] = deps;
247 	}
248 	if (bs.systemDependencies !is null) ret["systemDependencies"] = bs.systemDependencies;
249 	if (bs.targetType != TargetType.autodetect) ret["targetType"] = bs.targetType.to!string();
250 	if (!bs.targetPath.empty) ret["targetPath"] = bs.targetPath;
251 	if (!bs.targetName.empty) ret["targetName"] = bs.targetName;
252 	if (!bs.workingDirectory.empty) ret["workingDirectory"] = bs.workingDirectory;
253 	if (!bs.mainSourceFile.empty) ret["mainSourceFile"] = bs.mainSourceFile;
254 	if (bs.subConfigurations.length > 0) ret["subConfigurations"] = serializeToJson(bs.subConfigurations);
255 	foreach (suffix, arr; bs.dflags) ret["dflags"~suffix] = serializeToJson(arr);
256 	foreach (suffix, arr; bs.lflags) ret["lflags"~suffix] = serializeToJson(arr);
257 	foreach (suffix, arr; bs.libs) ret["libs"~suffix] = serializeToJson(arr);
258 	foreach (suffix, arr; bs.sourceFiles) ret["sourceFiles"~suffix] = serializeToJson(arr);
259 	foreach (suffix, arr; bs.sourcePaths) ret["sourcePaths"~suffix] = serializeToJson(arr);
260 	foreach (suffix, arr; bs.excludedSourceFiles) ret["excludedSourceFiles"~suffix] = serializeToJson(arr);
261 	foreach (suffix, arr; bs.copyFiles) ret["copyFiles"~suffix] = serializeToJson(arr);
262 	foreach (suffix, arr; bs.versions) ret["versions"~suffix] = serializeToJson(arr);
263 	foreach (suffix, arr; bs.debugVersions) ret["debugVersions"~suffix] = serializeToJson(arr);
264 	foreach (suffix, arr; bs.importPaths) ret["importPaths"~suffix] = serializeToJson(arr);
265 	foreach (suffix, arr; bs.stringImportPaths) ret["stringImportPaths"~suffix] = serializeToJson(arr);
266 	foreach (suffix, arr; bs.preGenerateCommands) ret["preGenerateCommands"~suffix] = serializeToJson(arr);
267 	foreach (suffix, arr; bs.postGenerateCommands) ret["postGenerateCommands"~suffix] = serializeToJson(arr);
268 	foreach (suffix, arr; bs.preBuildCommands) ret["preBuildCommands"~suffix] = serializeToJson(arr);
269 	foreach (suffix, arr; bs.postBuildCommands) ret["postBuildCommands"~suffix] = serializeToJson(arr);
270 	foreach (suffix, arr; bs.buildRequirements) {
271 		string[] val;
272 		foreach (i; [EnumMembers!BuildRequirement])
273 			if (arr & i) val ~= to!string(i);
274 		ret["buildRequirements"~suffix] = serializeToJson(val);
275 	}
276 	foreach (suffix, arr; bs.buildOptions) {
277 		string[] val;
278 		foreach (i; [EnumMembers!BuildOption])
279 			if (arr & i) val ~= to!string(i);
280 		ret["buildOptions"~suffix] = serializeToJson(val);
281 	}
282 	return ret;
283 }