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