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.easy; 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.recipe.sdl : parseSDL; 87 88 PackageRecipe ret; 89 90 ret.name = default_package_name; 91 92 if (filename.endsWith(".json")) 93 { 94 ret = parseConfigString!PackageRecipe(contents, filename, mode); 95 fixDependenciesNames(ret.name, ret); 96 } 97 else if (filename.endsWith(".sdl")) parseSDL(ret, contents, parent, filename); 98 else assert(false, "readPackageRecipe called with filename with unknown extension: "~filename); 99 100 // Fix for issue #711: `targetType` should be inherited, or default to library 101 static void sanitizeTargetType(ref PackageRecipe r) { 102 TargetType defaultTT = (r.buildSettings.targetType == TargetType.autodetect) ? 103 TargetType.library : r.buildSettings.targetType; 104 foreach (ref conf; r.configurations) 105 if (conf.buildSettings.targetType == TargetType.autodetect) 106 conf.buildSettings.targetType = defaultTT; 107 108 // recurse into sub packages 109 foreach (ref subPackage; r.subPackages) 110 sanitizeTargetType(subPackage.recipe); 111 } 112 113 sanitizeTargetType(ret); 114 115 return ret; 116 } 117 118 119 unittest { // issue #711 - configuration default target type not correct for SDL 120 import dub.compilers.buildsettings : TargetType; 121 auto inputs = [ 122 "dub.sdl": "name \"test\"\nconfiguration \"a\" {\n}", 123 "dub.json": "{\"name\": \"test\", \"configurations\": [{\"name\": \"a\"}]}" 124 ]; 125 foreach (file, content; inputs) { 126 auto pr = parsePackageRecipe(content, file); 127 assert(pr.name == "test"); 128 assert(pr.configurations.length == 1); 129 assert(pr.configurations[0].name == "a"); 130 assert(pr.configurations[0].buildSettings.targetType == TargetType.library); 131 } 132 } 133 134 unittest { // issue #711 - configuration default target type not correct for SDL 135 import dub.compilers.buildsettings : TargetType; 136 auto inputs = [ 137 "dub.sdl": "name \"test\"\ntargetType \"autodetect\"\nconfiguration \"a\" {\n}", 138 "dub.json": "{\"name\": \"test\", \"targetType\": \"autodetect\", \"configurations\": [{\"name\": \"a\"}]}" 139 ]; 140 foreach (file, content; inputs) { 141 auto pr = parsePackageRecipe(content, file); 142 assert(pr.name == "test"); 143 assert(pr.configurations.length == 1); 144 assert(pr.configurations[0].name == "a"); 145 assert(pr.configurations[0].buildSettings.targetType == TargetType.library); 146 } 147 } 148 149 unittest { // issue #711 - configuration default target type not correct for SDL 150 import dub.compilers.buildsettings : TargetType; 151 auto inputs = [ 152 "dub.sdl": "name \"test\"\ntargetType \"executable\"\nconfiguration \"a\" {\n}", 153 "dub.json": "{\"name\": \"test\", \"targetType\": \"executable\", \"configurations\": [{\"name\": \"a\"}]}" 154 ]; 155 foreach (file, content; inputs) { 156 auto pr = parsePackageRecipe(content, file); 157 assert(pr.name == "test"); 158 assert(pr.configurations.length == 1); 159 assert(pr.configurations[0].name == "a"); 160 assert(pr.configurations[0].buildSettings.targetType == TargetType.executable); 161 } 162 } 163 164 unittest { // make sure targetType of sub packages are sanitized too 165 import dub.compilers.buildsettings : TargetType; 166 auto inputs = [ 167 "dub.sdl": "name \"test\"\nsubPackage {\nname \"sub\"\ntargetType \"sourceLibrary\"\nconfiguration \"a\" {\n}\n}", 168 "dub.json": "{\"name\": \"test\", \"subPackages\": [ { \"name\": \"sub\", \"targetType\": \"sourceLibrary\", \"configurations\": [{\"name\": \"a\"}] } ] }" 169 ]; 170 foreach (file, content; inputs) { 171 auto pr = parsePackageRecipe(content, file); 172 assert(pr.name == "test"); 173 const spr = pr.subPackages[0].recipe; 174 assert(spr.name == "sub"); 175 assert(spr.configurations.length == 1); 176 assert(spr.configurations[0].name == "a"); 177 assert(spr.configurations[0].buildSettings.targetType == TargetType.sourceLibrary); 178 } 179 } 180 181 182 /** Writes the textual representation of a package recipe to a file. 183 184 Note that the file extension must be either "json" or "sdl". 185 */ 186 void writePackageRecipe(string filename, const scope ref PackageRecipe recipe) 187 { 188 writePackageRecipe(NativePath(filename), recipe); 189 } 190 191 /// ditto 192 void writePackageRecipe(NativePath filename, const scope ref PackageRecipe recipe) 193 { 194 import std.array; 195 auto app = appender!string(); 196 serializePackageRecipe(app, recipe, filename.toNativeString()); 197 writeFile(filename, app.data); 198 } 199 200 /** Converts a package recipe to its textual representation. 201 202 The extension of the supplied `filename` must be either "json" or "sdl". 203 The output format is chosen accordingly. 204 */ 205 void serializePackageRecipe(R)(ref R dst, const scope ref PackageRecipe recipe, string filename) 206 { 207 import std.algorithm : endsWith; 208 import dub.internal.vibecompat.data.json : writeJsonString; 209 import dub.recipe.json : toJson; 210 import dub.recipe.sdl : toSDL; 211 212 if (filename.endsWith(".json")) 213 dst.writeJsonString!(R, true)(toJson(recipe)); 214 else if (filename.endsWith(".sdl")) 215 toSDL(recipe).toSDLDocument(dst); 216 else assert(false, "writePackageRecipe called with filename with unknown extension: "~filename); 217 } 218 219 unittest { 220 import std.format; 221 import dub.dependency; 222 import dub.internal.utils : deepCompare; 223 224 static void success (string source, in PackageRecipe expected, size_t line = __LINE__) { 225 const result = parseConfigString!PackageRecipe(source, "dub.json"); 226 deepCompare(result, expected, __FILE__, line); 227 } 228 229 static void error (string source, string expected, size_t line = __LINE__) { 230 try 231 { 232 auto result = parseConfigString!PackageRecipe(source, "dub.json"); 233 assert(0, 234 format("[%s:%d] Exception should have been thrown but wasn't: %s", 235 __FILE__, line, result)); 236 } 237 catch (Exception exc) 238 assert(exc.toString() == expected, 239 format("[%s:%s] result != expected: '%s' != '%s'", 240 __FILE__, line, exc.toString(), expected)); 241 } 242 243 alias YAMLDep = typeof(BuildSettingsTemplate.dependencies[string.init]); 244 const PackageRecipe expected1 = 245 { 246 name: "foo", 247 buildSettings: { 248 dependencies: RecipeDependencyAA([ 249 "repo": YAMLDep(Dependency(Repository( 250 "git+https://github.com/dlang/dmd", 251 "09d04945bdbc0cba36f7bb1e19d5bd009d4b0ff2", 252 ))), 253 "path": YAMLDep(Dependency(NativePath("/foo/bar/jar/"))), 254 "version": YAMLDep(Dependency(VersionRange.fromString("~>1.0"))), 255 "version2": YAMLDep(Dependency(Version("4.2.0"))), 256 ])}, 257 }; 258 success( 259 `{ "name": "foo", "dependencies": { 260 "repo": { "repository": "git+https://github.com/dlang/dmd", 261 "version": "09d04945bdbc0cba36f7bb1e19d5bd009d4b0ff2" }, 262 "path": { "path": "/foo/bar/jar/" }, 263 "version": { "version": "~>1.0" }, 264 "version2": "4.2.0" 265 }}`, expected1); 266 267 268 error(`{ "name": "bar", "dependencies": {"bad": { "repository": "git+https://github.com/dlang/dmd" }}}`, 269 "dub.json(1:42): dependencies[bad]: Need to provide a commit hash in 'version' field with 'repository' dependency"); 270 }