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 }