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         ];
205 
206         enum debianPaths = [
207             "/usr/lib/llvm-10/lib",
208             "/usr/lib/llvm-9/lib",
209             "/usr/lib/llvm-8/lib",
210             "/usr/lib/llvm-7/lib",
211             "/usr/lib/llvm-6.0/lib",
212             "/usr/lib/llvm-5.0/lib",
213             "/usr/lib/llvm-4.0/lib",
214             "/usr/lib/llvm-3.9/lib",
215             "/usr/lib/llvm-3.8/lib",
216             "/usr/lib/llvm-3.7/lib"
217         ];
218 
219         enum centOsPaths = [
220             "/usr/lib64/llvm",
221             "/usr/lib32/llvm"
222         ];
223 
224         immutable llvmLibPaths = debianPaths ~ centOsPaths ~ standardPaths;
225         immutable additionalLibPaths = standardPaths;
226 
227         enum additionalLib = LibraryName("tinfo", "libtinfo.a");
228         enum cppLib = "stdc++";
229     }
230 
231     else version (FreeBSD)
232     {
233         enum clangLib = "libclang.so";
234 
235         enum standardPaths = [
236             "/usr/lib",
237             "/usr/local/lib"
238         ];
239 
240         immutable llvmLibPaths = [
241             "/usr/local/llvm90/lib",
242             "/usr/local/llvm80/lib",
243             "/usr/local/llvm70/lib",
244             "/usr/local/llvm60/lib",
245             "/usr/local/llvm50/lib",
246         ] ~ standardPaths;
247 
248         immutable additionalLibPaths = standardPaths;
249 
250         enum additionalLib = LibraryName("ncurses", "libncurses.a");
251         enum cppLib = "c++";
252     }
253 
254     else
255         static assert(false, "Unsupported platform");
256 
257     /// The name of the LLVM configure binary.
258     enum llvmConfigExecutable = "llvm-config";
259 }
260 
261 /**
262  * This class represents a path to a file, like a library or an executable.
263  *
264  * It's the abstract base class for the `LibraryPath` and `LLVMConfigPath`
265  * subclasses.
266  */
267 class Path
268 {
269     private
270     {
271         /**
272          * The name of the file this path represents.
273          *
274          * This is a name for the file that is used in error messages.
275          */
276         string name;
277 
278         /**
279          * A set of standard paths to which to search for the file this path
280          * represents.
281          */
282         const(string)[] standardPaths;
283 
284         /**
285          * The custom path that was specified when invoking this configuration
286          * script, or `null` if no custom path was specified.
287          */
288         string specifiedPath;
289 
290         /// The actual file to look for in `standardPaths` and `specifiedPath`.
291         string fileToCheck;
292 
293         /// Local cache for the full path to the file.
294         string path_;
295     }
296 
297     alias path this;
298 
299     /**
300      * Constructs a new instance of this class.
301      *
302      * Params:
303      *  name = the name of the file this path represents
304      *
305      *  standardPaths = a set of standard paths to which to search for the file
306      *      this path represents
307      *
308      *  specifiedPath = the custom path that was specified when invoking this
309      *      configuration script, or `null` if no custom path was specified
310      *
311      *  fileToCheck = the actual file to look for in `standardPaths` and
312      *      `specifiedPath`
313      */
314     this(string name, const(string)[] standardPaths,
315         string specifiedPath, string fileToCheck)
316     {
317         this.name = name;
318         this.standardPaths = standardPaths;
319         this.specifiedPath = specifiedPath;
320         this.fileToCheck = fileToCheck;
321     }
322 
323     /**
324      * Returns the full path to the file this path represents as a string.
325      *
326      * If `specifiedPath` is non-empty, `fileToCheck` will be searched for in
327      * `specifiedPath`. Otherwise `fileToCheck` will be searched for in
328      * `standardPaths`.
329      *
330      * Returns: the full path to the file this path represents
331      */
332     string path()
333     {
334         if (path_.ptr)
335             return path_;
336 
337         return path_ = specifiedPath.empty ? standardPath : customPath;
338     }
339 
340     override string toString()
341     {
342         return path;
343     }
344 
345     /**
346      * Returns the full path of `fileToCheck` by searching in `standardPaths`.
347      *
348      * Returns: the full path of `fileToCheck` by searching in `standardPaths`
349      *
350      * Throws: an `Exception` if `fileToCheck` cannot be found in any of the
351      *  paths in `standardPath`
352      */
353     string standardPath()
354     {
355         auto errorMessage = format("Could not find %s in any of the standard " ~
356             "paths for %s: \n%s\nPlease specify a path manually using " ~
357             "'./configure --%s-path=<path>'.",
358             fileToCheck, name, standardPaths.join('\n'), name.toLower
359         );
360 
361         auto result = standardPaths.
362             find!(exists).
363             find!(e => e.buildPath(fileToCheck).exists);
364 
365         enforce(!result.empty, errorMessage);
366 
367         return result.front.absolutePath;
368     }
369 
370 private:
371 
372     /**
373      * Returns the full path of `fileToCheck` by searching in `specifiedPath`
374      * and the `PATH` environment variable.
375      *
376      * If `fileToCheck` cannot be found in `specifiedPath` it will search for
377      * `fileToCheck` in the `PATH` environment variable. If that fails, an
378      * exception is thrown.
379      *
380      * Returns: the full path of `fileToCheck`
381      *
382      * Throws: an `Exception` if `fileToCheck` cannot be found in
383      *  `specifiedPath` or the `PATH` environment variable
384      */
385     string customPath()
386     {
387         auto path = specifiedPath.asAbsolutePath.asNormalizedPath.to!string;
388 
389         auto errorMessage = format("The specified library %s in path '%s' " ~
390             "does not exist.", name, path);
391 
392         if (path.exists)
393             return path;
394 
395         path = searchPath(specifiedPath);
396         enforce(path.exists, errorMessage);
397 
398         return path;
399     }
400 }
401 
402 /**
403  * This mixin template contains shared logic to generate the actual
404  * configuration.
405  */
406 mixin template BaseConfigurator()
407 {
408     private
409     {
410         /// The name of the file where the configuration is written.
411         enum configPath = "linker_flags.txt";
412 
413         /// The options that were the result of parsing the command line flags.
414         Options options;
415 
416         /// The default configuration.
417         DefaultConfig defaultConfig;
418 
419         /// The LLVM/Clang library path.
420         Path llvmLibPath;
421     }
422 
423     /**
424      * Initializes the receiver with the given arguments. This method acts as
425      * the shared constructor.
426      *
427      * Params:
428      *  options = the options
429      *  defaultConfig = the default configuration
430      */
431     void initialize(Options options, DefaultConfig defaultConfig)
432     {
433         this.options = options;
434         this.defaultConfig = defaultConfig;
435 
436         llvmLibPath = new Path(
437             "llvm",
438             defaultConfig.llvmLibPaths,
439             options.llvmLibPath,
440             defaultConfig.clangLib
441         );
442     }
443 
444 private:
445 
446     /**
447      * Writes given configuration to the config file.
448      *
449      * Params:
450      *  config = the configuration to write, that is, the linker flags
451      */
452     void writeConfig(string config)
453     {
454         write(configPath, config);
455     }
456 
457     /// Returns: the configuration, that is, the linker flags.
458     string config()
459     {
460         return flags.filter!(e => !e.empty).join("\n") ~ '\n';
461     }
462 }
463 
464 /**
465  * This struct contains the logic for generating the configuration for static
466  * linking.
467  */
468 struct StaticConfigurator
469 {
470     mixin BaseConfigurator;
471 
472     private
473     {
474         version (D_Ddoc)
475         {
476             /**
477              * Contains the `--start-group` flag on non-macOS platforms.
478              *
479              * Used on non-macOS platforms to group the LLVM and Clang
480              * libraries to be searched repeatedly to resolve undefined symbols.
481              */
482             enum startGroupFlag = "";
483 
484             /**
485              * Contains the `--end-group` flag on non-macOS platforms.
486              *
487              * Used on non-macOS platforms to group the LLVM and Clang
488              * libraries to be searched repeatedly to resolve undefined symbols.
489              */
490             enum endGroupFlag = "";
491         }
492 
493         else version (OSX)
494         {
495             enum startGroupFlag = "".only;
496             enum endGroupFlag = "".only;
497         }
498 
499         else
500         {
501             enum startGroupFlag = "--start-group".only;
502             enum endGroupFlag = "-Wl,--end-group".only;
503         }
504 
505         /// Local cache for the additional library path.
506         Path additionalLibPath;
507     }
508 
509     /**
510      * Constructs a new instance of this struct with the given arguments.
511      *
512      * Params:
513      *  options = the options
514      *  defaultConfig = the default configuration
515      */
516     this(Options options, DefaultConfig defaultConfig)
517     {
518         initialize(options, defaultConfig);
519 
520         additionalLibPath = new Path(defaultConfig.additionalLib.name,
521             DefaultConfig.additionalLibPaths,
522             options.additionalLibPath, defaultConfig.additionalLib.filename);
523     }
524 
525     /**
526      * Generates the actual configuration.
527      *
528      * This will locate all required libraries, build a set of linker flags and
529      * write the result to the configuration file.
530      */
531     void generateConfig()
532     {
533         enforceLibrariesExist(
534             DefaultConfig.additionalLib.name,
535             additionalLibPath,
536             DefaultConfig.additionalLib.filename
537         );
538 
539         writeConfig(config);
540     }
541 
542 private:
543 
544     /// Return: a range of all the necessary linker flags.
545     auto flags()
546     {
547         return chain(
548             startGroupFlag,
549             libclangFlags,
550             llvmFlags,
551             endGroupFlag,
552             additionalLibFlags,
553             cppFlags,
554             extraFlags
555         );
556     }
557 
558     /**
559      * Returns: a range of linker flags necessary to link with the standard C++
560      *  library.
561      */
562     auto cppFlags()
563     {
564         return format("-l%s", DefaultConfig.cppLib).only;
565     }
566 
567     /**
568      * Returns: a range of linker flags necessary to link with the ncurses
569      *  library.
570      */
571     auto additionalLibFlags()
572     {
573         return additionalLibPath
574             .buildPath(DefaultConfig.additionalLib.filename)
575             .only;
576     }
577 
578     /**
579      * Returns: a range of linker flags necessary to link with the LLVM
580      *  libraries.
581      */
582     auto llvmFlags()
583     {
584         const result = dirEntries(llvmLibPath, "libLLVM*.a", SpanMode.shallow)
585             .map!(e => e.name)
586             .array;
587 
588         const findAllSymbolsPath = llvmLibPath.buildPath("libfindAllSymbols.a");
589 
590         return findAllSymbolsPath.exists ? result ~ findAllSymbolsPath : result;
591     }
592 
593     /**
594      * Returns: a range of linker flags necessary to link with the Clang
595      *  libraries.
596      */
597     auto libclangFlags()
598     {
599         return dirEntries(llvmLibPath, "libclang*.a", SpanMode.shallow);
600     }
601 
602     auto extraFlags()
603     {
604          return (options.staticallyLinkBinary ? "-static" : "").only;
605     }
606 }
607 
608 /**
609  * This struct contains the logic for generating the configuration for dynamic
610  * linking.
611  */
612 struct DynamicConfigurator
613 {
614     mixin BaseConfigurator;
615 
616     /**
617      * Constructs a new instance of this struct with the given arguments.
618      *
619      * Params:
620      *  options = the options
621      *  defaultConfig = the default configuration
622      */
623     this(Options options, DefaultConfig defaultConfig)
624     {
625         initialize(options, defaultConfig);
626     }
627 
628     /**
629      * Generates the actual configuration.
630      *
631      * This will locate all required libraries, build a set of linker flags and
632      * write the result to the configuration file.
633      */
634     void generateConfig()
635     {
636         enforceLibrariesExist("libclang", llvmLibPath, DefaultConfig.clangLib);
637 
638         writeConfig(config);
639     }
640 
641 private:
642 
643     /// Return: a range of all the necessary linker flags.
644     auto flags()
645     {
646         return format("-L%1$s\n-lclang\n-Xlinker -rpath %1$s", llvmLibPath)
647             .only;
648     }
649 }
650 
651 /// The main entry point of this script.
652 void main(string[] args)
653 {
654     auto options = parseArguments(args);
655 
656     if (!options.help)
657     {
658         if (options.staticallyLinkClang)
659             StaticConfigurator(options, DefaultConfig()).generateConfig();
660         else
661             DynamicConfigurator(options, DefaultConfig()).generateConfig();
662     }
663 }
664 
665 private:
666 
667 /**
668  * Parses the command line arguments given to the application.
669  *
670  * Params:
671  *  args = the command line arguments to parse
672  *
673  * Returns: the options set while parsing the arguments
674  */
675 Options parseArguments(string[] args)
676 {
677     import std.typecons : tuple;
678 
679     Options options;
680 
681     auto defaultGetoptArgs = tuple(
682         args,
683         "llvm-path", "The path to the LLVM/Clang root directory.", &options.llvmPath,
684         // "ncurses-lib-path", "The path to the ncurses library.", &options.ncursesLibPath,
685         "statically-link-clang", "Statically link libclang. Defaults to no.", &options.staticallyLinkClang,
686         "statically-link-binary", "Completely statically link the binary. Defaults to no.", &options.staticallyLinkBinary
687     );
688 
689     version (OSX)
690         auto getoptArgs = defaultGetoptArgs;
691     else
692     {
693         auto getoptArgs = tuple(
694             defaultGetoptArgs.tupleof,
695             "statically-link-binary", "Completely statically link the binary. Defaults to no.", &options.staticallyLinkBinary
696         );
697     }
698 
699     auto help = getopt(defaultGetoptArgs.tupleof);
700     postProcessArguments(help, options);
701 
702     return options;
703 }
704 
705 /**
706  * Post processes the arguments.
707  *
708  * This will:
709  * $(UL
710  *      $(LI Print the help/usage information, if that was requested)
711  *      $(LI
712  *          Set the `help` field of the `options` struct to `true`, if help was
713  *          requested
714  *      )
715  *      $(LI
716  *           Set `staticallyLinkClang` to `true` if `staticallyLinkBinary` is
717  *           true
718  *      )
719  * )
720  *
721  * Params:
722  *  result = the result value from the call to `getopt`
723  *  options = the struct containing the parsed arguments
724  */
725 void postProcessArguments(GetoptResult result, ref Options options)
726 {
727     if (options.staticallyLinkBinary)
728         options.staticallyLinkClang = true;
729 
730     if (!result.helpWanted)
731         return;
732 
733     options.help = true;
734 
735     defaultGetoptPrinter("Usage: ./configure [options]\n\nOptions:",
736         result.options);
737 }
738 
739 /**
740  * Enforces that a given set of libraries exist.
741  *
742  * Params:
743  *  name = a textual representation of the set of libraries to check for.
744  *      Will be used in error messages
745  *
746  *  path = the path to the directory where to look for the libraries
747  *  libraries = the actual libraries to look for
748  *
749  * Throws: Exception if any of the given libraries don't exist
750  */
751 void enforceLibrariesExist(string name, string path,
752     const(string)[] libraries ...)
753 {
754     auto errorMessage = format("All required %s libraries could not be " ~
755         "found in the path '%s'.\nRequired libraries are:\n%s", name, path,
756         libraries.join("\n"));
757 
758     alias libraryExists = library => path.buildPath(library).exists;
759 
760     enforce(libraries.all!(libraryExists), errorMessage);
761 }
762 
763 /**
764  * Searches the `PATH` environment variable for the given filename.
765  *
766  * Params:
767  *  filename = the filename to search for in the `PATH`
768  *
769  * Return: the full path to the given filename if found, otherwise `null`
770  */
771 string searchPath(string filename)
772 {
773     auto path =
774         environment.get("PATH", "").
775         split(':').
776         map!(path => path.buildPath(filename)).
777         find!(exists);
778 
779     return path.empty ? null : path.front;
780 }