-
Notifications
You must be signed in to change notification settings - Fork 2
/
pkgconfig.c
563 lines (504 loc) · 18.9 KB
/
pkgconfig.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
// A re-implementation of pkg-config using llib.
// This has no external dynamic dependencies other than libc!
// Steve Donovan, c 2015
// BSD licence
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <llib/str.h>
#include <llib/template.h>
#include <llib/value.h>
#include <llib/file.h>
#include <llib/array.h>
#include <llib/arg.h>
static str_t subst (str_t s, char*** vars);
static void seq_adda_unique (str_t **ss, str_t *extra);
static str_t seq_concat(str_t **ss);
#define MAX_PATH 256
#define LINE_SIZE 1024
// pkg-config looks a package NAME as a file NAME.pc on its package path
static str_t default_paths[] = {
"/usr/lib/pkgconfig","/usr/share/pkgconfig",
"/usr/lib/x86_64-linux-gnu/pkgconfig", // <- configure!
"/usr/lib/i386-linux-gnu/pkgconfig",
"/usr/local/lib/pkgconfig","usr/local/share/pkgconfig"
};
static str_t *paths;
static bool verbose;
// Like with executable paths, you may add to the package path with the
// environment variable `PKG_CONFIG_PATH`, separated with colons on Unix
// and semi-colons on Windows.
static void init_paths() {
if (! paths) {
paths = array_new_copy(str_t,default_paths,sizeof(default_paths)/sizeof(str_t));
str_t extra = getenv("PKG_CONFIG_PATH");
if (extra) {
str_t *old = paths;
char **extra_entries = str_split(extra,":");
str_t **ss = seq_new_array(paths);
seq_adda(ss,extra_entries,-1);
paths = (str_t*)seq_array_ref(ss);
unref(old);
unref(extra_entries);
}
}
}
str_t search_pkg_path(str_t name) {
char path[MAX_PATH];
FOR(i,array_len(paths)) {
snprintf(path,MAX_PATH,"%s/%s.pc",paths[i],name);
if (file_exists(path,"r")) {
return str_new(path);
}
}
return NULL; // no can do
}
void kount() { fprintf(stderr,"kount = %d\n",obj_kount()); }
// pkg-config's --list-all option lists all .pc files found on the system, with their descriptions
// So we go through the package path and make an array containing the full path of all .pc files found.
void list_pkg_modules() {
char mask[MAX_PATH];
str_t **ss = seq_new_ref(str_t);
FOR(i,array_len(paths)) {
snprintf(mask,MAX_PATH,"%s/*.pc",paths[i]);
char **in_dir = file_files_in_dir(mask,false);
if (in_dir && array_len(in_dir) > 0) {
seq_adda (ss, in_dir, -1);
unref(in_dir);
}
}
str_t *files = (str_t*) seq_array_ref(ss);
FOR(i,array_len(files)) {
FILE *f = fopen(files[i],"r");
char* name = file_basename(files[i]);
// semi-barbaric method of removing .pc extension
name[strlen(name)-3] = '\0';
while (file_gets(f,mask,MAX_PATH)) {
int idx = 0;
if (str_contains(mask,"Description: ",&idx)) {
printf("%s\t%s\n",name,mask+idx);
break;
}
}
unref(name);
fclose(f);
}
unref(files);
}
//////// Parsing .pc files /////////
// This is relatively straightforward - the delicate stuff involves checking versioned dependencies
// and bringing in their headers and libraries.
typedef struct PkgConfig_ {
str_t pc;
str_t filename;
str_t name;
str_t description;
str_t URL;
str_t version;
// these are resizeable arrays (seqs) - pointers to arrays of strings.
str_t **libs;
str_t **cflags;
str_t **libs_private;
char ***vars;
} PkgConfig;
// Any llib object may have a dispose function. Fairly tedious, but at least
// we don't have to worry about exactly how to dispose of various objects!
static void PkgConfig_dispose(PkgConfig *pkg) {
unref(pkg->pc);
unref(pkg->filename);
unref(pkg->name);
unref(pkg->description);
unref(pkg->URL);
unref(pkg->version);
unref(pkg->libs);
unref(pkg->cflags);
unref(pkg->libs_private);
unref(pkg->vars);
}
// when processing keyword lines, we first do any variable substitution.
// Then split into words and append to the array.
void subst_and_split(str_t **ss, str_t v, char*** vars) {
str_t s = subst(v,vars);
char **res = str_split(s," ");
seq_adda(ss, res, -1);
unref(res);
unref(s);
}
static char ***pkg_map;
static void intialize_pkg_map() {
pkg_map = smap_new(true);
}
static str_t process_pkg_requires (PkgConfig *pkg, char *s, bool is_private);
// a note on error handling: pkg-config is traditionally a silent program that only emits error messages
// on request. No printing to the error stream takes place here outside main()! `read_pkg` returns an _error value_
// when it's unhappy. These are basically C strings but are labeled with a different dynamic type, so
// that `value_is_error` is true. In this way, our function can return a PkgConfig object if successful,
// or return an error value.
PkgConfig *read_pkg (str_t pc) {
char line [LINE_SIZE];
str_t err = NULL;
// cache package objects based on their package name.
PkgConfig *parsed = smap_get(pkg_map, pc);
if (parsed) {
return parsed;
}
str_t filename = search_pkg_path(pc);
if (! filename) { // we have searched in vain...
return (PkgConfig*)value_errorf("Cannot find package '%s'",pc);
}
// note the indirection! We use this llib function because it returns a useful error when it fails,
// and the resulting file object is closed when it is unref'd.
FILE** f = file_fopen(filename,"r");
if (value_is_error(f)) {
return (PkgConfig*)f;
}
char*** vars = smap_new(true);
if (verbose)
fprintf(stderr,"loading %s\n",pc);
PkgConfig *pkg = obj_new(PkgConfig,PkgConfig_dispose);
memset(pkg,0,sizeof(PkgConfig));
pkg->filename = filename;
pkg->vars = vars;
pkg->libs = seq_new_ref(str_t);
pkg->cflags = seq_new_ref(str_t);
pkg->libs_private = seq_new_ref(str_t);
pkg->pc = str_ref(pc);
smap_add(pkg_map,str_ref(pc),pkg);
while (file_gets(*f,line,LINE_SIZE)) {
// get rid of comments and blank lines
char *p = strchr(line,'#');
if (p) *p = '\0';
str_trim(line);
if (*line == '\0') continue;
// variables are immediately followed by '=' and their value.
// All values are passed through variable substitution in terms of previously
// defined variables
p = strchr(line,'=');
if (p && isalnum(*(p-1))) {
*p++ = '\0';
// simple linear lookup map does fine for us here...
smap_put(vars,str_new(line),subst(p,vars));
continue;
}
// keyword values: these are words followed by a colon, either followed by a space
// and some text or nothing.
p = strchr(line,':');
if (p && (*(p+1) == ' ' || *(p+1) == '\0')) {
*p++ = '\0';
if (! *p)
continue;
str_t keyword = line;
str_t value = str_new(p+1) ;
if (str_eq(keyword, "Name")) {
pkg->name = ref(value);
} else
if (str_eq(keyword, "Description")) {
pkg->description = ref(value);
} else
if (str_eq(keyword, "Version")) {
pkg->version = ref(value);
} else
if (str_eq(keyword, "Requires") || str_eq(keyword, "Requires.private")) {
char *s = (char*)subst(value,pkg->vars);
err = process_pkg_requires(pkg,s, str_eq(keyword, "Requires.private"));
unref(s);
if (err)
break;
} else
if (str_eq(keyword, "Conflicts")) {
puts("NYI");
} else
if (str_eq(keyword, "Libs")) {
subst_and_split(pkg->libs,value,vars);
} else
if (str_eq(keyword, "Libs.private")) {
subst_and_split(pkg->libs_private,value,vars);
} else
if (str_eq(keyword, "Cflags")) {
subst_and_split(pkg->cflags,value,vars);
} else
if (str_eq(keyword, "URL")) {
pkg->URL = ref(value);
} else {
err = value_errorf("%s: unknown keyword '%s'\n",filename,keyword);
break;
}
unref(value);
} else {
err = value_errorf("%s: garbage line '%s'\n",filename,line);
break;
}
}
// These fields are compulsory, so it's an immediate error
if (! err && ! (pkg->name && pkg->description && pkg->version)) {
err = value_errorf("%s: must have Name, Description and Version fields",filename);
}
unref(f); // closes the underlying FILE*
if (err) {
unref(pkg);
return (PkgConfig*)err;
} else {
return pkg;
}
}
// How to compare version strings: the basic idea is to treat them as numbers where
// the integers between the periods are base 1024 'digits'. So 1.2 is 1024*1 + 2.
// The shenanigans with lmax are needed because we must have e.g. 1.10 > 1.1.92 !
static bool version_pkg_matches(str_t found_vs, str_t needed_vs, str_t op) {
char **parts1 = str_split(found_vs,".");
char **parts2 = str_split(needed_vs,".");
int l1 = array_len(parts1), l2 = array_len(parts2);
int lmax = l1;
uint64_t res1 = 0, res2 = 0, power = 1;
if (l2 > l1) lmax = l2;
for (int i = lmax-1; i >= 0; i--) {
if (i < l2)
res2 += atoi(parts2[i])*power;
if (i < l1)
res1 += atoi(parts1[i])*power;
power *= 1024;
}
unref(parts1);
unref(parts2);
uint64_t found = res1, needed = res2;
if (str_eq(op,"=")) {
return found == needed;
} else
if (str_eq(op,">=")) {
return found >= needed;
} else
if (str_eq(op,"<=")) {
return found <= needed;
} else {
return false;
}
}
// this is the tricky heart of the problem - any package may require other packages
// which may need to satisfy a version constraint, e.g. boo >= 0.9.
// We 'inherit' compile and link flags from required packages.
//
// The manual, although well done and comprehensive, is not a full description of the actual
// behaviour of pkg-config. For instance, pkg-config lets 'private' packages add to include path,
// to accomodate some upstream packages. (see https://github.com/NixOS/nixpkgs/issues/292)
// Also, although the manual clearly says that required items are comma-separated, this isn't
// true in practice. So we ignore commas and parse space-separated tokens. In the require line
// 'boo foo >= 1.1 bar' it's clear that 'boo' and 'bar' have no version constraint, since they are
// not followed by a relational operator.
static str_t process_pkg_requires (PkgConfig *pkg, char *s, bool is_private) {
str_t err = NULL;
// not _always_ comma-separated! So get rid of commas!
FORAP(s, if(*_ == ',') *_ = ' ');
// and go through space-separated tokens
char **parts = str_split(s," ");
char **P = parts;
while (*P) {
str_t op = NULL, vs = NULL, package = P[0];
// PACKAGE OPERATOR VERSION?
if (P[1] && str_find_first_of(P[1],"=<>") != -1) {
op = P[1];
vs = P[2];
P += 2;
}
bool seen = smap_get(pkg_map, package) != NULL;
if (verbose && pkg != NULL) {
fprintf(stderr,"%s requires %s %s\n",pkg->pc,package, seen ? "SEEN" : "");
}
if (seen) { // don't bother if we've already seen this...the flags have already been included
++P;
continue;
}
// read the required package - may not succeed!
PkgConfig *required_pkg = read_pkg(package);
if (value_is_error(required_pkg)) {
err = (str_t)required_pkg;
} else // and even so, does it match required version?
if (op && ! version_pkg_matches(required_pkg->version,vs,op)) {
if (pkg != NULL) {
err = value_errorf("%s required %s (%s), version mismatch '%s %s %s'",
pkg->pc,required_pkg->pc,vs,
required_pkg->version,op,vs);
} else {
err = value_errorf("version %s %s %s of %s required",
required_pkg->version,op,vs,
required_pkg->pc);
}
}
if (err) {
return err;
}
if (pkg != NULL) {
// finally may add any compile or link flags to the current package's flags, if they exist!
// To prevent nonsensically long output, only add if not already present.
if (! is_private) {
seq_adda_unique(pkg->cflags, *required_pkg->cflags);
seq_adda_unique(pkg->libs, *required_pkg->libs);
if (verbose)
fprintf(stderr,"updated public %s from %s\n",pkg->pc, required_pkg->pc);
} else {
seq_adda_unique(pkg->libs, *required_pkg->libs_private);
seq_adda_unique(pkg->cflags, *required_pkg->cflags);
if (verbose)
fprintf(stderr,"updated private %s from %s\n",pkg->pc, required_pkg->pc);
}
}
++P;
}
unref(parts);
return NULL;
}
void print_pkg_variables(PkgConfig *pkg) {
for (char **p = *pkg->vars; *p; p+=2) {
printf("%s\n",*p);
}
}
void print_pkg_var_value(PkgConfig *pkg, str_t var) {
str_t v = smap_get(pkg->vars,var);
printf("%s\n",v ? v : "");
}
void set_pkg_var_value(PkgConfig *pkg, str_t var, str_t val) {
smap_put(pkg->vars,var,val);
}
// llib has a somewhat unconvential way of parsing command-line arguments, based on
// the idea that program usage (which you ought always write) should be parseable. This is
// obviously easier in a dynamic language (e.g. the lapp Lua library in Penlight) but
// here the old dog C shows its surprising flexibility.
//
// The `arg` module is fed an array of argument specifications; usually the spec is
// followed by a pointer to the variable which we wish to _bind_ that argument to.
// Boolean flags aren't followed by values, and if a flag has a default then it
// will not be a _required_ flag. Array flags (with '[]') may be used multiple times.
static bool modversion;
static bool print_errors;
static bool cflags;
static bool libs;
static bool ppath;
static bool print_variables;
static bool list_all;
static str_t variable, define_variable;
static str_t *exists;
static str_t* packages;
void* args[] = {
"// pkgconfig: show build information from .pc files",
"bool modversion; // show version information of packages",&modversion,
"bool print-errors; // verbose errors (normally silent)",&print_errors,
"bool cflags; // flags needed to compile against packages",&cflags,
"bool libs; // flags needed to link against packages",&libs,
"bool print-variables; // print all variables defined in packages",&print_variables,
"bool list-all; // list all modules found on package path",&list_all,
"string variable=''; // value of variable defined in package's .pc file",&variable,
"string exists[]; // does package exist with possible required version?",&exists,
"string define_variable=''; // overide variable value in package's .pc file",&define_variable,
"bool path; // just show full paths of .pc files",&ppath,
"string #1[]; // packages",&packages,
NULL
};
int main(int argc, const char **argv)
{
ArgState *as = arg_command_line(args,argv);
init_paths();
verbose = getenv("PKG_CONFIG_DEBUG_SPEW") != NULL;
if (verbose)
print_errors = true;
intialize_pkg_map();
if (list_all) {
list_pkg_modules();
return 0;
}
// --exists is either followed by a package name, or a constraint like 'foo >= 1.0';
// there may be multiple such.
if (array_len(exists) > 0) {
bool ok = true;
FOR (i, array_len(exists)) {
str_t arg = exists[i];
void *res;
if (strchr(arg,' ') != NULL) {
res = (void*)process_pkg_requires(NULL,str_new(arg),false);
} else {
res = read_pkg(arg);
}
ok = ok && ! value_is_error(res);
}
return ok ? 0 : 1;
}
if (array_len(packages)==0) {
arg_quit(as,"must provide packages, unless --list-all or --exists",true);
}
bool output = false;
//kount();
FOR (i,array_len(packages)) {
str_t package = packages[i];
if (ppath) { // this is a useful extension - equivalent to 'which' for .pc files!
str_t filename = search_pkg_path(package);
if (filename) {
puts(filename);
} else {
return 1;
}
continue;
}
PkgConfig *pkg = read_pkg(package);
if (value_is_error(pkg)) {
if (print_errors)
fprintf(stderr,"error: %s\n",(str_t)pkg);
return 1;
}
if (*define_variable) {
str_t *parts = (str_t*)str_split(define_variable,"=");
set_pkg_var_value(pkg,parts[0],parts[1]);
unref(parts);
}
if (*variable) {
print_pkg_var_value(pkg,variable);
} else
if (print_variables) {
print_pkg_variables(pkg);
} else {
if (cflags) {
str_t flagstr = seq_concat(pkg->cflags);
printf("%s ",flagstr);
unref(flagstr);
output = true;
}
if (libs) {
str_t flagstr = seq_concat(pkg->libs);
printf("%s ",flagstr);
unref(flagstr);
output = true;
}
}
}
unref(pkg_map);
//kount();
if (output) {
printf("\n");
fflush(stdout);
}
return 0;
}
////// Some useful utilities
// we use llib's template support to do the necessary ${var} substitutions.
// The variables are a 'simple map', (which is a pointer to an array of strings where
// odd entries are keys and even entries are values)
static str_t subst (str_t s, char*** vars) {
StrTempl *T = str_templ_new(s,"${}");
str_t res = str_templ_subst_using(T,(StrLookup)smap_get,vars);
unref(T);
return res;
}
// seq_adda adds an array at the end of a seq - this specialised version
// does not add duplicated strings.
static void seq_adda_unique (str_t **ss, str_t *extra) {
for (str_t *P = extra; *P; ++P) {
str_t s = *P;
// but have we already seen this flag?
FINDA(idx,*ss,0,str_eq(_,s));
if (idx == -1) {
seq_add(ss,ref(s));
}
}
}
// this conveniently does the dereferencing (seqs are pointers to arrays) and makes a
// string with items separated by spaces.
static str_t seq_concat(str_t **ss) {
return str_concat((char**)*ss," ");
}