Created
April 4, 2026 08:50
-
-
Save unstabler/16413f60851484a3b6048dfd015975ab to your computer and use it in GitHub Desktop.
ppkg-config.bat - Python Core-Only replacement for pkg-config
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @echo off & python -x "%~f0" %* & exit /b %errorlevel% | |
| # ----------------------------------------------------------------------------- | |
| # ppkg-config.bat - Python Core-Only replacement for pkg-config | |
| # | |
| # Python 3 port implemented by Gemini 3.1 Pro | |
| # | |
| # Original Perl Implementation: | |
| # Copyright (C) 2012 M. Nunberg. | |
| # You may use and distribute this software under the same terms and | |
| # conditions as Perl itself (Artistic License / GPL). | |
| # ----------------------------------------------------------------------------- | |
| import sys | |
| import os | |
| import re | |
| import shlex | |
| import argparse | |
| from glob import glob | |
| from collections import OrderedDict | |
| VERSION = '0.26026' | |
| COMPAT_VERSION = VERSION[-2:] | |
| # ----------------------------------------------------------------------------- | |
| # Sane Defaults | |
| # ----------------------------------------------------------------------------- | |
| DEFAULT_SEARCH_PATH = [ | |
| '/usr/local/lib/pkgconfig', '/usr/local/share/pkgconfig', | |
| '/usr/lib/pkgconfig', '/usr/share/pkgconfig' | |
| ] | |
| DEFAULT_EXCLUDE_CFLAGS = ['-I/usr/include', '-I/usr/local/include'] | |
| DEFAULT_EXCLUDE_LFLAGS = [] | |
| for base in ['/lib', '/lib32', '/lib64', '/usr/lib', '/usr/lib32', '/usr/lib64', '/usr/local/lib']: | |
| DEFAULT_EXCLUDE_LFLAGS.extend([f"-L{base}", f"-R{base}"]) | |
| if os.environ.get('PKG_CONFIG_NO_OS_CUSTOMIZATION'): | |
| pass | |
| elif os.environ.get('PKG_CONFIG_LIBDIR'): | |
| DEFAULT_SEARCH_PATH = os.environ['PKG_CONFIG_LIBDIR'].split(os.pathsep) | |
| else: | |
| platform = sys.platform | |
| if platform == 'win32': | |
| DEFAULT_EXCLUDE_CFLAGS = [] | |
| DEFAULT_EXCLUDE_LFLAGS = [] | |
| elif platform == 'darwin': | |
| if os.path.exists('/usr/local/Homebrew/bin/brew'): | |
| DEFAULT_SEARCH_PATH.extend(glob('/usr/local/opt/*/lib/pkgconfig')) | |
| env_search_path = os.environ.get('PKG_CONFIG_PATH', '').split(os.pathsep) if os.environ.get('PKG_CONFIG_PATH') else [] | |
| DEFAULT_SEARCH_PATH = env_search_path + [p for p in DEFAULT_SEARCH_PATH if p] | |
| if os.environ.get('PKG_CONFIG_ALLOW_SYSTEM_CFLAGS'): | |
| DEFAULT_EXCLUDE_CFLAGS = [] | |
| if os.environ.get('PKG_CONFIG_ALLOW_SYSTEM_LIBS'): | |
| DEFAULT_EXCLUDE_LFLAGS = [] | |
| # ----------------------------------------------------------------------------- | |
| # PkgConfig Core Class | |
| # ----------------------------------------------------------------------------- | |
| class PkgConfig: | |
| def __init__(self, search_path=None, use_static=False, uservars=None, exclude_cflags=None, exclude_ldflags=None, no_recurse=False): | |
| self.search_path = search_path if search_path is not None else DEFAULT_SEARCH_PATH | |
| self.use_static = use_static | |
| self.no_recurse = no_recurse | |
| self.exclude_cflags = exclude_cflags if exclude_cflags is not None else DEFAULT_EXCLUDE_CFLAGS | |
| self.exclude_ldflags = exclude_ldflags if exclude_ldflags is not None else DEFAULT_EXCLUDE_LFLAGS | |
| self.cflags = [] | |
| self.ldflags = [] | |
| self.libs_deplist = {} | |
| self.recursion_level = 0 | |
| self.filevars = {} | |
| self.uservars = uservars if uservars else {} | |
| self.defined_variables = {} | |
| self.pkg_exists = False | |
| self.pkg_version = "" | |
| self.pkg_url = "" | |
| self.pkg_description = "" | |
| self.errmsg = "" | |
| def _expand_vars(self, value): | |
| def replacer(match): | |
| var_name = match.group(1) | |
| val = self.filevars.get(var_name, "") | |
| return " ".join(val) if isinstance(val, list) else val | |
| out_value = value | |
| while re.search(r'\$\{([a-zA-Z0-9_.]+)\}', out_value): | |
| out_value = re.sub(r'\$\{([a-zA-Z0-9_.]+)\}', replacer, out_value) | |
| out_value = out_value.replace('$$', '$') | |
| return out_value | |
| def assign_var(self, field, value, force=False): | |
| if not force and field in self.uservars: | |
| return | |
| if field.endswith(('dir', 'prefix')) and '$' not in value: | |
| self.filevars[field] = [v for v in shlex.split(value) if v] | |
| return | |
| expanded = self._expand_vars(value) | |
| self.filevars[field] = [v for v in shlex.split(expanded) if v] | |
| def parse_line(self, line): | |
| line = line.split('#')[0].strip() | |
| if not line: return | |
| match = re.search(r'([=:])', line) | |
| if not match: return | |
| tok = match.group(1) | |
| parts = line.split(tok, 1) | |
| if len(parts) != 2: return | |
| field, value = parts[0].strip(), parts[1].strip() | |
| if tok == '=': | |
| self.defined_variables[field] = value | |
| field = field.lower() | |
| if tok == ':' and not field.startswith(('cflags', 'libs')): | |
| value = self._expand_vars(value) | |
| self.filevars[field] = [v for v in value.split() if v] | |
| return | |
| field = field.replace("'", "").replace('"', "") | |
| self.assign_var(field, value) | |
| def get_requires(self, requires_str): | |
| if not requires_str: return [] | |
| reqlist = requires_str.replace(',', ' ').split() | |
| ret = [] | |
| while reqlist: | |
| req = reqlist.pop(0) | |
| cmp_op = want = None | |
| match = re.search(r'([<>=]+)', req) | |
| if match: | |
| cmp_op = match.group(1) | |
| if req.endswith(cmp_op): | |
| want = reqlist.pop(0) if reqlist else "" | |
| else: | |
| want = req.split(cmp_op)[-1] | |
| req = req.split(cmp_op)[0] | |
| elif reqlist and re.match(r'[<>=]+', reqlist[0]): | |
| cmp_op = reqlist.pop(0) | |
| want = reqlist.pop(0) if reqlist else "" | |
| reqlet = [req] | |
| if cmp_op: | |
| reqlet.extend([cmp_op, want]) | |
| ret.append(reqlet) | |
| return ret | |
| def parse_pcfile(self, pcfile): | |
| try: | |
| with open(pcfile, 'r', encoding='utf-8') as f: | |
| text = f.read().replace('\\\n', '').replace('\\\r\n', '') | |
| except IOError as e: | |
| self.errmsg = f"{pcfile}: {e}" | |
| return | |
| for k, v in self.uservars.items(): | |
| self.assign_var(k, v, force=True) | |
| lines = text.splitlines() | |
| pcfiledir = os.path.dirname(pcfile).replace('\\', '/') | |
| self.parse_line(f"pcfiledir={pcfiledir}") | |
| for line in lines: | |
| self.parse_line(line) | |
| self.cflags.extend(self.filevars.get('cflags', [])) | |
| if self.use_static: | |
| self.cflags.extend(self.filevars.get('cflags.private', [])) | |
| libs = self.filevars.get('libs', []) | |
| if libs: | |
| self.libs_deplist.setdefault(self.recursion_level, []).extend(libs) | |
| if self.use_static: | |
| libs_priv = self.filevars.get('libs.private', []) | |
| if libs_priv: | |
| self.libs_deplist.setdefault(self.recursion_level, []).extend(libs_priv) | |
| deps = self.get_requires(" ".join(self.filevars.get('requires', []))) | |
| if self.use_static: | |
| deps.extend(self.get_requires(" ".join(self.filevars.get('requires.private', [])))) | |
| if self.recursion_level == 1 and not self.pkg_exists: | |
| self.pkg_version = " ".join(self.filevars.get('version', [])) | |
| self.pkg_url = " ".join(self.filevars.get('url', [])) | |
| self.pkg_description = " ".join(self.filevars.get('description', [])) | |
| self.pkg_exists = True | |
| if not self.no_recurse: | |
| for dep_item in deps: | |
| dep_name = dep_item[0] | |
| other = PkgConfig( | |
| search_path=self.search_path, use_static=self.use_static, | |
| uservars=self.uservars, exclude_cflags=self.exclude_cflags, | |
| exclude_ldflags=self.exclude_ldflags | |
| ) | |
| other.recursion_level = self.recursion_level + 1 | |
| other.find_pcfile(dep_name) | |
| if other.errmsg: | |
| self.errmsg = other.errmsg | |
| break | |
| self.cflags.extend(other.get_cflags_raw()) | |
| for level, libs in other.libs_deplist.items(): | |
| self.libs_deplist.setdefault(level, []).extend(libs) | |
| def find_pcfile(self, libname): | |
| self.recursion_level += 1 | |
| pcfile = f"{libname}.pc" | |
| found_path = None | |
| for path in self.search_path: | |
| full_path = os.path.join(path, pcfile) | |
| if os.path.exists(full_path): | |
| found_path = full_path | |
| break | |
| if not found_path: | |
| if not self.errmsg: | |
| self.errmsg = f"Can't find {pcfile} in any of {self.search_path}\nuse the PKG_CONFIG_PATH environment variable." | |
| self.recursion_level -= 1 | |
| return | |
| self.parse_pcfile(found_path) | |
| self.recursion_level -= 1 | |
| def get_cflags_raw(self): | |
| return self.cflags | |
| def get_cflags(self): | |
| filtered = [f for f in self.cflags if f not in self.exclude_cflags] | |
| return list(OrderedDict.fromkeys(filtered)) | |
| def get_ldflags(self): | |
| ordered_libs = [] | |
| for level in sorted(self.libs_deplist.keys()): | |
| lcopy = self.libs_deplist[level] | |
| lcopy = [l for l in lcopy if l not in self.exclude_ldflags] | |
| ordered_libs.extend(list(OrderedDict.fromkeys(lcopy))) | |
| ordered_libs.reverse() | |
| ordered_libs = list(OrderedDict.fromkeys(ordered_libs)) | |
| ordered_libs.reverse() | |
| return ordered_libs | |
| # ----------------------------------------------------------------------------- | |
| # Main CLI Logic | |
| # ----------------------------------------------------------------------------- | |
| def main(): | |
| parser = argparse.ArgumentParser(description="Python Core-Only replacement for pkg-config") | |
| parser.add_argument('libraries', nargs='*', help='Library names to query') | |
| parser.add_argument('--libs', action='store_true', help='Output linker flags') | |
| parser.add_argument('--libs-only-L', action='store_true', help='Output -L linker flags only') | |
| parser.add_argument('--libs-only-l', action='store_true', help='Output -l linker flags only') | |
| parser.add_argument('--cflags', action='store_true', help='Output compiler flags') | |
| parser.add_argument('--cflags-only-I', action='store_true', help='Output -I compiler flags only') | |
| parser.add_argument('--static', action='store_true', help='Use static dependencies') | |
| parser.add_argument('--exists', action='store_true', help='Return 0 if the package exists') | |
| parser.add_argument('--modversion', action='store_true', help='Print the version of the package') | |
| parser.add_argument('--version', action='store_true', help='Print emulated pkg-config version') | |
| parser.add_argument('--real-version', action='store_true', help='Print actual script version') | |
| parser.add_argument('--with-path', action='append', default=[], help='Prepend to search paths') | |
| parser.add_argument('--define-variable', action='append', default=[], help='Define a variable') | |
| parser.add_argument('--variable', help='Return the value of a variable') | |
| parser.add_argument('--print-variables', action='store_true', help='Print all defined variables') | |
| parser.add_argument('--print-errors', action='store_true', help='Print errors to stderr') | |
| parser.add_argument('--silence-errors', action='store_true', help='Turn off errors') | |
| args = parser.parse_args() | |
| if args.version: | |
| print(f"0.{COMPAT_VERSION}") | |
| sys.exit(0) | |
| if args.real_version: | |
| print(f"ppkg-config.py - cruftless pkg-config\nVersion: {VERSION}") | |
| sys.exit(0) | |
| if not args.libraries and not args.print_variables and not args.variable: | |
| parser.print_help() | |
| sys.exit(1) | |
| uservars = {} | |
| for var in args.define_variable: | |
| if '=' in var: | |
| k, v = var.split('=', 1) | |
| uservars[k.strip()] = v.strip() | |
| search_paths = args.with_path + DEFAULT_SEARCH_PATH | |
| pkg = PkgConfig( | |
| search_path=search_paths, | |
| use_static=args.static, | |
| uservars=uservars, | |
| no_recurse=(args.exists or args.modversion) | |
| ) | |
| for lib in args.libraries: | |
| pkg.recursion_level = 0 | |
| pkg.find_pcfile(lib) | |
| if pkg.errmsg: | |
| if args.print_errors and not args.silence_errors: | |
| sys.stderr.write(pkg.errmsg + "\n") | |
| sys.exit(1) | |
| if args.print_variables: | |
| for k, v in pkg.defined_variables.items(): | |
| print(f"{k}={v}") | |
| if args.variable: | |
| val = pkg.filevars.get(args.variable, []) | |
| print(" ".join(val) if isinstance(val, list) else val) | |
| if args.modversion: | |
| print(pkg.pkg_version) | |
| sys.exit(0) | |
| want_flags = args.libs or args.libs_only_L or args.libs_only_l or args.cflags or args.cflags_only_I | |
| if not want_flags: | |
| sys.exit(0) | |
| print_flags = [] | |
| if args.cflags: | |
| print_flags.extend(pkg.get_cflags()) | |
| if args.cflags_only_I: | |
| print_flags.extend([f for f in pkg.get_cflags() if f.startswith('-I')]) | |
| if args.libs: | |
| print_flags.extend(pkg.get_ldflags()) | |
| if args.libs_only_L: | |
| print_flags.extend([f for f in pkg.get_ldflags() if f.startswith('-L') or f.startswith('-R')]) | |
| if args.libs_only_l: | |
| print_flags.extend([f for f in pkg.get_ldflags() if f.startswith('-l')]) | |
| if print_flags: | |
| print(" ".join(shlex.quote(f) for f in print_flags)) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment