1 /**
2  * This file implements a configuration script that will setup the correct flags
3  * to link with libclang.
4  *
5  * The script will try to automatically detect the location of libclang by
6  * searching through a number of preset library search paths for different
7  * platforms.
8  *
9  * If the script fails to find libclang or fails to find the correct version,
10  * this script provides a flag,`--llvm-config`, which can be used by manually
11  * executing this script (`./configure`) and specifying the path to the LLVM
12  * configuration binary, `llvm-config`. The LLVM configuration binary will then
13  * be used to find the location of libclang.
14  *
15  * The result of invoking this configuration script is a file,
16  * `linker_flags.txt`, which will be created. This file contains the necessary
17  * linker flags which will be read by the linker when building DStep.
18  *
19  * This script is only intended to be used on the Posix platforms.
20  */
21 module configure;
22 
23 import std.algorithm;
24 import std.array;
25 import std.conv;
26 import std.exception;
27 import std.file;
28 import std.format;
29 import std.getopt;
30 import std.path;
31 import std.process;
32 import std.range;
33 import std.string;
34 import std.uni;
35 import std.traits;
36 import std.typecons : Tuple;
37 
38 version (Posix)
39 {
40     version (OSX) {}
41     else version (linux) {}
42     else version (FreeBSD) {}
43     else
44         static assert("The current platform is not supported");
45 }
46 else
47     static assert("This script should only be run on Posix platforms");
48 
49 /**
50  * The options of the application.
51  *
52  * When parsing the command line arguments, these fields will be set.
53  */
54 struct Options
55 {
56     /// Print extra information.
57     bool verbose = true;
58 
59     /// Indicates if help/usage information was requested.
60     bool help = false;
61 
62     /// The specified path to the LLVM/Clang root directory.
63     string llvmPath;
64 
65     /**
66      * The specified path to the location of additional libraries, like
67      * `ncurses` or `tinfo`, that needs to linked when linking libclang
68      * statically.
69      */
70     string additionalLibPath;
71 
72     /// Indicates if libclang should be statically or dynamically linked.
73     bool staticallyLinkClang = false;
74 
75     /**
76      * Indicates if the whole binary, including the C standard library, should
77      * be statically linked.
78      */
79     bool staticallyLinkBinary = false;
80 
81     /// The path to the LLVM/Clang library directory.
82     string llvmLibPath ()
83     {
84         return llvmPath.empty ? "" : buildPath(llvmPath, "lib");
85     }
86 }
87 
88 /// This struct contains the name and filename of a library.
89 struct LibraryName
90 {
91     /**
92      * The name of the library.
93      *
94      * Used in error message and similar.
95      */
96     string name;
97 
98     /// The filename of the library.
99     string filename;
100 }
101 
102 /// Default configuration and paths.
103 struct DefaultConfig
104 {
105 static:
106 
107     version (D_Ddoc)
108     {
109         /// The name of the Clang dynamic library.
110         enum clangLib = "";
111 
112         /**
113          * A list of default paths where to look for the LLVM and Clang
114          * libraries.
115          */
116         immutable string[] llvmLibPaths = [];
117 
118         /**
119          * A list of default paths where to look for additional libraries.
120          *
121          * Thes are libraries that are not part of LLVM or Clang which are used
122          * when statically linking libclang.
123          */
124         immutable string[] additionalLibPaths = [];
125 
126         /**
127          * The name of the additional static library, like `ncurses` or `tinfo`.
128          *
129          * Used when statically linking libclang.
130          */
131         enum additionalLib = LibraryName();
132 
133         /**
134          * The name of the C++ standard library.
135          *
136          * Used when statically linking libclang.
137          */
138         enum cppLib = "c++";
139     }
140 
141     else version (OSX)
142     {
143         enum clangLib = "libclang.dylib";
144 
145         enum standardPaths = [
146             "/usr/local/lib",
147             "/usr/lib"
148         ];
149 
150         enum macPortsPaths = [
151             "/opt/local/libexec/llvm-10/lib",
152             "/opt/local/libexec/llvm-9.0/lib",
153             "/opt/local/libexec/llvm-8.0/lib",
154             "/opt/local/libexec/llvm-7.0/lib",
155             "/opt/local/libexec/llvm-6.0/lib",
156             "/opt/local/libexec/llvm-5.0/lib",
157             "/opt/local/libexec/llvm-4.0/lib",
158             "/opt/local/libexec/llvm-3.9/lib",
159             "/opt/local/libexec/llvm-3.8/lib",
160             "/opt/local/libexec/llvm-3.7/lib"
161         ];
162 
163         enum homebrewPaths = [
164             "/usr/local/opt/llvm@10/lib",
165             "/usr/local/opt/llvm@9/lib",
166             "/usr/local/opt/llvm@8/lib",
167             "/usr/local/opt/llvm@7/lib",
168             "/usr/local/opt/llvm@6/lib",
169             "/usr/local/opt/llvm@5/lib",
170             "/usr/local/opt/llvm40/lib",
171             "/usr/local/opt/llvm39/lib",
172             "/usr/local/opt/llvm38/lib",
173             "/usr/local/opt/llvm37/lib"
174         ];
175 
176         static assert(macPortsPaths.length == homebrewPaths.length);
177 
178         enum llvmLibPaths = macPortsPaths
179             .zip(homebrewPaths)
180             .map!((Tuple!(string, string) t) => [t[0], t[1]])
181             .joiner
182             .chain(standardPaths)
183             .array;
184 
185         immutable additionalLibPaths = [
186             "/opt/local/lib",
187             "/usr/local/opt/ncurses/lib" // the brew ncurses formula is a keg-only
188         ] ~ standardPaths;
189 
190         enum additionalLib = LibraryName("ncurses", "libncurses.a");
191         enum cppLib = "c++";
192     }
193 
194     else version (linux)
195     {
196         enum clangLib = "libclang.so";
197 
198         enum standardPaths = [
199             "/usr/lib",
200             "/usr/local/lib",
201             "/usr/lib/x86_64-linux-gnu", // Debian
202             "/usr/lib64", // Fedora
203             "/usr/lib32", // Fedora
204             "/data/data/com.termux/files/usr/lib", // Termux
205         ];
206 
207         enum debianPaths = [
208             "/usr/lib/llvm-10/lib",
209             "/usr/lib/llvm-9/lib",
210             "/usr/lib/llvm-8/lib",
211             "/usr/lib/llvm-7/lib",
212             "/usr/lib/llvm-6.0/lib",
213             "/usr/lib/llvm-5.0/lib",
214             "/usr/lib/llvm-4.0/lib",
215             "/usr/lib/llvm-3.9/lib",
216             "/usr/lib/llvm-3.8/lib",
217             "/usr/lib/llvm-3.7/lib"
218         ];
219 
220         enum centOsPaths = [
221             "/usr/lib64/llvm",
222             "/usr/lib32/llvm"
223         ];
224 
225         immutable llvmLibPaths = debianPaths ~ centOsPaths ~ standardPaths;
226         immutable additionalLibPaths = standardPaths;
227 
228         enum additionalLib = LibraryName("tinfo", "libtinfo.a");
229         enum cppLib = "stdc++";
230     }
231 
232     else version (FreeBSD)
233     {
234         enum clangLib = "libclang.so";
235 
236         enum standardPaths = [
237             "/usr/lib",
238             "/usr/local/lib"
239         ];
240 
241         immutable llvmLibPaths = [
242             "/usr/local/llvm90/lib",
243             "/usr/local/llvm80/lib",
244             "/usr/local/llvm70/lib",
245             "/usr/local/llvm60/lib",
246             "/usr/local/llvm50/lib",
247         ] ~ standardPaths;
248 
249         immutable additionalLibPaths = standardPaths;
250 
251         enum additionalLib = LibraryName("ncurses", "libncurses.a");
252         enum cppLib = "c++";
253     }
254 
255     else
256         static assert(false, "Unsupported platform");
257 
258     /// The name of the LLVM configure binary.
259     enum llvmConfigExecutable = "llvm-config";
260 }
261 
262 /**
263  * This class represents a path to a file, like a library or an executable.
264  *
265  * It's the abstract base class for the `LibraryPath` and `LLVMConfigPath`
266  * subclasses.
267  */
268 class Path
269 {
270     private
271     {
272         /**
273          * The name of the file this path represents.
274          *
275          * This is a name for the file that is used in error messages.
276          */
277         string name;
278 
279         /**
280          * A set of standard paths to which to search for the file this path
281          * represents.
282          */
283         const(string)[] standardPaths;
284 
285         /**
286          * The custom path that was specified when invoking this configuration
287          * script, or `null` if no custom path was specified.
288          */
289         string specifiedPath;
290 
291         /// The actual file to look for in `standardPaths` and `specifiedPath`.
292         string fileToCheck;
293 
294         /// Local cache for the full path to the file.
295         string path_;
296     }
297 
298     alias path this;
299 
300     /**
301      * Constructs a new instance of this class.
302      *
303      * Params:
304      *  name = the name of the file this path represents
305      *
306      *  standardPaths = a set of standard paths to which to search for the file
307      *      this path represents
308      *
309      *  specifiedPath = the custom path that was specified when invoking this
310      *      configuration script, or `null` if no custom path was specified
311      *
312      *  fileToCheck = the actual file to look for in `standardPaths` and
313      *      `specifiedPath`
314      */
315     this(string name, const(string)[] standardPaths,
316         string specifiedPath, string fileToCheck)
317     {
318         this.name = name;
319         this.standardPaths = standardPaths;
320         this.specifiedPath = specifiedPath;
321         this.fileToCheck = fileToCheck;
322     }
323 
324     /**
325      * Returns the full path to the file this path represents as a string.
326      *
327      * If `specifiedPath` is non-empty, `fileToCheck` will be searched for in
328      * `specifiedPath`. Otherwise `fileToCheck` will be searched for in
329      * `standardPaths`.
330      *
331      * Returns: the full path to the file this path represents
332      */
333     string path()
334     {
335         if (path_.ptr)
336             return path_;
337 
338         return path_ = specifiedPath.empty ? standardPath : customPath;
339     }
340 
341     override string toString()
342     {
343         return path;
344     }
345 
346     /**
347      * Returns the full path of `fileToCheck` by searching in `standardPaths`.
348      *
349      * Returns: the full path of `fileToCheck` by searching in `standardPaths`
350      *
351      * Throws: an `Exception` if `fileToCheck` cannot be found in any of the
352      *  paths in `standardPath`
353      */
354     string standardPath()
355     {
356         auto errorMessage = format("Could not find %s in any of the standard " ~
357             "paths for %s: \n%s\nPlease specify a path manually using " ~
358             "'./configure --%s-path=<path>'.",
359             fileToCheck, name, standardPaths.join('\n'), name.toLower
360         );
361 
362         auto result = standardPaths.
363             find!(exists).
364             find!(e => e.buildPath(fileToCheck).exists);
365 
366         enforce(!result.empty, errorMessage);
367 
368         return result.front.absolutePath;
369     }
370 
371 private:
372 
373     /**
374      * Returns the full path of `fileToCheck` by searching in `specifiedPath`
375      * and the `PATH` environment variable.
376      *
377      * If `fileToCheck` cannot be found in `specifiedPath` it will search for
378      * `fileToCheck` in the `PATH` environment variable. If that fails, an
379      * exception is thrown.
380      *
381      * Returns: the full path of `fileToCheck`
382      *
383      * Throws: an `Exception` if `fileToCheck` cannot be found in
384      *  `specifiedPath` or the `PATH` environment variable
385      */
386     string customPath()
387     {
388         auto path = specifiedPath.asAbsolutePath.asNormalizedPath.to!string;
389 
390         auto errorMessage = format("The specified library %s in path '%s' " ~
391             "does not exist.", name, path);
392 
393         if (path.exists)
394             return path;
395 
396         path = searchPath(specifiedPath);
397         enforce(path.exists, errorMessage);
398 
399         return path;
400     }
401 }
402 
403 /**
404  * This mixin template contains shared logic to generate the actual
405  * configuration.
406  */
407 mixin template BaseConfigurator()
408 {
409     private
410     {
411         /// The name of the file where the configuration is written.
412         enum configPath = "linker_flags.txt";
413 
414         /// The options that were the result of parsing the command line flags.
415         Options options;
416 
417         /// The default configuration.
418         DefaultConfig defaultConfig;
419 
420         /// The LLVM/Clang library path.
421         Path llvmLibPath;
422     }
423 
424     /**
425      * Initializes the receiver with the given arguments. This method acts as
426      * the shared constructor.
427      *
428      * Params:
429      *  options = the options
430      *  defaultConfig = the default configuration
431      */
432     void initialize(Options options, DefaultConfig defaultConfig)
433     {
434         this.options = options;
435         this.defaultConfig = defaultConfig;
436 
437         llvmLibPath = new Path(
438             "llvm",
439             defaultConfig.llvmLibPaths,
440             options.llvmLibPath,
441             defaultConfig.clangLib
442         );
443     }
444 
445 private:
446 
447     /**
448      * Writes given configuration to the config file.
449      *
450      * Params:
451      *  config = the configuration to write, that is, the linker flags
452      */
453     void writeConfig(string config)
454     {
455         write(configPath, config);
456     }
457 
458     /// Returns: the configuration, that is, the linker flags.
459     string config()
460     {
461         return flags.filter!(e => !e.empty).join("\n") ~ '\n';
462     }
463 }
464 
465 /**
466  * This struct contains the logic for generating the configuration for static
467  * linking.
468  */
469 struct StaticConfigurator
470 {
471     mixin BaseConfigurator;
472 
473     private
474     {
475         version (D_Ddoc)
476         {
477             /**
478              * Contains the `--start-group` flag on non-macOS platforms.
479              *
480              * Used on non-macOS platforms to group the LLVM and Clang
481              * libraries to be searched repeatedly to resolve undefined symbols.
482              */
483             enum startGroupFlag = "";
484 
485             /**
486              * Contains the `--end-group` flag on non-macOS platforms.
487              *
488              * Used on non-macOS platforms to group the LLVM and Clang
489              * libraries to be searched repeatedly to resolve undefined symbols.
490              */
491             enum endGroupFlag = "";
492         }
493 
494         else version (OSX)
495         {
496             enum startGroupFlag = "".only;
497             enum endGroupFlag = "".only;
498         }
499 
500         else
501         {
502             enum startGroupFlag = "--start-group".only;
503             enum endGroupFlag = "-Wl,--end-group".only;
504         }
505 
506         /// Local cache for the additional library path.
507         Path additionalLibPath;
508     }
509 
510     /**
511      * Constructs a new instance of this struct with the given arguments.
512      *
513      * Params:
514      *  options = the options
515      *  defaultConfig = the default configuration
516      */
517     this(Options options, DefaultConfig defaultConfig)
518     {
519         initialize(options, defaultConfig);
520 
521         additionalLibPath = new Path(defaultConfig.additionalLib.name,
522             DefaultConfig.additionalLibPaths,
523             options.additionalLibPath, defaultConfig.additionalLib.filename);
524     }
525 
526     /**
527      * Generates the actual configuration.
528      *
529      * This will locate all required libraries, build a set of linker flags and
530      * write the result to the configuration file.
531      */
532     void generateConfig()
533     {
534         enforceLibrariesExist(
535             DefaultConfig.additionalLib.name,
536             additionalLibPath,
537             DefaultConfig.additionalLib.filename
538         );
539 
540         writeConfig(config);
541     }
542 
543 private:
544 
545     /// Return: a range of all the necessary linker flags.
546     auto flags()
547     {
548         return chain(
549             startGroupFlag,
550             libclangFlags,
551             llvmFlags,
552             endGroupFlag,
553             additionalLibFlags,
554             cppFlags,
555             extraFlags
556         );
557     }
558 
559     /**
560      * Returns: a range of linker flags necessary to link with the standard C++
561      *  library.
562      */
563     auto cppFlags()
564     {
565         return format("-l%s", DefaultConfig.cppLib).only;
566     }
567 
568     /**
569      * Returns: a range of linker flags necessary to link with the ncurses
570      *  library.
571      */
572     auto additionalLibFlags()
573     {
574         return additionalLibPath
575             .buildPath(DefaultConfig.additionalLib.filename)
576             .only;
577     }
578 
579     /**
580      * Returns: a range of linker flags necessary to link with the LLVM
581      *  libraries.
582      */
583     auto llvmFlags()
584     {
585         const result = dirEntries(llvmLibPath, "libLLVM*.a", SpanMode.shallow)
586             .map!(e => e.name)
587             .array;
588 
589         const findAllSymbolsPath = llvmLibPath.buildPath("libfindAllSymbols.a");
590 
591         return findAllSymbolsPath.exists ? result ~ findAllSymbolsPath : result;
592     }
593 
594     /**
595      * Returns: a range of linker flags necessary to link with the Clang
596      *  libraries.
597      */
598     auto libclangFlags()
599     {
600         return dirEntries(llvmLibPath, "libclang*.a", SpanMode.shallow);
601     }
602 
603     auto extraFlags()
604     {
605          return (options.staticallyLinkBinary ? "-static" : "").only;
606     }
607 }
608 
609 /**
610  * This struct contains the logic for generating the configuration for dynamic
611  * linking.
612  */
613 struct DynamicConfigurator
614 {
615     mixin BaseConfigurator;
616 
617     /**
618      * Constructs a new instance of this struct with the given arguments.
619      *
620      * Params:
621      *  options = the options
622      *  defaultConfig = the default configuration
623      */
624     this(Options options, DefaultConfig defaultConfig)
625     {
626         initialize(options, defaultConfig);
627     }
628 
629     /**
630      * Generates the actual configuration.
631      *
632      * This will locate all required libraries, build a set of linker flags and
633      * write the result to the configuration file.
634      */
635     void generateConfig()
636     {
637         enforceLibrariesExist("libclang", llvmLibPath, DefaultConfig.clangLib);
638 
639         writeConfig(config);
640     }
641 
642 private:
643 
644     /// Return: a range of all the necessary linker flags.
645     auto flags()
646     {
647         return format("-L%1$s\n-lclang\n-Xlinker -rpath %1$s", llvmLibPath)
648             .only;
649     }
650 }
651 
652 /// The main entry point of this script.
653 void main(string[] args)
654 {
655     auto options = parseArguments(args);
656 
657     if (!options.help)
658     {
659         if (options.staticallyLinkClang)
660             StaticConfigurator(options, DefaultConfig()).generateConfig();
661         else
662             DynamicConfigurator(options, DefaultConfig()).generateConfig();
663     }
664 }
665 
666 private:
667 
668 /**
669  * Parses the command line arguments given to the application.
670  *
671  * Params:
672  *  args = the command line arguments to parse
673  *
674  * Returns: the options set while parsing the arguments
675  */
676 Options parseArguments(string[] args)
677 {
678     import std.typecons : tuple;
679 
680     Options options;
681 
682     auto defaultGetoptArgs = tuple(
683         args,
684         "llvm-path", "The path to the LLVM/Clang root directory.", &options.llvmPath,
685         // "ncurses-lib-path", "The path to the ncurses library.", &options.ncursesLibPath,
686         "statically-link-clang", "Statically link libclang. Defaults to no.", &options.staticallyLinkClang,
687         "statically-link-binary", "Completely statically link the binary. Defaults to no.", &options.staticallyLinkBinary
688     );
689 
690     version (OSX)
691         auto getoptArgs = defaultGetoptArgs;
692     else
693     {
694         auto getoptArgs = tuple(
695             defaultGetoptArgs.tupleof,
696             "statically-link-binary", "Completely statically link the binary. Defaults to no.", &options.staticallyLinkBinary
697         );
698     }
699 
700     auto help = getopt(defaultGetoptArgs.tupleof);
701     postProcessArguments(help, options);
702 
703     return options;
704 }
705 
706 /**
707  * Post processes the arguments.
708  *
709  * This will:
710  * $(UL
711  *      $(LI Print the help/usage information, if that was requested)
712  *      $(LI
713  *          Set the `help` field of the `options` struct to `true`, if help was
714  *          requested
715  *      )
716  *      $(LI
717  *           Set `staticallyLinkClang` to `true` if `staticallyLinkBinary` is
718  *           true
719  *      )
720  * )
721  *
722  * Params:
723  *  result = the result value from the call to `getopt`
724  *  options = the struct containing the parsed arguments
725  */
726 void postProcessArguments(GetoptResult result, ref Options options)
727 {
728     if (options.staticallyLinkBinary)
729         options.staticallyLinkClang = true;
730 
731     if (!result.helpWanted)
732         return;
733 
734     options.help = true;
735 
736     defaultGetoptPrinter("Usage: ./configure [options]\n\nOptions:",
737         result.options);
738 }
739 
740 /**
741  * Enforces that a given set of libraries exist.
742  *
743  * Params:
744  *  name = a textual representation of the set of libraries to check for.
745  *      Will be used in error messages
746  *
747  *  path = the path to the directory where to look for the libraries
748  *  libraries = the actual libraries to look for
749  *
750  * Throws: Exception if any of the given libraries don't exist
751  */
752 void enforceLibrariesExist(string name, string path,
753     const(string)[] libraries ...)
754 {
755     auto errorMessage = format("All required %s libraries could not be " ~
756         "found in the path '%s'.\nRequired libraries are:\n%s", name, path,
757         libraries.join("\n"));
758 
759     alias libraryExists = library => path.buildPath(library).exists;
760 
761     enforce(libraries.all!(libraryExists), errorMessage);
762 }
763 
764 /**
765  * Searches the `PATH` environment variable for the given filename.
766  *
767  * Params:
768  *  filename = the filename to search for in the `PATH`
769  *
770  * Return: the full path to the given filename if found, otherwise `null`
771  */
772 string searchPath(string filename)
773 {
774     auto path =
775         environment.get("PATH", "").
776         split(':').
777         map!(path => path.buildPath(filename)).
778         find!(exists);
779 
780     return path.empty ? null : path.front;
781 }