1 /// Author: Aziz Köksal
2 /// License: GPL3
3 /// $(Maturity average)
4 module SettingsLoader;
5 
6 import dil.ast.Node,
7        dil.ast.Declarations,
8        dil.ast.Expressions;
9 import dil.semantic.Module,
10        dil.semantic.Pass1,
11        dil.semantic.Symbol,
12        dil.semantic.Symbols;
13 import dil.lexer.Funcs,
14        dil.lexer.Identifier;
15 import dil.i18n.Messages,
16        dil.i18n.ResourceBundle;
17 import dil.Diagnostics,
18        dil.Compilation,
19        dil.Unicode,
20        dil.String,
21        dil.Array;
22 import util.Path;
23 import Settings,
24        common;
25 
26 /// Loads settings from a D module file.
27 abstract class SettingsLoader
28 {
29   Diagnostics diag; /// Collects error messages.
30   Module mod; /// Current module.
31   CompilationContext cc; /// The context.
32 
33   /// Constructs a SettingsLoader object.
34   this(CompilationContext cc, Diagnostics diag)
35   {
36     this.cc = cc;
37     this.diag = diag;
38   }
39 
40   /// Creates an error report.
41   /// Params:
42   ///   token = Where the error occurred.
43   ///   formatMsg = Error message.
44   void error(Token* token, cstring formatMsg, ...)
45   {
46     auto location = token.getErrorLocation(mod.filePath);
47     auto msg = Format(_arguments, _argptr, formatMsg);
48     diag ~= new SemanticError(location, msg);
49   }
50 
51   T getValue(T)(cstring name, bool isOptional = false)
52   {
53     auto var = mod.lookup(hashOf(name));
54     T value;
55     if (!var && isOptional)
56     {}
57     else if (!var)
58       error(mod.loc.t, "variable ‘{}’ is not defined", name);
59     else if (!var.isVariable)
60       error(var.loc.t, "‘{}’ is not a variable declaration", name);
61     else if (auto e = var.to!(VariableSymbol).value)
62     {
63       if ((value = e.Is!(T)) is null) // Try casting to T.
64         error(e.begin,
65           "the value of ‘{}’ must be of type ‘{}’", name, T.stringof);
66     }
67     else
68       error(var.loc.t, "‘{}’ variable has no value set", name);
69     return value;
70   }
71 
72   T castTo(T)(Node n)
73   {
74     if (auto result = n.Is!(T))
75       return result;
76     auto type = T.stringof;
77     if (is(T == StringExpr))
78       type = "char[]";
79     else if (is(T == ArrayInitExpr))
80       type = "[]";
81     else if(is(T == IntExpr))
82       type = "int";
83     error(n.begin, "expression is not of type ‘{}’", type);
84     return null;
85   }
86 
87   void load()
88   {}
89 }
90 
91 /// Loads the configuration file of DIL.
92 class ConfigLoader : SettingsLoader
93 {
94   /// Name of the configuration file.
95   static cstring configFileName = "dilconf.d";
96   cstring executablePath; /// Absolute path to DIL's executable.
97   cstring executableDir; /// Absolute path to the directory of DIL's executable.
98   cstring dataDir; /// Absolute path to DIL's data directory.
99   cstring homePath; /// Path to the home directory.
100   cstring dilconfPath; /// Path to dilconf.d to be used.
101 
102   ResourceBundle resourceBundle; /// A bundle for compiler messages.
103 
104   /// Constructs a ConfigLoader object.
105   this(CompilationContext cc, Diagnostics diag, cstring arg0)
106   {
107     super(cc, diag);
108     this.homePath = Environment.get("HOME");
109     this.executablePath = GetExecutableFilePath(arg0);
110     this.executableDir = Path(this.executablePath).path();
111     Environment.set("BINDIR", this.executableDir);
112   }
113 
114   static ConfigLoader opCall(CompilationContext cc, Diagnostics diag,
115     cstring arg0)
116   {
117     return new ConfigLoader(cc, diag, arg0);
118   }
119 
120   /// Expands environment variables such as ${HOME} in a string.
121   static cstring expandVariables(cstring str)
122   {
123     char[] result;
124     const s = String(str);
125     cchar* p = s.ptr, end = s.end;
126     auto pieceBegin = p; // Points past the closing brace '}' of a variable.
127 
128     while (p+3 < end)
129     {
130       auto variableBegin = String(p, end).findp(String("${"));
131       if (!variableBegin)
132         break;
133       auto variableEnd = String(variableBegin + 2, end).findp('}');
134       if (!variableEnd)
135         break; // Don't expand unterminated variables.
136       result ~= slice(pieceBegin, variableBegin); // Copy previous string.
137       // Get the environment variable and append it to the result.
138       result ~= Environment.get(slice(variableBegin + 2, variableEnd));
139       pieceBegin = p = variableEnd + 1; // Point to character after '}'.
140     }
141     if (pieceBegin is s.ptr)
142       return str; // Return unchanged string.
143     if (pieceBegin < end) // Copy end piece.
144       result ~= slice(pieceBegin, end);
145     return result;
146   }
147 
148   /// Returns a normalized path.
149   cstring normalize(cstring path)
150   {
151     return Path(path).normalize()[];
152   }
153 
154   /// Loads the configuration file.
155   override void load()
156   {
157     // Search for the configuration file.
158     dilconfPath = findConfigurationFilePath();
159     if (dilconfPath is null)
160     {
161       diag ~= new GeneralError(new Location("",0),
162         "the configuration file ‘"~configFileName~"’ could not be found.");
163       return;
164     }
165     // Load the file as a D module.
166     mod = new Module(dilconfPath, cc);
167     mod.parse();
168 
169     if (mod.hasErrors)
170       return;
171 
172     auto pass1 = new SemanticPass1(mod, cc);
173     pass1.run();
174 
175     // Initialize the dataDir member.
176     if (auto val = getValue!(StringExpr)("DATADIR"))
177       this.dataDir = val.getString();
178     this.dataDir = normalize(expandVariables(this.dataDir));
179     GlobalSettings.dataDir = this.dataDir;
180     Environment.set("DATADIR", this.dataDir);
181 
182     if (auto val = getValue!(StringExpr)("KANDILDIR"))
183     {
184       auto kandilDir = normalize(expandVariables(val.getString()));
185       GlobalSettings.kandilDir = kandilDir;
186       Environment.set("KANDILDIR", kandilDir);
187     }
188 
189     if (auto array = getValue!(ArrayInitExpr)("VERSION_IDS"))
190       foreach (value; array.values)
191         if (auto val = castTo!(StringExpr)(value))
192           GlobalSettings.versionIds ~= expandVariables(val.getString());
193     if (auto val = getValue!(StringExpr)("LANG_FILE"))
194       GlobalSettings.langFile = normalize(expandVariables(val.getString()));
195     if (auto array = getValue!(ArrayInitExpr)("IMPORT_PATHS"))
196       foreach (value; array.values)
197         if (auto val = castTo!(StringExpr)(value))
198           GlobalSettings.importPaths ~=
199             normalize(expandVariables(val.getString()));
200     if (auto array = getValue!(ArrayInitExpr)("DDOC_FILES"))
201       foreach (value; array.values)
202         if (auto val = castTo!(StringExpr)(value))
203           GlobalSettings.ddocFilePaths ~=
204             normalize(expandVariables(val.getString()));
205     if (auto val = getValue!(StringExpr)("XML_MAP"))
206       GlobalSettings.xmlMapFile = normalize(expandVariables(val.getString()));
207     if (auto val = getValue!(StringExpr)("HTML_MAP"))
208       GlobalSettings.htmlMapFile = normalize(expandVariables(val.getString()));
209     if (auto val = getValue!(StringExpr)("LEXER_ERROR"))
210       GlobalSettings.lexerErrorFormat = expandVariables(val.getString());
211     if (auto val = getValue!(StringExpr)("PARSER_ERROR"))
212       GlobalSettings.parserErrorFormat = expandVariables(val.getString());
213     if (auto val = getValue!(StringExpr)("SEMANTIC_ERROR"))
214       GlobalSettings.semanticErrorFormat = expandVariables(val.getString());
215     if (auto val = getValue!(IntExpr)("TAB_WIDTH"))
216     {
217       GlobalSettings.tabWidth = cast(uint)val.number;
218       Location.TAB_WIDTH = cast(uint)val.number;
219     }
220 
221     auto langFile = expandVariables(GlobalSettings.langFile);
222     resourceBundle = loadResource(langFile);
223   }
224 
225   /// Loads a language file and returns a ResouceBundle object.
226   ResourceBundle loadResource(cstring langFile)
227   {
228     if (!Path(langFile).exists)
229     {
230       diag ~= new GeneralError(new Location("", 0),
231         "the language file ‘"~langFile~"’ does not exist.");
232       goto Lerror;
233     }
234 
235     // 1. Load language file.
236     mod = new Module(langFile, cc);
237     mod.parse();
238 
239     if (mod.hasErrors)
240       goto Lerror;
241 
242     {
243     auto pass1 = new SemanticPass1(mod, cc);
244     pass1.run();
245 
246     // 2. Extract the values of the variables.
247     cstring[] messages = new cstring[MID.max + 1];
248     if (auto array = getValue!(ArrayInitExpr)("messages"))
249     {
250       foreach (i, value; array.values)
251         if (i >= messages.length)
252           break; // More messages given than allowed.
253         else if (value.Is!(NullExpr))
254         {} // messages[i] = null;
255         else if (auto val = castTo!(StringExpr)(value))
256           messages[i] = val.getString();
257       //if (messages.length != MID.max+1)
258         //error(mod.loc.t,
259           //"messages table in {} must exactly have {} entries, but not {}.",
260           //langFile, MID.max+1, messages.length);
261     }
262     cstring langCode;
263     if (auto val = getValue!(StringExpr)("lang_code"))
264       langCode = val.getString();
265     cstring parentLangFile;
266     if (auto val = getValue!(StringExpr)("inherit", true))
267       parentLangFile = expandVariables(val.getString());
268 
269     // 3. Load the parent bundle if one is specified.
270     auto parentRB = parentLangFile ? loadResource(parentLangFile) : null;
271 
272     // 4. Return a new bundle.
273     auto rb = new ResourceBundle(messages, parentRB);
274     rb.langCode = langCode;
275 
276     return rb;
277     }
278   Lerror:
279     return new ResourceBundle();
280   }
281 
282   /// Searches for the configuration file of DIL.
283   /// Returns: the filePath or null if the file couldn't be found.
284   cstring findConfigurationFilePath()
285   {
286     auto path = Path();
287     bool exists(cstring s)
288     {
289       return path.set(s).exists;
290     }
291         // 1. Look in environment variable DILCONF.
292     if (exists(Environment.get("DILCONF")) ||
293         // 2. Look in the current working directory.
294         exists(configFileName) ||
295         // 3. Look in the directory set by HOME.
296         exists(homePath~"/"~configFileName) ||
297         // 4. Look in the binary's directory.
298         exists(executableDir~"/"~configFileName) ||
299         // 5. Look in /etc/.
300         exists("/etc/"~configFileName))
301       return normalize(path[]);
302     else
303       return null;
304   }
305 }
306 
307 /// Loads an associative array from a D module file.
308 class TagMapLoader : SettingsLoader
309 {
310   /// Constructs a TagMapLoader object.
311   this(CompilationContext cc, Diagnostics diag)
312   {
313     super(cc, diag);
314   }
315 
316   static TagMapLoader opCall(CompilationContext cc, Diagnostics diag)
317   {
318     return new TagMapLoader(cc, diag);
319   }
320 
321   cstring[hash_t] load(cstring filePath)
322   {
323     mod = new Module(filePath, cc);
324     mod.parse();
325     if (mod.hasErrors)
326       return null;
327 
328     auto pass1 = new SemanticPass1(mod, cc);
329     pass1.run();
330 
331     cstring[hash_t] map;
332     if (auto array = getValue!(ArrayInitExpr)("map"))
333       foreach (i, value; array.values)
334       {
335         auto key = array.keys[i];
336         if (auto valExp = castTo!(StringExpr)(value))
337           if (!key)
338             error(value.begin, "expected key : value");
339           else if (auto keyExp = castTo!(StringExpr)(key))
340             map[hashOf(keyExp.getString())] = valExp.getString();
341       }
342     return map;
343   }
344 }
345 
346 /// Resolves the path to a file from the executable's dir path
347 /// if it is relative.
348 /// Returns: filePath if it is absolute or execPath + filePath.
349 cstring resolvePath(cstring execPath, cstring filePath)
350 {
351   scope path = Path(filePath);
352   if (path.isAbsolute())
353     return filePath;
354   path.set(execPath).append(filePath);
355   return path[];
356 }
357 
358 extern(Windows) uint GetModuleFileNameW(void*, wchar*, uint);
359 extern(C) size_t readlink(const char* path, char* buf, size_t bufsize);
360 extern(C) int _NSGetExecutablePath(char* buf, uint* bufsize);
361 extern(C) char* realpath(char* base, char* dest);
362 
363 /// Returns the fully qualified path to this executable,
364 /// or arg0 on failure or when a platform is unsupported.
365 /// Params:
366 ///   arg0 = This is argv[0] from main(cstring[] argv).
367 cstring GetExecutableFilePath(cstring arg0)
368 {
369   version(Windows)
370   {
371   wchar[] buffer = new wchar[256];
372   size_t count;
373 
374   while (1)
375   {
376     count = GetModuleFileNameW(null, buffer.ptr, buffer.length);
377     if (count == 0)
378       return arg0;
379     if (buffer.length != count && buffer[count] == '\0')
380       break;
381     buffer.length = buffer.length * 2;
382   }
383   // Reduce buffer to the actual length of the string (excluding '\0'.)
384   buffer.length = count;
385   return toUTF8(buffer);
386   } // version(Windows)
387 
388   else version(linux)
389   {
390   auto buffer = CharArray(256);
391   size_t count;
392 
393   while (1)
394   { // This won't work on very old Linux systems.
395     count = readlink("/proc/self/exe".ptr, buffer.ptr, buffer.cap);
396     if (count == -1)
397       return arg0;
398     if (count < buffer.cap)
399       break;
400     buffer.cap = buffer.cap * 2;
401   }
402   buffer.len = count;
403   return buffer[];
404   } // version(linux)
405 
406   else version(darwin)
407   {
408   // First, get the executable path.
409   uint size;
410   char[] path; // The first time .ptr needs to be null and size to be 0.
411   while (_NSGetExecutablePath(path.ptr, &size) == -1)
412     path = new char[size];
413 
414   // Then, convert it to the »real path«, resolving symlinks, etc.
415   char[] buffer = new char[1024];  // 1024 = PATH_MAX on Mac OS X 10.5
416   if (!realpath(path.ptr, buffer.ptr))
417     return arg0;
418   return MString(buffer.ptr, '\0')[];
419   } // version(darwin)
420 
421   else
422   {
423   pragma(msg,
424     "Warning: GetExecutableFilePath() is not implemented on this platform.");
425   return arg0;
426   }
427 }