1 module pkg_config;
2 
3 auto pkgConfig(in string lib, in string minVersion=null)
4 {
5     PkgConfig pc;
6     pc._lib = lib;
7     pc._minVersion = minVersion;
8     return pc;
9 }
10 
11 version (DubPR1453) {
12 
13     bool pkgConfigDubLines(in string lib, in string minVersion=null)
14     {
15         try {
16             auto pc = pkgConfig(lib, minVersion)
17                 .libs()
18                 .msvc();
19             auto library = pc.invoke();
20             library.echoDubLines();
21             return true;
22         }
23         catch (Exception ex) {
24             import std.stdio : stderr;
25             stderr.writeln(ex.msg);
26             return false;
27         }
28     }
29 
30     version(linux) unittest {
31         import std.exception : assertNotThrown;
32         assert(pkgConfigDubLines("libpng", "1.6.0"));
33         assert(!pkgConfigDubLines("libpng", "99.0"));
34     }
35 
36 }
37 
38 
39 class LibraryNotFound : Exception {
40     this (string msg) {
41         super(msg);
42     }
43 }
44 
45 struct Library
46 {
47     import std.stdio : File, stdout;
48 
49     /// the name of the library
50     string name;
51     /// the version of the library
52     string ver;
53     /// all C flags verbatim
54     string[] cflags;
55     /// include paths to look for headers
56     string[] includePaths;
57     /// preprocessor definitions
58     string[] defines;
59     /// C flags that are not include paths or defines
60     string[] otherCFlags;
61     /// all link flags verbatim
62     string[] lflags;
63     /// libraries search paths
64     string[] libPaths;
65     /// libraries to link
66     string[] libs;
67     /// other link flags that are not searchs path and not libraries
68     string[] otherLFlags;
69 
70     version (DubPR1453) void echoDubLines(File f=stdout)
71     {
72         import std.algorithm : map;
73         import std.array : join;
74         import std.format : format;
75 
76         if (libs.length)
77             f.writeln(
78                 "dub:sdl:libs ",
79                 libs.map!(l => format("\"%s\"", l)).join(" ")
80             );
81         if (libPaths.length)
82             f.writeln(
83                 "dub:sdl:lflags ",
84                 libPaths.map!(p => format("\"%s\"", p)).join(" ")
85             );
86         if (otherLFlags)
87             f.writeln(
88                 "dub:sdl:lflags ",
89                 otherLFlags.map!(f => format("\"%s\"", f)).join(" ")
90             );
91     }
92 
93     private void parseCFlags(string flagStr)
94     {
95         import std.algorithm : startsWith;
96 
97         const flags = splitFlags(flagStr);
98 
99         cflags ~= flags;
100 
101         foreach (f; flags) {
102             if (f.startsWith("-I")) {
103                 includePaths ~= f[2 .. $];
104             }
105             else if (f.startsWith("-D")) {
106                 defines ~= f[2 .. $];
107             }
108             else {
109                 otherCFlags ~= f;
110             }
111         }
112     }
113 
114     private void parseLFlags(string flagStr)
115     {
116         import std.algorithm : startsWith;
117 
118         const flags = splitFlags(flagStr);
119 
120         lflags ~= flags;
121 
122         foreach (f; flags) {
123             if (f.startsWith("-L")) {
124                 libPaths ~= f[2 .. $];
125             }
126             else if (f.startsWith("-l")) {
127                 libs ~= f[2 .. $];
128             }
129             else {
130                 otherLFlags ~= f;
131             }
132         }
133     }
134 }
135 
136 struct PkgConfig
137 {
138     private string _lib;
139     private string _minVersion;
140     private string _exe = defaultExe;
141     private string[] _pkgConfigPath;
142     private bool _cflags;
143     private bool _libs;
144     private bool _systemLibs;
145     private bool _static;
146     version(Windows) private bool _msvc;
147 
148     PkgConfig exe(in string exe) {
149         _exe = exe;
150         return this;
151     }
152     PkgConfig pkgConfigPath(in string path) {
153         _pkgConfigPath = [ path ];
154         return this;
155     }
156     PkgConfig pkgConfigPath(in string[] paths) {
157         _pkgConfigPath = paths.dup;
158         return this;
159     }
160     PkgConfig cflags() {
161         _cflags = true;
162         return this;
163     }
164     PkgConfig libs() {
165         _libs = true;
166         return this;
167     }
168     PkgConfig systemLibs() {
169         _systemLibs = true;
170         return this;
171     }
172     PkgConfig staticLib() {
173         _static = true;
174         return this;
175     }
176     PkgConfig msvc() {
177         version(Windows) {
178             _msvc = true;
179         }
180         return this;
181     }
182 
183     Library invoke()
184     {
185         auto env = buildCmdEnv();
186 
187         // checking for package support
188         run!LibraryNotFound(["--exists"], env);
189 
190         Library lib;
191         lib.name = _lib;
192         lib.ver = run!Exception(["--modversion"], env);
193         if (_cflags) {
194             lib.parseCFlags(run!Exception(["--cflags"], env));
195         }
196         if (_libs) {
197             lib.parseLFlags(run!Exception(["--libs"], env));
198         }
199         return lib;
200     }
201 
202     private string[string] buildCmdEnv()
203     {
204         string[string] env;
205         if (_pkgConfigPath) {
206             import std.array : join;
207             import std.process : environment;
208             string path = _pkgConfigPath.join(pathEnvSep);
209             const curVal = environment.get("PKG_CONFIG_PATH", null);
210             if (curVal) {
211                 path ~= pathEnvSep ~ curVal;
212             }
213             env["PKG_CONFIG_PATH"] = path;
214         }
215         if (_systemLibs) {
216             env["PKG_CONFIG_ALLOW_SYSTEM_LIBS"] = "1";
217         }
218         return env;
219     }
220 
221     private string run (Ex = Exception)(string[] args, string[string] env)
222     {
223         import std.array : join;
224         import std.process : Config, pipe, spawnProcess, wait;
225         import std.stdio : stdin, stderr;
226         import std..string : strip;
227 
228         auto p = pipe();
229         auto cmdArgs = [ _exe, "--print-errors", "--errors-to-stdout"];
230         if (_static) {
231             cmdArgs ~= "--static";
232         }
233         version(Windows) if (_msvc) {
234             cmdArgs ~= "--msvc-syntax";
235         }
236         cmdArgs ~= args;
237         if (_minVersion) {
238             cmdArgs ~= (_lib ~ " >= " ~ _minVersion);
239         }
240         else {
241             cmdArgs ~= _lib;
242         }
243         auto pid = spawnProcess(cmdArgs, stdin, p.writeEnd, stderr, env);
244         string output = p.readEnd.byLine().join("\n").strip().idup;
245         if (wait(pid)) {
246             throw new Ex(output);
247         }
248         return output;
249     }
250 }
251 
252 private:
253 
254 string[] splitFlags(string flagStr) pure
255 {
256     import std.uni : isSpace;
257     string[] flags;
258     string flag;
259     bool escaped;
260     foreach(c; flagStr) {
261         if (escaped) {
262             escaped = false;
263             flag ~= c;
264         }
265         else {
266             if (c == '\\') {
267                 escaped = true;
268             }
269             else if (isSpace(c)) {
270                 if (flag.length) {
271                     flags ~= flag;
272                     flag.length = 0;
273                 }
274             }
275             else {
276                 flag ~= c;
277             }
278         }
279     }
280     if (flag.length) flags ~= flag;
281     return flags;
282 }
283 
284 version(Windows) {
285     enum exeExt = ".exe";
286     enum pathEnvSep = ";";
287 }
288 else {
289     enum exeExt = "";
290     enum pathEnvSep = ":";
291 }
292 
293 enum defaultExe = "pkg-config" ~ exeExt;