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 }
21 
22 /**
23  * User-provided settings (configuration)
24  *
25  * All fields in this struct should be optional.
26  * Fields that are *not* optional should be mandatory from the POV
27  * of the application, not the POV of file parsing.
28  * For example, git's `core.author` and `core.email` are required to commit,
29  * but the error happens on the commit, not when the gitconfig is parsed.
30  *
31  * We have multiple configuration locations, and two kinds of fields:
32  * additive and non-additive. Additive fields are fields which are the union
33  * of all configuration files (e.g. `registryURLs`). Non-additive fields
34  * will ignore values set in lower priorities configuration, although parsing
35  * must still succeed. Additive fields are marked as `@Optional`,
36  * non-additive are marked as `SetInfo`.
37  */
38 package(dub) struct Settings {
39     @Optional string[] registryUrls;
40     @Optional NativePath[] customCachePaths;
41 
42     SetInfo!(SkipPackageSuppliers) skipRegistry;
43     SetInfo!(string) defaultCompiler;
44     SetInfo!(string) defaultArchitecture;
45     SetInfo!(bool) defaultLowMemory;
46 
47     SetInfo!(string[string]) defaultEnvironments;
48     SetInfo!(string[string]) defaultBuildEnvironments;
49     SetInfo!(string[string]) defaultRunEnvironments;
50     SetInfo!(string[string]) defaultPreGenerateEnvironments;
51     SetInfo!(string[string]) defaultPostGenerateEnvironments;
52     SetInfo!(string[string]) defaultPreBuildEnvironments;
53     SetInfo!(string[string]) defaultPostBuildEnvironments;
54     SetInfo!(string[string]) defaultPreRunEnvironments;
55     SetInfo!(string[string]) defaultPostRunEnvironments;
56     SetInfo!(string) dubHome;
57 
58     /// Merge a lower priority config (`this`) with a `higher` priority config
59     public Settings merge(Settings higher)
60         return @safe pure nothrow
61     {
62         import std.traits : hasUDA;
63         Settings result;
64 
65         static foreach (idx, _; Settings.tupleof) {
66             static if (hasUDA!(Settings.tupleof[idx], Optional))
67                 result.tupleof[idx] = higher.tupleof[idx] ~ this.tupleof[idx];
68             else static if (IsSetInfo!(typeof(this.tupleof[idx]))) {
69                 if (higher.tupleof[idx].set)
70                     result.tupleof[idx] = higher.tupleof[idx];
71                 else
72                     result.tupleof[idx] = this.tupleof[idx];
73             } else
74                 static assert(false,
75                               "Expect `@Optional` or `SetInfo` on: `" ~
76                               __traits(identifier, this.tupleof[idx]) ~
77                               "` of type : `" ~
78                               typeof(this.tupleof[idx]).stringof ~ "`");
79         }
80 
81         return result;
82     }
83 
84     /// Workaround multiple `E` declaration in `static foreach` when inline
85     private template IsSetInfo(T) { enum bool IsSetInfo = is(T : SetInfo!E, E); }
86 }
87 
88 unittest {
89     import dub.internal.configy.Read;
90 
91     const str1 = `{
92   "registryUrls": [ "http://foo.bar\/optional\/escape" ],
93   "customCachePaths": [ "foo/bar", "foo/foo" ],
94 
95   "skipRegistry": "all",
96   "defaultCompiler": "dmd",
97   "defaultArchitecture": "fooarch",
98   "defaultLowMemory": false,
99 
100   "defaultEnvironments": {
101     "VAR2": "settings.VAR2",
102     "VAR3": "settings.VAR3",
103     "VAR4": "settings.VAR4"
104   }
105 }`;
106 
107     const str2 = `{
108   "registryUrls": [ "http://bar.foo" ],
109   "customCachePaths": [ "bar/foo", "bar/bar" ],
110 
111   "skipRegistry": "none",
112   "defaultCompiler": "ldc",
113   "defaultArchitecture": "bararch",
114   "defaultLowMemory": true,
115 
116   "defaultEnvironments": {
117     "VAR": "Hi",
118   }
119 }`;
120 
121      auto c1 = parseConfigString!Settings(str1, "/dev/null");
122      assert(c1.registryUrls == [ "http://foo.bar/optional/escape" ]);
123      assert(c1.customCachePaths == [ NativePath("foo/bar"), NativePath("foo/foo") ]);
124      assert(c1.skipRegistry == SkipPackageSuppliers.all);
125      assert(c1.defaultCompiler == "dmd");
126      assert(c1.defaultArchitecture == "fooarch");
127      assert(c1.defaultLowMemory == false);
128      assert(c1.defaultEnvironments.length == 3);
129      assert(c1.defaultEnvironments["VAR2"] == "settings.VAR2");
130      assert(c1.defaultEnvironments["VAR3"] == "settings.VAR3");
131      assert(c1.defaultEnvironments["VAR4"] == "settings.VAR4");
132 
133      auto c2 = parseConfigString!Settings(str2, "/dev/null");
134      assert(c2.registryUrls == [ "http://bar.foo" ]);
135      assert(c2.customCachePaths == [ NativePath("bar/foo"), NativePath("bar/bar") ]);
136      assert(c2.skipRegistry == SkipPackageSuppliers.none);
137      assert(c2.defaultCompiler == "ldc");
138      assert(c2.defaultArchitecture == "bararch");
139      assert(c2.defaultLowMemory == true);
140      assert(c2.defaultEnvironments.length == 1);
141      assert(c2.defaultEnvironments["VAR"] == "Hi");
142 
143      auto m1 = c2.merge(c1);
144      // c1 takes priority, so its registryUrls is first
145      assert(m1.registryUrls == [ "http://foo.bar/optional/escape", "http://bar.foo" ]);
146      // Same with CCP
147      assert(m1.customCachePaths == [
148          NativePath("foo/bar"), NativePath("foo/foo"),
149          NativePath("bar/foo"), NativePath("bar/bar"),
150      ]);
151 
152      // c1 fields only
153      assert(m1.skipRegistry == c1.skipRegistry);
154      assert(m1.defaultCompiler == c1.defaultCompiler);
155      assert(m1.defaultArchitecture == c1.defaultArchitecture);
156      assert(m1.defaultLowMemory == c1.defaultLowMemory);
157      assert(m1.defaultEnvironments == c1.defaultEnvironments);
158 
159      auto m2 = c1.merge(c2);
160      assert(m2.registryUrls == [ "http://bar.foo", "http://foo.bar/optional/escape" ]);
161      assert(m2.customCachePaths == [
162          NativePath("bar/foo"), NativePath("bar/bar"),
163          NativePath("foo/bar"), NativePath("foo/foo"),
164      ]);
165      assert(m2.skipRegistry == c2.skipRegistry);
166      assert(m2.defaultCompiler == c2.defaultCompiler);
167      assert(m2.defaultArchitecture == c2.defaultArchitecture);
168      assert(m2.defaultLowMemory == c2.defaultLowMemory);
169      assert(m2.defaultEnvironments == c2.defaultEnvironments);
170 
171      auto m3 = Settings.init.merge(c1);
172      assert(m3 == c1);
173 }