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