Newer
Older
dub_jkp / source / dub / recipe / io.d
/**
	Package recipe reading/writing facilities.

	Copyright: © 2015-2016, Sönke Ludwig
	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
	Authors: Sönke Ludwig
*/
module dub.recipe.io;

import dub.dependency : PackageName;
import dub.recipe.packagerecipe;
import dub.internal.logging;
import dub.internal.vibecompat.core.file;
import dub.internal.vibecompat.inet.path;
import dub.internal.configy.Read;

/** Reads a package recipe from a file.

	The file format (JSON/SDLang) will be determined from the file extension.

	Params:
		filename = NativePath of the package recipe file
		parent = Optional name of the parent package (if this is a sub package)
		mode = Whether to issue errors, warning, or ignore unknown keys in dub.json

	Returns: Returns the package recipe contents
	Throws: Throws an exception if an I/O or syntax error occurs
*/
deprecated("Use the overload that accepts a `NativePath` as first argument")
PackageRecipe readPackageRecipe(
	string filename, string parent = null, StrictMode mode = StrictMode.Ignore)
{
	return readPackageRecipe(NativePath(filename), parent, mode);
}

/// ditto
deprecated("Use the overload that accepts a `PackageName` as second argument")
PackageRecipe readPackageRecipe(
	NativePath filename, string parent, StrictMode mode = StrictMode.Ignore)
{
	return readPackageRecipe(filename, parent.length ? PackageName(parent) : PackageName.init, mode);
}


/// ditto
PackageRecipe readPackageRecipe(NativePath filename,
	in PackageName parent = PackageName.init, StrictMode mode = StrictMode.Ignore)
{
	string text = readText(filename);
	return parsePackageRecipe(text, filename.toNativeString(), parent, null, mode);
}

/** Parses an in-memory package recipe.

	The file format (JSON/SDLang) will be determined from the file extension.

	Params:
		contents = The contents of the recipe file
		filename = Name associated with the package recipe - this is only used
			to determine the file format from the file extension
		parent = Optional name of the parent package (if this is a sub
		package)
		default_package_name = Optional default package name (if no package name
		is found in the recipe this value will be used)
		mode = Whether to issue errors, warning, or ignore unknown keys in dub.json

	Returns: Returns the package recipe contents
	Throws: Throws an exception if an I/O or syntax error occurs
*/
deprecated("Use the overload that accepts a `PackageName` as 3rd argument")
PackageRecipe parsePackageRecipe(string contents, string filename, string parent,
	string default_package_name = null, StrictMode mode = StrictMode.Ignore)
{
    return parsePackageRecipe(contents, filename, parent.length ?
        PackageName(parent) : PackageName.init,
        default_package_name, mode);
}

/// Ditto
PackageRecipe parsePackageRecipe(string contents, string filename,
    in PackageName parent = PackageName.init,
	string default_package_name = null, StrictMode mode = StrictMode.Ignore)
{
	import std.algorithm : endsWith;
	import dub.compilers.buildsettings : TargetType;
	import dub.internal.vibecompat.data.json;
	import dub.recipe.json : parseJson;
	import dub.recipe.sdl : parseSDL;

	PackageRecipe ret;

	ret.name = default_package_name;

	if (filename.endsWith(".json"))
	{
		try {
			ret = parseConfigString!PackageRecipe(contents, filename, mode);
			fixDependenciesNames(ret.name, ret);
		} catch (ConfigException exc) {
			logWarn("Your `dub.json` file use non-conventional features that are deprecated");
			logWarn("Please adjust your `dub.json` file as those warnings will turn into errors in dub v1.40.0");
			logWarn("Error was: %s", exc);
			// Fallback to JSON parser
			ret = PackageRecipe.init;
			parseJson(ret, parseJsonString(contents, filename), parent);
		} catch (Exception exc) {
			logWarn("Your `dub.json` file use non-conventional features that are deprecated");
			logWarn("This is most likely due to duplicated keys.");
			logWarn("Please adjust your `dub.json` file as those warnings will turn into errors in dub v1.40.0");
			logWarn("Error was: %s", exc);
			// Fallback to JSON parser
			ret = PackageRecipe.init;
			parseJson(ret, parseJsonString(contents, filename), parent);
		}
		// `debug = ConfigFillerDebug` also enables verbose parser output
		debug (ConfigFillerDebug)
		{
			import std.stdio;

			PackageRecipe jsonret;
			parseJson(jsonret, parseJsonString(contents, filename), parent_name);
			if (ret != jsonret)
			{
				writeln("Content of JSON and YAML parsing differ for file: ", filename);
				writeln("-------------------------------------------------------------------");
				writeln("JSON (excepted): ", jsonret);
				writeln("-------------------------------------------------------------------");
				writeln("YAML (actual  ): ", ret);
				writeln("========================================");
				ret = jsonret;
			}
		}
	}
	else if (filename.endsWith(".sdl")) parseSDL(ret, contents, parent, filename);
	else assert(false, "readPackageRecipe called with filename with unknown extension: "~filename);

	// Fix for issue #711: `targetType` should be inherited, or default to library
	static void sanitizeTargetType(ref PackageRecipe r) {
		TargetType defaultTT = (r.buildSettings.targetType == TargetType.autodetect) ?
			TargetType.library : r.buildSettings.targetType;
		foreach (ref conf; r.configurations)
			if (conf.buildSettings.targetType == TargetType.autodetect)
				conf.buildSettings.targetType = defaultTT;

		// recurse into sub packages
		foreach (ref subPackage; r.subPackages)
			sanitizeTargetType(subPackage.recipe);
	}

	sanitizeTargetType(ret);

	return ret;
}


unittest { // issue #711 - configuration default target type not correct for SDL
	import dub.compilers.buildsettings : TargetType;
	auto inputs = [
		"dub.sdl": "name \"test\"\nconfiguration \"a\" {\n}",
		"dub.json": "{\"name\": \"test\", \"configurations\": [{\"name\": \"a\"}]}"
	];
	foreach (file, content; inputs) {
		auto pr = parsePackageRecipe(content, file);
		assert(pr.name == "test");
		assert(pr.configurations.length == 1);
		assert(pr.configurations[0].name == "a");
		assert(pr.configurations[0].buildSettings.targetType == TargetType.library);
	}
}

unittest { // issue #711 - configuration default target type not correct for SDL
	import dub.compilers.buildsettings : TargetType;
	auto inputs = [
		"dub.sdl": "name \"test\"\ntargetType \"autodetect\"\nconfiguration \"a\" {\n}",
		"dub.json": "{\"name\": \"test\", \"targetType\": \"autodetect\", \"configurations\": [{\"name\": \"a\"}]}"
	];
	foreach (file, content; inputs) {
		auto pr = parsePackageRecipe(content, file);
		assert(pr.name == "test");
		assert(pr.configurations.length == 1);
		assert(pr.configurations[0].name == "a");
		assert(pr.configurations[0].buildSettings.targetType == TargetType.library);
	}
}

unittest { // issue #711 - configuration default target type not correct for SDL
	import dub.compilers.buildsettings : TargetType;
	auto inputs = [
		"dub.sdl": "name \"test\"\ntargetType \"executable\"\nconfiguration \"a\" {\n}",
		"dub.json": "{\"name\": \"test\", \"targetType\": \"executable\", \"configurations\": [{\"name\": \"a\"}]}"
	];
	foreach (file, content; inputs) {
		auto pr = parsePackageRecipe(content, file);
		assert(pr.name == "test");
		assert(pr.configurations.length == 1);
		assert(pr.configurations[0].name == "a");
		assert(pr.configurations[0].buildSettings.targetType == TargetType.executable);
	}
}

unittest { // make sure targetType of sub packages are sanitized too
	import dub.compilers.buildsettings : TargetType;
	auto inputs = [
		"dub.sdl": "name \"test\"\nsubPackage {\nname \"sub\"\ntargetType \"sourceLibrary\"\nconfiguration \"a\" {\n}\n}",
		"dub.json": "{\"name\": \"test\", \"subPackages\": [ { \"name\": \"sub\", \"targetType\": \"sourceLibrary\", \"configurations\": [{\"name\": \"a\"}] } ] }"
	];
	foreach (file, content; inputs) {
		auto pr = parsePackageRecipe(content, file);
		assert(pr.name == "test");
		const spr = pr.subPackages[0].recipe;
		assert(spr.name == "sub");
		assert(spr.configurations.length == 1);
		assert(spr.configurations[0].name == "a");
		assert(spr.configurations[0].buildSettings.targetType == TargetType.sourceLibrary);
	}
}


/** Writes the textual representation of a package recipe to a file.

	Note that the file extension must be either "json" or "sdl".
*/
void writePackageRecipe(string filename, const scope ref PackageRecipe recipe)
{
	writePackageRecipe(NativePath(filename), recipe);
}

/// ditto
void writePackageRecipe(NativePath filename, const scope ref PackageRecipe recipe)
{
	import std.array;
	auto app = appender!string();
	serializePackageRecipe(app, recipe, filename.toNativeString());
	writeFile(filename, app.data);
}

/** Converts a package recipe to its textual representation.

	The extension of the supplied `filename` must be either "json" or "sdl".
	The output format is chosen accordingly.
*/
void serializePackageRecipe(R)(ref R dst, const scope ref PackageRecipe recipe, string filename)
{
	import std.algorithm : endsWith;
	import dub.internal.vibecompat.data.json : writeJsonString;
	import dub.recipe.json : toJson;
	import dub.recipe.sdl : toSDL;

	if (filename.endsWith(".json"))
		dst.writeJsonString!(R, true)(toJson(recipe));
	else if (filename.endsWith(".sdl"))
		toSDL(recipe).toSDLDocument(dst);
	else assert(false, "writePackageRecipe called with filename with unknown extension: "~filename);
}

unittest {
	import std.format;
	import dub.dependency;
	import dub.internal.utils : deepCompare;

	static void success (string source, in PackageRecipe expected, size_t line = __LINE__) {
		const result = parseConfigString!PackageRecipe(source, "dub.json");
		deepCompare(result, expected, __FILE__, line);
	}

	static void error (string source, string expected, size_t line = __LINE__) {
		try
		{
			auto result = parseConfigString!PackageRecipe(source, "dub.json");
			assert(0,
				   format("[%s:%d] Exception should have been thrown but wasn't: %s",
						  __FILE__, line, result));
		}
		catch (Exception exc)
			assert(exc.toString() == expected,
				   format("[%s:%s] result != expected: '%s' != '%s'",
						  __FILE__, line, exc.toString(), expected));
	}

	alias YAMLDep = typeof(BuildSettingsTemplate.dependencies[string.init]);
	const PackageRecipe expected1 =
	{
		name: "foo",
		buildSettings: {
		dependencies: RecipeDependencyAA([
			"repo": YAMLDep(Dependency(Repository(
				"git+https://github.com/dlang/dmd",
				"09d04945bdbc0cba36f7bb1e19d5bd009d4b0ff2",
			))),
			"path": YAMLDep(Dependency(NativePath("/foo/bar/jar/"))),
			"version": YAMLDep(Dependency(VersionRange.fromString("~>1.0"))),
			"version2": YAMLDep(Dependency(Version("4.2.0"))),
		])},
	};
	success(
		`{ "name": "foo", "dependencies": {
	"repo": { "repository": "git+https://github.com/dlang/dmd",
			  "version": "09d04945bdbc0cba36f7bb1e19d5bd009d4b0ff2" },
	"path":    { "path": "/foo/bar/jar/" },
	"version": { "version": "~>1.0" },
	"version2": "4.2.0"
}}`, expected1);


	error(`{ "name": "bar", "dependencies": {"bad": { "repository": "git+https://github.com/dlang/dmd" }}}`,
		"dub.json(0:41): dependencies[bad]: Need to provide a commit hash in 'version' field with 'repository' dependency");
}