Newer
Older
dub_jkp / source / dub / recipe / io.d
  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. }