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 }