Author: Armin Rigo <[email protected]>
Branch:
Changeset: r3205:bafc95c0591a
Date: 2019-01-31 12:14 +0100
http://bitbucket.org/cffi/cffi/changeset/bafc95c0591a/
Log: Tweaks to the pkgconfig support
diff --git a/cffi/__init__.py b/cffi/__init__.py
--- a/cffi/__init__.py
+++ b/cffi/__init__.py
@@ -3,6 +3,7 @@
from .api import FFI
from .error import CDefError, FFIError, VerificationError, VerificationMissing
+from .error import PkgConfigError
__version__ = "1.12.0"
__version_info__ = (1, 12, 0)
diff --git a/cffi/api.py b/cffi/api.py
--- a/cffi/api.py
+++ b/cffi/api.py
@@ -2,7 +2,6 @@
from .lock import allocate_lock
from .error import CDefError
from . import model
-from . import pkgconfig
try:
callable
@@ -642,8 +641,11 @@
raise ValueError("'module_name' must not contain '/': use a dotted
"
"name to make a 'package.module' location")
if "pkgconfig" in kwds:
- pkgconfig.merge_flags(kwds, pkgconfig.flags(kwds["pkgconfig"]))
- del kwds["pkgconfig"]
+ from . import pkgconfig
+ libs = kwds.pop("pkgconfig")
+ if not isinstance(libs, (list, tuple)):
+ libs = [libs]
+ pkgconfig.merge_flags(kwds, pkgconfig.flags_from_pkgconfig(libs))
self._assigned_source = (str(module_name), source,
source_extension, kwds)
diff --git a/cffi/error.py b/cffi/error.py
--- a/cffi/error.py
+++ b/cffi/error.py
@@ -1,8 +1,9 @@
class FFIError(Exception):
- pass
+ __module__ = 'cffi'
class CDefError(Exception):
+ __module__ = 'cffi'
def __str__(self):
try:
current_decl = self.args[1]
@@ -16,15 +17,15 @@
class VerificationError(Exception):
""" An error raised when verification fails
"""
+ __module__ = 'cffi'
class VerificationMissing(Exception):
""" An error raised when incomplete structures are passed into
cdef, but no verification has been done
"""
+ __module__ = 'cffi'
class PkgConfigError(Exception):
- """ An error raised for all pkg-config related errors
- except version mismatch"""
-
-class PkgConfigModuleVersionNotFound(Exception):
- """ An error raised when requested version was not found"""
+ """ An error raised for missing modules in pkg-config
+ """
+ __module__ = 'cffi'
diff --git a/cffi/pkgconfig.py b/cffi/pkgconfig.py
--- a/cffi/pkgconfig.py
+++ b/cffi/pkgconfig.py
@@ -1,51 +1,63 @@
# pkg-config, https://www.freedesktop.org/wiki/Software/pkg-config/
integration for cffi
-import subprocess
-import sys
-import re
+import sys, os, subprocess
-from .error import PkgConfigModuleVersionNotFound
from .error import PkgConfigError
+
def merge_flags(cfg1, cfg2):
"""Merge values from cffi config flags cfg2 to cf1
Example:
- merge_flags({"libraries": ["one"]}, {"libraries": "two"})
- {"libraries}" : ["one", "two"]}
+ merge_flags({"libraries": ["one"]}, {"libraries": ["two"]})
+ {"libraries": ["one", "two"]}
"""
for key, value in cfg2.items():
- if not key in cfg1:
- cfg1 [key] = value
+ if key not in cfg1:
+ cfg1[key] = value
else:
- cfg1 [key].extend(value)
+ if not isinstance(cfg1[key], list):
+ raise TypeError("cfg1[%r] should be a list of strings" %
(key,))
+ if not isinstance(value, list):
+ raise TypeError("cfg2[%r] should be a list of strings" %
(key,))
+ cfg1[key].extend(value)
return cfg1
-def call(libname, flag):
- """Calls pkg-config and returing the output if found
+def call(libname, flag, encoding=sys.getfilesystemencoding()):
+ """Calls pkg-config and returns the output if found
"""
a = ["pkg-config", "--print-errors"]
a.append(flag)
a.append(libname)
- pc = None
try:
pc = subprocess.Popen(a, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
- except FileNotFoundError:
- pass
- if pc is None:
- raise PkgConfigError("pkg-config was not found on this system")
-
+ except EnvironmentError as e:
+ raise PkgConfigError("cannot run pkg-config: %s" % (str(e).strip(),))
+
bout, berr = pc.communicate()
- if berr is not None:
- err = berr.decode(sys.getfilesystemencoding())
- if re.search("Requested '.*' but version of ", err, re.MULTILINE) is
not None:
- raise PkgConfigModuleVersionNotFound(err)
- else:
- PkgConfigError(err)
+ if pc.returncode != 0:
+ try:
+ berr = berr.decode(encoding)
+ except Exception:
+ pass
+ raise PkgConfigError(berr.strip())
+
+ if sys.version_info >= (3,) and not isinstance(bout, str): # Python 3.x
+ try:
+ bout = bout.decode(encoding)
+ except UnicodeDecodeError:
+ raise PkgConfigError("pkg-config %s %s returned bytes that cannot "
+ "be decoded with encoding %r:\n%r" %
+ (flag, libname, encoding, bout))
+
+ if os.altsep != '\\' and '\\' in bout:
+ raise PkgConfigError("pkg-config %s %s returned an unsupported "
+ "backslash-escaped output:\n%r" %
+ (flag, libname, bout))
return bout
-def flags(libs):
+def flags_from_pkgconfig(libs):
r"""Return compiler line flags for FFI.set_source based on pkg-config
output
Usage
@@ -57,49 +69,53 @@
extra_link_args are extended with an output of pkg-config for libfoo and
libbar.
- Raises
- * PkgConfigModuleVersionNotFound if requested version does not match
- * PkgConfigError for all other errors
+ Raises PkgConfigError in case the pkg-config call fails.
"""
- subprocess.check_output(["pkg-config", "--version"])
+ def get_include_dirs(string):
+ return [x[2:] for x in string.split() if x.startswith("-I")]
- # make API great again!
- if isinstance(libs, (str, bytes)):
- libs = (libs, )
-
- # drop starting -I -L -l from cflags
- def dropILl(string):
- def _dropILl(string):
- if string.startswith("-I") or string.startswith("-L") or
string.startswith("-l"):
- return string [2:]
- return [_dropILl(x) for x in string.split()]
+ def get_library_dirs(string):
+ return [x[2:] for x in string.split() if x.startswith("-L")]
- # convert -Dfoo=bar to list of tuples [("foo", "bar")] expected by cffi
- def macros(string):
- def _macros(string):
- return tuple(string [2:].split("=", 2))
- return [_macros(x) for x in string.split() if x.startswith("-D")]
+ def get_libraries(string):
+ return [x[2:] for x in string.split() if x.startswith("-l")]
- def drop_macros(string):
- return [x for x in string.split() if not x.startswith("-D")]
+ # convert -Dfoo=bar to list of tuples [("foo", "bar")] expected by
distutils
+ def get_macros(string):
+ def _macro(x):
+ x = x[2:] # drop "-D"
+ if '=' in x:
+ return tuple(x.split("=", 1)) # "-Dfoo=bar" => ("foo", "bar")
+ else:
+ return (x, None) # "-Dfoo" => ("foo", None)
+ return [_macro(x) for x in string.split() if x.startswith("-D")]
+
+ def get_other_cflags(string):
+ return [x for x in string.split() if not x.startswith("-I") and
+ not x.startswith("-D")]
+
+ def get_other_libs(string):
+ return [x for x in string.split() if not x.startswith("-L") and
+ not x.startswith("-l")]
# return kwargs for given libname
def kwargs(libname):
fse = sys.getfilesystemencoding()
+ all_cflags = call(libname, "--cflags")
+ all_libs = call(libname, "--libs")
return {
- "include_dirs" : dropILl(call(libname,
"--cflags-only-I").decode(fse)),
- "library_dirs" : dropILl(call(libname,
"--libs-only-L").decode(fse)),
- "libraries" : dropILl(call(libname,
"--libs-only-l").decode(fse)),
- "define_macros" : macros(call(libname,
"--cflags-only-other").decode('ascii')),
- "extra_compile_args" : drop_macros(call(libname,
"--cflags-only-other").decode('ascii')),
- "extra_link_args" : call(libname,
"--libs-only-other").decode('ascii').split()
- }
+ "include_dirs": get_include_dirs(all_cflags),
+ "library_dirs": get_library_dirs(all_libs),
+ "libraries": get_libraries(all_libs),
+ "define_macros": get_macros(all_cflags),
+ "extra_compile_args": get_other_cflags(all_cflags),
+ "extra_link_args": get_other_libs(all_libs),
+ }
# merge all arguments together
ret = {}
for libname in libs:
- foo = kwargs(libname)
- merge_flags(ret, foo)
-
+ lib_flags = kwargs(libname)
+ merge_flags(ret, lib_flags)
return ret
diff --git a/testing/cffi1/test_pkgconfig.py b/testing/cffi1/test_pkgconfig.py
--- a/testing/cffi1/test_pkgconfig.py
+++ b/testing/cffi1/test_pkgconfig.py
@@ -2,24 +2,19 @@
import subprocess
import py
import cffi.pkgconfig as pkgconfig
+from cffi import PkgConfigError
+
def mock_call(libname, flag):
- assert libname=="python-3.6", "mocked pc function supports python-3.6
input ONLY"
-
+ assert libname=="foobarbaz"
flags = {
- "--cflags-only-I": b"-I/usr/include/python3.6m\n",
- "--libs-only-L": b"-L/usr/lib64\n",
- "--libs-only-l": b"-lpython3.6\n",
- "--cflags-only-other": b"-DCFFI_TEST=1 -O42\n",
- "--libs-only-other": b"-lm\n",
+ "--cflags": "-I/usr/include/python3.6m -DABCD -DCFFI_TEST=1 -O42\n",
+ "--libs": "-L/usr/lib64 -lpython3.6 -shared\n",
}
return flags[flag]
-pkgconfig.call = mock_call
-
def test_merge_flags():
-
d1 = {"ham": [1, 2, 3], "spam" : ["a", "b", "c"], "foo" : []}
d2 = {"spam" : ["spam", "spam", "spam"], "bar" : ["b", "a", "z"]}
@@ -32,12 +27,68 @@
def test_pkgconfig():
- flags = pkgconfig.flags("python-3.6")
+ assert pkgconfig.flags_from_pkgconfig([]) == {}
+
+ saved = pkgconfig.call
+ try:
+ pkgconfig.call = mock_call
+ flags = pkgconfig.flags_from_pkgconfig(["foobarbaz"])
+ finally:
+ pkgconfig.call = saved
assert flags == {
- 'include_dirs': [u'/usr/include/python3.6m'],
- 'library_dirs': [u'/usr/lib64'],
- 'libraries': [u'python3.6'],
- 'define_macros': [(u'CFFI_TEST', u'1')],
- 'extra_compile_args': [u'-O42'],
- 'extra_link_args': [u'-lm']
+ 'include_dirs': ['/usr/include/python3.6m'],
+ 'library_dirs': ['/usr/lib64'],
+ 'libraries': ['python3.6'],
+ 'define_macros': [('ABCD', None), ('CFFI_TEST', '1')],
+ 'extra_compile_args': ['-O42'],
+ 'extra_link_args': ['-shared']
}
+
+class mock_subprocess:
+ PIPE = Ellipsis
+ class Popen:
+ def __init__(self, cmd, stdout, stderr):
+ if mock_subprocess.RESULT is None:
+ raise OSError("oops can't run")
+ assert cmd == ['pkg-config', '--print-errors', '--cflags',
'libfoo']
+ def communicate(self):
+ bout, berr, rc = mock_subprocess.RESULT
+ self.returncode = rc
+ return bout, berr
+
+def test_call():
+ saved = pkgconfig.subprocess
+ try:
+ pkgconfig.subprocess = mock_subprocess
+
+ mock_subprocess.RESULT = None
+ e = py.test.raises(PkgConfigError, pkgconfig.call, "libfoo",
"--cflags")
+ assert str(e.value) == "cannot run pkg-config: oops can't run"
+
+ mock_subprocess.RESULT = b"", "Foo error!\n", 1
+ e = py.test.raises(PkgConfigError, pkgconfig.call, "libfoo",
"--cflags")
+ assert str(e.value) == "Foo error!"
+
+ mock_subprocess.RESULT = b"abc\\def\n", "", 0
+ e = py.test.raises(PkgConfigError, pkgconfig.call, "libfoo",
"--cflags")
+ assert str(e.value).startswith("pkg-config --cflags libfoo returned an
"
+ "unsupported backslash-escaped output:")
+
+ mock_subprocess.RESULT = b"abc def\n", "", 0
+ result = pkgconfig.call("libfoo", "--cflags")
+ assert result == "abc def\n"
+
+ mock_subprocess.RESULT = b"abc def\n", "", 0
+ result = pkgconfig.call("libfoo", "--cflags")
+ assert result == "abc def\n"
+
+ if sys.version_info >= (3,):
+ mock_subprocess.RESULT = b"\xff\n", "", 0
+ e = py.test.raises(PkgConfigError, pkgconfig.call,
+ "libfoo", "--cflags", encoding="utf-8")
+ assert str(e.value) == (
+ "pkg-config --cflags libfoo returned bytes that cannot be "
+ "decoded with encoding 'utf-8':\nb'\\xff\\n'")
+
+ finally:
+ pkgconfig.subprocess = saved
_______________________________________________
pypy-commit mailing list
[email protected]
https://mail.python.org/mailman/listinfo/pypy-commit