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