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