1 /**
2 	Package recipe reading/writing facilities.
3 
4 	Copyright: © 2015-2016, Sönke Ludwig
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Sönke Ludwig
7 */
8 module dub.recipe.io;
9 
10 import dub.dependency : PackageName;
11 import dub.recipe.packagerecipe;
12 import dub.internal.logging;
13 import dub.internal.vibecompat.core.file;
14 import dub.internal.vibecompat.inet.path;
15 import dub.internal.configy.Read;
16 
17 /** Reads a package recipe from a file.
18 
19 	The file format (JSON/SDLang) will be determined from the file extension.
20 
21 	Params:
22 		filename = NativePath of the package recipe file
23 		parent = Optional name of the parent package (if this is a sub package)
24 		mode = Whether to issue errors, warning, or ignore unknown keys in dub.json
25 
26 	Returns: Returns the package recipe contents
27 	Throws: Throws an exception if an I/O or syntax error occurs
28 */
29 deprecated("Use the overload that accepts a `NativePath` as first argument")
30 PackageRecipe readPackageRecipe(
31 	string filename, string parent = null, StrictMode mode = StrictMode.Ignore)
32 {
33 	return readPackageRecipe(NativePath(filename), parent, mode);
34 }
35 
36 /// ditto
37 deprecated("Use the overload that accepts a `PackageName` as second argument")
38 PackageRecipe readPackageRecipe(
39 	NativePath filename, string parent, StrictMode mode = StrictMode.Ignore)
40 {
41 	return readPackageRecipe(filename, parent.length ? PackageName(parent) : PackageName.init, mode);
42 }
43 
44 
45 /// ditto
46 PackageRecipe readPackageRecipe(NativePath filename,
47 	in PackageName parent = PackageName.init, StrictMode mode = StrictMode.Ignore)
48 {
49 	string text = readText(filename);
50 	return parsePackageRecipe(text, filename.toNativeString(), parent, null, mode);
51 }
52 
53 /** Parses an in-memory package recipe.
54 
55 	The file format (JSON/SDLang) will be determined from the file extension.
56 
57 	Params:
58 		contents = The contents of the recipe file
59 		filename = Name associated with the package recipe - this is only used
60 			to determine the file format from the file extension
61 		parent = Optional name of the parent package (if this is a sub
62 		package)
63 		default_package_name = Optional default package name (if no package name
64 		is found in the recipe this value will be used)
65 		mode = Whether to issue errors, warning, or ignore unknown keys in dub.json
66 
67 	Returns: Returns the package recipe contents
68 	Throws: Throws an exception if an I/O or syntax error occurs
69 */
70 deprecated("Use the overload that accepts a `PackageName` as 3rd argument")
71 PackageRecipe parsePackageRecipe(string contents, string filename, string parent,
72 	string default_package_name = null, StrictMode mode = StrictMode.Ignore)
73 {
74     return parsePackageRecipe(contents, filename, parent.length ?
75         PackageName(parent) : PackageName.init,
76         default_package_name, mode);
77 }
78 
79 /// Ditto
80 PackageRecipe parsePackageRecipe(string contents, string filename,
81     in PackageName parent = PackageName.init,
82 	string default_package_name = null, StrictMode mode = StrictMode.Ignore)
83 {
84 	import std.algorithm : endsWith;
85 	import dub.compilers.buildsettings : TargetType;
86 	import dub.internal.vibecompat.data.json;
87 	import dub.recipe.json : parseJson;
88 	import dub.recipe.sdl : parseSDL;
89 
90 	PackageRecipe ret;
91 
92 	ret.name = default_package_name;
93 
94 	if (filename.endsWith(".json"))
95 	{
96 		try {
97 			ret = parseConfigString!PackageRecipe(contents, filename, mode);
98 			fixDependenciesNames(ret.name, ret);
99 		} catch (ConfigException exc) {
100 			logWarn("Your `dub.json` file use non-conventional features that are deprecated");
101 			logWarn("Please adjust your `dub.json` file as those warnings will turn into errors in dub v1.40.0");
102 			logWarn("Error was: %s", exc);
103 			// Fallback to JSON parser
104 			ret = PackageRecipe.init;
105 			parseJson(ret, parseJsonString(contents, filename), parent);
106 		} catch (Exception exc) {
107 			logWarn("Your `dub.json` file use non-conventional features that are deprecated");
108 			logWarn("This is most likely due to duplicated keys.");
109 			logWarn("Please adjust your `dub.json` file as those warnings will turn into errors in dub v1.40.0");
110 			logWarn("Error was: %s", exc);
111 			// Fallback to JSON parser
112 			ret = PackageRecipe.init;
113 			parseJson(ret, parseJsonString(contents, filename), parent);
114 		}
115 		// `debug = ConfigFillerDebug` also enables verbose parser output
116 		debug (ConfigFillerDebug)
117 		{
118 			import std.stdio;
119 
120 			PackageRecipe jsonret;
121 			parseJson(jsonret, parseJsonString(contents, filename), parent_name);
122 			if (ret != jsonret)
123 			{
124 				writeln("Content of JSON and YAML parsing differ for file: ", filename);
125 				writeln("-------------------------------------------------------------------");
126 				writeln("JSON (excepted): ", jsonret);
127 				writeln("-------------------------------------------------------------------");
128 				writeln("YAML (actual  ): ", ret);
129 				writeln("========================================");
130 				ret = jsonret;
131 			}
132 		}
133 	}
134 	else if (filename.endsWith(".sdl")) parseSDL(ret, contents, parent, filename);
135 	else assert(false, "readPackageRecipe called with filename with unknown extension: "~filename);
136 
137 	// Fix for issue #711: `targetType` should be inherited, or default to library
138 	static void sanitizeTargetType(ref PackageRecipe r) {
139 		TargetType defaultTT = (r.buildSettings.targetType == TargetType.autodetect) ?
140 			TargetType.library : r.buildSettings.targetType;
141 		foreach (ref conf; r.configurations)
142 			if (conf.buildSettings.targetType == TargetType.autodetect)
143 				conf.buildSettings.targetType = defaultTT;
144 
145 		// recurse into sub packages
146 		foreach (ref subPackage; r.subPackages)
147 			sanitizeTargetType(subPackage.recipe);
148 	}
149 
150 	sanitizeTargetType(ret);
151 
152 	return ret;
153 }
154 
155 
156 unittest { // issue #711 - configuration default target type not correct for SDL
157 	import dub.compilers.buildsettings : TargetType;
158 	auto inputs = [
159 		"dub.sdl": "name \"test\"\nconfiguration \"a\" {\n}",
160 		"dub.json": "{\"name\": \"test\", \"configurations\": [{\"name\": \"a\"}]}"
161 	];
162 	foreach (file, content; inputs) {
163 		auto pr = parsePackageRecipe(content, file);
164 		assert(pr.name == "test");
165 		assert(pr.configurations.length == 1);
166 		assert(pr.configurations[0].name == "a");
167 		assert(pr.configurations[0].buildSettings.targetType == TargetType.library);
168 	}
169 }
170 
171 unittest { // issue #711 - configuration default target type not correct for SDL
172 	import dub.compilers.buildsettings : TargetType;
173 	auto inputs = [
174 		"dub.sdl": "name \"test\"\ntargetType \"autodetect\"\nconfiguration \"a\" {\n}",
175 		"dub.json": "{\"name\": \"test\", \"targetType\": \"autodetect\", \"configurations\": [{\"name\": \"a\"}]}"
176 	];
177 	foreach (file, content; inputs) {
178 		auto pr = parsePackageRecipe(content, file);
179 		assert(pr.name == "test");
180 		assert(pr.configurations.length == 1);
181 		assert(pr.configurations[0].name == "a");
182 		assert(pr.configurations[0].buildSettings.targetType == TargetType.library);
183 	}
184 }
185 
186 unittest { // issue #711 - configuration default target type not correct for SDL
187 	import dub.compilers.buildsettings : TargetType;
188 	auto inputs = [
189 		"dub.sdl": "name \"test\"\ntargetType \"executable\"\nconfiguration \"a\" {\n}",
190 		"dub.json": "{\"name\": \"test\", \"targetType\": \"executable\", \"configurations\": [{\"name\": \"a\"}]}"
191 	];
192 	foreach (file, content; inputs) {
193 		auto pr = parsePackageRecipe(content, file);
194 		assert(pr.name == "test");
195 		assert(pr.configurations.length == 1);
196 		assert(pr.configurations[0].name == "a");
197 		assert(pr.configurations[0].buildSettings.targetType == TargetType.executable);
198 	}
199 }
200 
201 unittest { // make sure targetType of sub packages are sanitized too
202 	import dub.compilers.buildsettings : TargetType;
203 	auto inputs = [
204 		"dub.sdl": "name \"test\"\nsubPackage {\nname \"sub\"\ntargetType \"sourceLibrary\"\nconfiguration \"a\" {\n}\n}",
205 		"dub.json": "{\"name\": \"test\", \"subPackages\": [ { \"name\": \"sub\", \"targetType\": \"sourceLibrary\", \"configurations\": [{\"name\": \"a\"}] } ] }"
206 	];
207 	foreach (file, content; inputs) {
208 		auto pr = parsePackageRecipe(content, file);
209 		assert(pr.name == "test");
210 		const spr = pr.subPackages[0].recipe;
211 		assert(spr.name == "sub");
212 		assert(spr.configurations.length == 1);
213 		assert(spr.configurations[0].name == "a");
214 		assert(spr.configurations[0].buildSettings.targetType == TargetType.sourceLibrary);
215 	}
216 }
217 
218 
219 /** Writes the textual representation of a package recipe to a file.
220 
221 	Note that the file extension must be either "json" or "sdl".
222 */
223 void writePackageRecipe(string filename, const scope ref PackageRecipe recipe)
224 {
225 	writePackageRecipe(NativePath(filename), recipe);
226 }
227 
228 /// ditto
229 void writePackageRecipe(NativePath filename, const scope ref PackageRecipe recipe)
230 {
231 	import std.array;
232 	auto app = appender!string();
233 	serializePackageRecipe(app, recipe, filename.toNativeString());
234 	writeFile(filename, app.data);
235 }
236 
237 /** Converts a package recipe to its textual representation.
238 
239 	The extension of the supplied `filename` must be either "json" or "sdl".
240 	The output format is chosen accordingly.
241 */
242 void serializePackageRecipe(R)(ref R dst, const scope ref PackageRecipe recipe, string filename)
243 {
244 	import std.algorithm : endsWith;
245 	import dub.internal.vibecompat.data.json : writeJsonString;
246 	import dub.recipe.json : toJson;
247 	import dub.recipe.sdl : toSDL;
248 
249 	if (filename.endsWith(".json"))
250 		dst.writeJsonString!(R, true)(toJson(recipe));
251 	else if (filename.endsWith(".sdl"))
252 		toSDL(recipe).toSDLDocument(dst);
253 	else assert(false, "writePackageRecipe called with filename with unknown extension: "~filename);
254 }
255 
256 unittest {
257 	import std.format;
258 	import dub.dependency;
259 	import dub.internal.utils : deepCompare;
260 
261 	static void success (string source, in PackageRecipe expected, size_t line = __LINE__) {
262 		const result = parseConfigString!PackageRecipe(source, "dub.json");
263 		deepCompare(result, expected, __FILE__, line);
264 	}
265 
266 	static void error (string source, string expected, size_t line = __LINE__) {
267 		try
268 		{
269 			auto result = parseConfigString!PackageRecipe(source, "dub.json");
270 			assert(0,
271 				   format("[%s:%d] Exception should have been thrown but wasn't: %s",
272 						  __FILE__, line, result));
273 		}
274 		catch (Exception exc)
275 			assert(exc.toString() == expected,
276 				   format("[%s:%s] result != expected: '%s' != '%s'",
277 						  __FILE__, line, exc.toString(), expected));
278 	}
279 
280 	alias YAMLDep = typeof(BuildSettingsTemplate.dependencies[string.init]);
281 	const PackageRecipe expected1 =
282 	{
283 		name: "foo",
284 		buildSettings: {
285 		dependencies: RecipeDependencyAA([
286 			"repo": YAMLDep(Dependency(Repository(
287 				"git+https://github.com/dlang/dmd",
288 				"09d04945bdbc0cba36f7bb1e19d5bd009d4b0ff2",
289 			))),
290 			"path": YAMLDep(Dependency(NativePath("/foo/bar/jar/"))),
291 			"version": YAMLDep(Dependency(VersionRange.fromString("~>1.0"))),
292 			"version2": YAMLDep(Dependency(Version("4.2.0"))),
293 		])},
294 	};
295 	success(
296 		`{ "name": "foo", "dependencies": {
297 	"repo": { "repository": "git+https://github.com/dlang/dmd",
298 			  "version": "09d04945bdbc0cba36f7bb1e19d5bd009d4b0ff2" },
299 	"path":    { "path": "/foo/bar/jar/" },
300 	"version": { "version": "~>1.0" },
301 	"version2": "4.2.0"
302 }}`, expected1);
303 
304 
305 	error(`{ "name": "bar", "dependencies": {"bad": { "repository": "git+https://github.com/dlang/dmd" }}}`,
306 		"dub.json(0:41): dependencies[bad]: Need to provide a commit hash in 'version' field with 'repository' dependency");
307 }