1 /******************************************************************************* 2 3 Contains struct definition for settings.json files 4 5 User settings are file that allow to configure dub default behavior. 6 7 *******************************************************************************/ 8 9 module dub.data.settings; 10 11 import dub.internal.configy.Attributes; 12 import dub.internal.vibecompat.inet.path; 13 14 /// Determines which of the default package suppliers are queried for packages. 15 public enum SkipPackageSuppliers { 16 none, /// Uses all configured package suppliers. 17 standard, /// Does not use the default package suppliers (`defaultPackageSuppliers`). 18 configured, /// Does not use default suppliers or suppliers configured in DUB's configuration file 19 all, /// Uses only manually specified package suppliers. 20 default_, /// The value wasn't specified. It is provided in order to know when it is safe to ignore it 21 } 22 23 /** 24 * User-provided settings (configuration) 25 * 26 * All fields in this struct should be optional. 27 * Fields that are *not* optional should be mandatory from the POV 28 * of the application, not the POV of file parsing. 29 * For example, git's `core.author` and `core.email` are required to commit, 30 * but the error happens on the commit, not when the gitconfig is parsed. 31 * 32 * We have multiple configuration locations, and two kinds of fields: 33 * additive and non-additive. Additive fields are fields which are the union 34 * of all configuration files (e.g. `registryURLs`). Non-additive fields 35 * will ignore values set in lower priorities configuration, although parsing 36 * must still succeed. Additive fields are marked as `@Optional`, 37 * non-additive are marked as `SetInfo`. 38 */ 39 package(dub) struct Settings { 40 @Optional string[] registryUrls; 41 @Optional NativePath[] customCachePaths; 42 43 private struct SkipRegistry { 44 SkipPackageSuppliers skipRegistry; 45 static SkipRegistry fromString (string value) { 46 import std.conv : to; 47 auto result = value.to!SkipPackageSuppliers; 48 if (result == SkipPackageSuppliers.default_) { 49 throw new Exception( 50 "skipRegistry value `default_` is only meant for interal use." 51 ~ " Instead, use one of `none`, `standard`, `configured`, or `all`." 52 ); 53 } 54 return SkipRegistry(result); 55 } 56 alias skipRegistry this; 57 } 58 SetInfo!(SkipRegistry) skipRegistry; 59 SetInfo!(string) defaultCompiler; 60 SetInfo!(string) defaultArchitecture; 61 SetInfo!(bool) defaultLowMemory; 62 63 SetInfo!(string[string]) defaultEnvironments; 64 SetInfo!(string[string]) defaultBuildEnvironments; 65 SetInfo!(string[string]) defaultRunEnvironments; 66 SetInfo!(string[string]) defaultPreGenerateEnvironments; 67 SetInfo!(string[string]) defaultPostGenerateEnvironments; 68 SetInfo!(string[string]) defaultPreBuildEnvironments; 69 SetInfo!(string[string]) defaultPostBuildEnvironments; 70 SetInfo!(string[string]) defaultPreRunEnvironments; 71 SetInfo!(string[string]) defaultPostRunEnvironments; 72 SetInfo!(string) dubHome; 73 74 /// Merge a lower priority config (`this`) with a `higher` priority config 75 public Settings merge(Settings higher) 76 return @safe pure nothrow 77 { 78 import std.traits : hasUDA; 79 Settings result; 80 81 static foreach (idx, _; Settings.tupleof) { 82 static if (hasUDA!(Settings.tupleof[idx], Optional)) 83 result.tupleof[idx] = higher.tupleof[idx] ~ this.tupleof[idx]; 84 else static if (IsSetInfo!(typeof(this.tupleof[idx]))) { 85 if (higher.tupleof[idx].set) 86 result.tupleof[idx] = higher.tupleof[idx]; 87 else 88 result.tupleof[idx] = this.tupleof[idx]; 89 } else 90 static assert(false, 91 "Expect `@Optional` or `SetInfo` on: `" ~ 92 __traits(identifier, this.tupleof[idx]) ~ 93 "` of type : `" ~ 94 typeof(this.tupleof[idx]).stringof ~ "`"); 95 } 96 97 return result; 98 } 99 100 /// Workaround multiple `E` declaration in `static foreach` when inline 101 private template IsSetInfo(T) { enum bool IsSetInfo = is(T : SetInfo!E, E); } 102 } 103 104 unittest { 105 import dub.internal.configy.Read; 106 107 const str1 = `{ 108 "registryUrls": [ "http://foo.bar\/optional\/escape" ], 109 "customCachePaths": [ "foo/bar", "foo/foo" ], 110 111 "skipRegistry": "all", 112 "defaultCompiler": "dmd", 113 "defaultArchitecture": "fooarch", 114 "defaultLowMemory": false, 115 116 "defaultEnvironments": { 117 "VAR2": "settings.VAR2", 118 "VAR3": "settings.VAR3", 119 "VAR4": "settings.VAR4" 120 } 121 }`; 122 123 const str2 = `{ 124 "registryUrls": [ "http://bar.foo" ], 125 "customCachePaths": [ "bar/foo", "bar/bar" ], 126 127 "skipRegistry": "none", 128 "defaultCompiler": "ldc", 129 "defaultArchitecture": "bararch", 130 "defaultLowMemory": true, 131 132 "defaultEnvironments": { 133 "VAR": "Hi", 134 } 135 }`; 136 137 auto c1 = parseConfigString!Settings(str1, "/dev/null"); 138 assert(c1.registryUrls == [ "http://foo.bar/optional/escape" ]); 139 assert(c1.customCachePaths == [ NativePath("foo/bar"), NativePath("foo/foo") ]); 140 assert(c1.skipRegistry == SkipPackageSuppliers.all); 141 assert(c1.defaultCompiler == "dmd"); 142 assert(c1.defaultArchitecture == "fooarch"); 143 assert(c1.defaultLowMemory == false); 144 assert(c1.defaultEnvironments.length == 3); 145 assert(c1.defaultEnvironments["VAR2"] == "settings.VAR2"); 146 assert(c1.defaultEnvironments["VAR3"] == "settings.VAR3"); 147 assert(c1.defaultEnvironments["VAR4"] == "settings.VAR4"); 148 149 auto c2 = parseConfigString!Settings(str2, "/dev/null"); 150 assert(c2.registryUrls == [ "http://bar.foo" ]); 151 assert(c2.customCachePaths == [ NativePath("bar/foo"), NativePath("bar/bar") ]); 152 assert(c2.skipRegistry == SkipPackageSuppliers.none); 153 assert(c2.defaultCompiler == "ldc"); 154 assert(c2.defaultArchitecture == "bararch"); 155 assert(c2.defaultLowMemory == true); 156 assert(c2.defaultEnvironments.length == 1); 157 assert(c2.defaultEnvironments["VAR"] == "Hi"); 158 159 auto m1 = c2.merge(c1); 160 // c1 takes priority, so its registryUrls is first 161 assert(m1.registryUrls == [ "http://foo.bar/optional/escape", "http://bar.foo" ]); 162 // Same with CCP 163 assert(m1.customCachePaths == [ 164 NativePath("foo/bar"), NativePath("foo/foo"), 165 NativePath("bar/foo"), NativePath("bar/bar"), 166 ]); 167 168 // c1 fields only 169 assert(m1.skipRegistry == c1.skipRegistry); 170 assert(m1.defaultCompiler == c1.defaultCompiler); 171 assert(m1.defaultArchitecture == c1.defaultArchitecture); 172 assert(m1.defaultLowMemory == c1.defaultLowMemory); 173 assert(m1.defaultEnvironments == c1.defaultEnvironments); 174 175 auto m2 = c1.merge(c2); 176 assert(m2.registryUrls == [ "http://bar.foo", "http://foo.bar/optional/escape" ]); 177 assert(m2.customCachePaths == [ 178 NativePath("bar/foo"), NativePath("bar/bar"), 179 NativePath("foo/bar"), NativePath("foo/foo"), 180 ]); 181 assert(m2.skipRegistry == c2.skipRegistry); 182 assert(m2.defaultCompiler == c2.defaultCompiler); 183 assert(m2.defaultArchitecture == c2.defaultArchitecture); 184 assert(m2.defaultLowMemory == c2.defaultLowMemory); 185 assert(m2.defaultEnvironments == c2.defaultEnvironments); 186 187 auto m3 = Settings.init.merge(c1); 188 assert(m3 == c1); 189 } 190 191 unittest { 192 // Test that SkipPackageRegistry.default_ is not allowed 193 194 import dub.internal.configy.Read; 195 import std.exception : assertThrown; 196 197 const str1 = `{ 198 "skipRegistry": "all" 199 `; 200 assertThrown!Exception(parseConfigString!Settings(str1, "/dev/null")); 201 }