Skip to content

Commit 5dc3a6c

Browse files
committed
Initial implementation
1 parent bcb76b8 commit 5dc3a6c

28 files changed

+653
-18
lines changed

.coveragerc

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
[run]
2+
parallel = True
23
branch = True
3-
source =
4-
.
4+
source = $TOP
5+
data_file = $TOP/.coverage
56
omit =
67
.tox/*
78
/usr/*

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
.*.sw[a-z]
66
.coverage
77
.tox
8-
.venv-touch
8+
.venv.touch
99
/.cache
1010
/build
1111
/venv*

.travis.yml

+17-7
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
language: python
2-
env: # These should match the tox env list
3-
- TOXENV=py27
4-
- TOXENV=py34
5-
- TOXENV=py35
6-
- TOXENV=pypy
7-
install: pip install coveralls tox
8-
script: tox
2+
env:
3+
- TOXENV=py27 GO=1.5
4+
- TOXENV=py27 GO=1.6
5+
- TOXENV=py34 GO=1.6
6+
- TOXENV=py35 GO=1.6
7+
- TOXENV=pypy GO=1.6
8+
install:
9+
- eval "$(gimme $GO)"
10+
- pip install coveralls tox
11+
script:
12+
- tox
913
after_success:
1014
- coveralls
1115
sudo: false
1216
cache:
1317
directories:
1418
- $HOME/.cache/pip
1519
- $HOME/.pre-commit
20+
addons:
21+
apt:
22+
sources:
23+
- deadsnakes
24+
packages:
25+
- python3.5-dev

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ setuptools-golang
66

77
A setuptools extension for building cpython extensions written in golang.
88

9+
## Requirements
10+
11+
This requires golang >= 1.5 to be installed on your system.
12+
913
## Usage
1014

1115
Add `setuptools-golang` to the `setup_requires` in your setup.py and

requirements-dev.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
-e .
2-
coverage
2+
coverage-enable-subprocess
33
pre-commit
44
pytest

setuptools_golang.py

+103-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,110 @@
1-
from setuptools.command.build_ext import build_ext
1+
from __future__ import print_function
2+
from __future__ import unicode_literals
23

4+
import distutils.sysconfig
5+
import os
6+
import pipes
7+
import subprocess
8+
import sys
39

4-
class BuildExtGolang(build_ext):
5-
pass
10+
from setuptools.command.build_ext import build_ext as _build_ext
11+
12+
13+
PYPY = '__pypy__' in sys.builtin_module_names
14+
15+
16+
def _get_ldflags_pypy():
17+
if PYPY: # pragma: no cover (pypy only)
18+
return '-L{} -lpypy-c'.format(
19+
os.path.dirname(os.path.realpath(sys.executable)),
20+
)
21+
else:
22+
return None
23+
24+
25+
def _get_ldflags_pkg_config():
26+
try:
27+
return subprocess.check_output((
28+
'pkg-config', '--libs',
29+
'python-{}.{}'.format(*sys.version_info[:2]),
30+
)).decode('UTF-8').strip()
31+
except (subprocess.CalledProcessError, OSError):
32+
return None
33+
34+
35+
def _get_ldflags_bldlibrary():
36+
return distutils.sysconfig.get_config_var('BLDLIBRARY')
37+
38+
39+
def _get_ldflags():
40+
for func in (
41+
_get_ldflags_pypy,
42+
_get_ldflags_pkg_config,
43+
_get_ldflags_bldlibrary,
44+
):
45+
ret = func()
46+
if ret is not None:
47+
return ret
48+
else:
49+
raise AssertionError('Could not determine ldflags!')
50+
51+
52+
def _print_cmd(env, cmd):
53+
envparts = [
54+
'{}={}'.format(k, pipes.quote(v))
55+
for k, v in sorted(tuple(env.items()))
56+
]
57+
print(
58+
'$ {}'.format(' '.join(envparts + [pipes.quote(p) for p in cmd])),
59+
file=sys.stderr,
60+
)
61+
62+
63+
class build_ext(_build_ext):
64+
def build_extension(self, ext):
65+
# If there are no .go files then the parent should handle this
66+
if not any(source.endswith('.go') for source in ext.sources):
67+
return _build_ext.build_extension(self, ext)
68+
69+
for source in ext.sources:
70+
if not os.path.exists(source):
71+
raise IOError(
72+
'Error building extension `{}`: {} does not exist'.format(
73+
ext.name, source,
74+
),
75+
)
76+
77+
# Passing non-.go files to `go build` results in a failure
78+
# Passing only .go files to `go build` causes it to ignore C files
79+
# So we'll set our cwd to the root of the files and go from there!
80+
source_dirs = {os.path.dirname(src) for src in ext.sources}
81+
if len(source_dirs) != 1:
82+
raise IOError(
83+
'Error building extension `{}`: '
84+
'Cannot compile across directories: {}'.format(
85+
ext.name, ' '.join(sorted(source_dirs)),
86+
)
87+
)
88+
source_dir, = source_dirs
89+
source_dir = os.path.abspath(source_dir)
90+
91+
env = {
92+
'CGO_CFLAGS': ' '.join(
93+
'-I{}'.format(p) for p in self.compiler.include_dirs
94+
),
95+
'CGO_LDFLAGS': _get_ldflags(),
96+
}
97+
cmd = (
98+
'go', 'build', '-buildmode=c-shared',
99+
'-o', os.path.abspath(self.get_ext_fullpath(ext.name)),
100+
)
101+
_print_cmd(env, cmd)
102+
subprocess.check_call(
103+
cmd, cwd=source_dir, env=dict(os.environ, **env),
104+
)
6105

7106

8107
def set_build_ext(dist, attr, value):
9108
if not value:
10109
return
11-
dist.cmdclass['build_ext'] = BuildExtGolang
110+
dist.cmdclass['build_ext'] = build_ext

setuptools_golang_test.py

+139-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,152 @@
1+
from __future__ import unicode_literals
2+
3+
import collections
4+
import os
5+
import subprocess
6+
import sys
7+
8+
import pytest
19
from setuptools.dist import Distribution
210

311
import setuptools_golang
412

513

14+
xfailif_pypy = pytest.mark.xfail(
15+
setuptools_golang.PYPY, reason='pypy is a special snowflake',
16+
)
17+
18+
19+
@pytest.fixture(autouse=True, scope='session')
20+
def enable_coverage_subprocesses():
21+
here = os.path.dirname(os.path.abspath(__file__))
22+
os.environ['TOP'] = here
23+
os.environ['COVERAGE_PROCESS_START'] = os.path.join(here, '.coveragerc')
24+
25+
26+
def auto_namedtuple(**kwargs):
27+
return collections.namedtuple('auto_namedtuple', kwargs.keys())(**kwargs)
28+
29+
30+
def run(*cmd, **kwargs):
31+
returncode = kwargs.pop('returncode', 0)
32+
proc = subprocess.Popen(cmd, **kwargs)
33+
out, err = proc.communicate()
34+
out = out.decode('UTF-8') if out is not None else None
35+
err = err.decode('UTF-8') if err is not None else None
36+
if returncode is not None:
37+
if proc.returncode != returncode:
38+
raise AssertionError(
39+
'{!r} returned {} (expected {})\nout:\n{}err:\n{}'.format(
40+
cmd, proc.returncode, returncode, out, err,
41+
)
42+
)
43+
return auto_namedtuple(returncode=proc.returncode, out=out, err=err)
44+
45+
46+
def run_output(*cmd, **kwargs):
47+
return run(*cmd, stdout=subprocess.PIPE, **kwargs).out
48+
49+
650
def test_sets_cmdclass():
751
dist = Distribution()
852
setuptools_golang.set_build_ext(dist, 'build_golang', True)
9-
assert dist.cmdclass['build_ext'] == setuptools_golang.BuildExtGolang
53+
assert dist.cmdclass['build_ext'] == setuptools_golang.build_ext
1054

1155

1256
def test_sets_cmdclass_value_falsey():
1357
dist = Distribution()
1458
setuptools_golang.set_build_ext(dist, 'build_golang', False)
15-
assert dist.cmdclass.get('build_ext') != setuptools_golang.BuildExtGolang
59+
assert dist.cmdclass.get('build_ext') != setuptools_golang.build_ext
60+
61+
62+
GET_LDFLAGS = (
63+
'import distutils.spawn;'
64+
"print(bool(distutils.spawn.find_executable('pkg-config')));"
65+
'import setuptools_golang;'
66+
'print(setuptools_golang._get_ldflags());'
67+
)
68+
69+
70+
@xfailif_pypy
71+
def test_from_pkg_config():
72+
output = run_output(sys.executable, '-c', GET_LDFLAGS)
73+
assert output.startswith('True\n')
74+
assert '-lpython' in output
75+
76+
77+
@xfailif_pypy
78+
def test_no_pkg_config():
79+
# Blank PATH so we don't have pkg-config
80+
env = dict(os.environ, PATH='')
81+
output = run_output(sys.executable, '-c', GET_LDFLAGS, env=env)
82+
assert output.startswith('False\n')
83+
assert '-lpython' in output
84+
85+
86+
@pytest.yield_fixture(scope='session')
87+
def venv(tmpdir_factory):
88+
"""A shared virtualenv fixture, be careful not to install two of the same
89+
package into this -- or sadness...
90+
"""
91+
venv = tmpdir_factory.mktemp('venv').join('venv')
92+
pip = venv.join('bin/pip').strpath
93+
python = venv.join('bin/python').strpath
94+
# Make sure this virtualenv has the same executable
95+
run('virtualenv', venv.strpath, '-p', sys.executable)
96+
# Install this so we can get coverage
97+
run(pip, 'install', 'coverage-enable-subprocess')
98+
# Install us!
99+
run(pip, 'install', '-e', '.')
100+
yield auto_namedtuple(venv=venv, pip=pip, python=python)
101+
102+
103+
SUM = 'import {0}; print({0}.sum(1, 2))'
104+
105+
106+
@pytest.mark.parametrize(
107+
('pkg', 'mod'),
108+
(
109+
('testing/sum', 'sum'),
110+
('testing/sum_pure_go', 'sum_pure_go'),
111+
('testing/sum_sub_package', 'sum_sub_package.sum'),
112+
),
113+
)
114+
def test_sum_integration(venv, pkg, mod):
115+
run(venv.pip, 'install', '-v', pkg)
116+
out = run_output(venv.python, '-c', SUM.format(mod))
117+
assert out == '3\n'
118+
119+
120+
HELLO_WORLD = 'import project_with_c; print(project_with_c.hello_world())'
121+
122+
123+
def test_integration_project_with_c(venv):
124+
test_sum_integration(
125+
venv, 'testing/project_with_c', 'project_with_c_sum.sum',
126+
)
127+
out = run_output(venv.python, '-c', HELLO_WORLD)
128+
assert out == 'hello world\n'
129+
130+
131+
def test_integration_notfound(venv):
132+
ret = run(
133+
venv.pip, 'install', 'testing/notfound',
134+
returncode=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
135+
)
136+
assert ret.returncode != 0
137+
assert (
138+
'Error building extension `notfound`: notfound.go does not exist' in
139+
ret.out
140+
)
141+
142+
143+
def test_integration_multidir(venv):
144+
ret = run(
145+
venv.pip, 'install', 'testing/multidir',
146+
returncode=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
147+
)
148+
assert ret.returncode != 0
149+
assert (
150+
'Error building extension `multidir`: '
151+
'Cannot compile across directories: dir1 dir2' in ret.out
152+
)

testing/multidir/dir1/sum.go

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package main
2+
3+
// #include <Python.h>
4+
// int PyArg_ParseTuple_ll(PyObject*, long*, long*);
5+
import "C"
6+
7+
//export sum
8+
func sum(self *C.PyObject, args *C.PyObject) *C.PyObject {
9+
var a C.long
10+
var b C.long
11+
if C.PyArg_ParseTuple_ll(args, &a, &b) == 0 {
12+
return nil
13+
}
14+
return C.PyLong_FromLong(a + b)
15+
}
16+
17+
func main() {}

testing/multidir/dir2/sum_support.go

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package main
2+
3+
// #include <Python.h>
4+
//
5+
// PyObject* sum(PyObject* , PyObject*);
6+
//
7+
// int PyArg_ParseTuple_ll(PyObject* args, long* a, long* b) {
8+
// return PyArg_ParseTuple(args, "ll", a, b);
9+
// }
10+
//
11+
// static struct PyMethodDef methods[] = {
12+
// {"sum", (PyCFunction)sum, METH_VARARGS},
13+
// {NULL, NULL}
14+
// };
15+
//
16+
// #if PY_MAJOR_VERSION >= 3
17+
// static struct PyModuleDef module = {
18+
// PyModuleDef_HEAD_INIT,
19+
// "sum_pure_go",
20+
// NULL,
21+
// -1,
22+
// methods
23+
// };
24+
//
25+
// PyMODINIT_FUNC PyInit_sum_pure_go(void) {
26+
// return PyModule_Create(&module);
27+
// }
28+
// #else
29+
// PyMODINIT_FUNC initsum_pure_go(void) {
30+
// Py_InitModule3("sum_pure_go", methods, NULL);
31+
// }
32+
// #endif
33+
import "C"

0 commit comments

Comments
 (0)