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 }