Skip to content

Commit df47613

Browse files
committed
linux: compile readline extension against both libreadline and libedit
Now that we have support for variants in the PYTHON.json file, we can start to put it to use. This commit teaches the Linux build system to build the readline extension with both libreadline and libedit. In order to accomplish this, we hacked up static-modules to support custom syntax which is translated to custom build rules. We then taught the distribution "parsing" code to handle extension variants. This is all a bit hacky. But it gets the job done. As part of this, Python's configure and the installable distribution now revert back to readline instead of libedit. While there are GPLv3 issues, I think this is acceptable. We have to pick 1 and readline is the only supported "backend" on Linux. So I think readline makes sense over libedit.
1 parent e67e041 commit df47613

File tree

5 files changed

+129
-18
lines changed

5 files changed

+129
-18
lines changed

cpython-linux/build-cpython.sh

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ mv Setup.local Python-${PYTHON_VERSION}/Modules/Setup.local
2828

2929
pushd Python-${PYTHON_VERSION}
3030

31+
cp Modules/readline.c Modules/readline-libedit.c
32+
3133
# Python supports using libedit instead of readline. But Modules/readline.c
3234
# has all of this behind ``#ifdef __APPLE__`` instead of a more specific
3335
# feature flag. All occurrences of __APPLE__ in that file are related to
3436
# libedit. So we just replace the content. USE_LIBEDIT comes from our
3537
# static-modules file.
3638
# TODO make changes upstream to allow libedit to more easily be used
37-
sed -i s/__APPLE__/USE_LIBEDIT/g Modules/readline.c
39+
sed -i s/__APPLE__/USE_LIBEDIT/g Modules/readline-libedit.c
3840

3941
# Most bits look at CFLAGS. But setup.py only looks at CPPFLAGS.
4042
# So we need to set both.
@@ -71,10 +73,14 @@ for d in Modules Objects Parser Programs Python; do
7173
cp -av $d/*.o /build/out/python/build/$d/
7274
done
7375

76+
# Also copy extension variant metadata files.
77+
cp -av Modules/VARIANT-*.data /build/out/python/build/Modules/
78+
7479
# The object files need to be linked against library dependencies. So copy
7580
# library files as well.
7681
mkdir /build/out/python/build/lib
7782
cp -av /tools/deps/lib/*.a /build/out/python/build/lib/
83+
cp -av /tools/deps/libedit/lib/*.a /build/out/python/build/lib/
7884

7985
# config.c defines _PyImport_Inittab and extern references to modules, which
8086
# downstream consumers may want to strip. We bundle config.c and config.c.in so

cpython-linux/build-libedit.sh

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,17 @@ pushd libedit-${LIBEDIT_VERSION}
1717

1818
cflags="${EXTRA_TARGET_CFLAGS} -fPIC -I/tools/deps/include -I/tools/deps/include/ncurses"
1919

20+
# Install to /tools/deps/libedit so it doesn't conflict with readline's files.
2021
CLFAGS="${cflags}" CPPFLAGS="${cflags}" LDFLAGS="-L/tools/deps/lib" \
2122
./configure \
2223
--build=x86_64-unknown-linux-gnu \
2324
--host=${TARGET} \
24-
--prefix=/tools/deps \
25+
--prefix=/tools/deps/libedit \
2526

2627
make -j `nproc`
2728
make -j `nproc` install DESTDIR=/build/out
2829

2930
# Alias readline/{history.h, readline.h} for readline compatibility.
30-
mkdir /build/out/tools/deps/include/readline
31-
ln -s ../editline/readline.h /build/out/tools/deps/include/readline/readline.h
32-
ln -s ../editline/readline.h /build/out/tools/deps/include/readline/history.h
31+
mkdir /build/out/tools/deps/libedit/include/readline
32+
ln -s ../editline/readline.h /build/out/tools/deps/libedit/include/readline/readline.h
33+
ln -s ../editline/readline.h /build/out/tools/deps/libedit/include/readline/history.h

cpython-linux/build.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -443,14 +443,14 @@ def python_build_info(container, config_c_in, setup_dist, setup_local):
443443

444444
# Extension data is derived by "parsing" the Setup.dist and Setup.local files.
445445

446-
def process_setup_line(line):
447-
d = parse_setup_line(line)
446+
def process_setup_line(line, variant=None):
447+
d = parse_setup_line(line, variant)
448448

449449
if not d:
450450
return
451451

452452
extension = d['extension']
453-
log('processing extension %s' % extension)
453+
log('processing extension %s (variant %s)' % (extension, d['variant']))
454454

455455
objs = []
456456

@@ -481,13 +481,13 @@ def process_setup_line(line):
481481
'system': True,
482482
})
483483

484-
bi['extensions'][extension] = [{
484+
bi['extensions'].setdefault(extension, []).append({
485485
'in_core': False,
486486
'init_fn': 'PyInit_%s' % extension,
487487
'links': links,
488488
'objs': objs,
489-
'variant': 'default',
490-
}]
489+
'variant': d['variant'],
490+
})
491491

492492

493493
found_start = False
@@ -511,19 +511,37 @@ def process_setup_line(line):
511511

512512
process_setup_line(line)
513513

514+
# Extension variants are denoted by the presence of
515+
# Modules/VARIANT-<extension>-<variant>.data files that describe the
516+
# extension. Find those files and process them.
517+
data, stat = container.get_archive('/build/out/python/build/Modules')
518+
data = io.BytesIO(b''.join(data))
519+
520+
tf = tarfile.open(fileobj=data)
521+
522+
for ti in tf:
523+
basename = os.path.basename(ti.name)
524+
525+
if not basename.startswith('VARIANT-') or not basename.endswith('.data'):
526+
continue
527+
528+
variant = basename[:-5].split('-')[2]
529+
line = tf.extractfile(ti).read().strip()
530+
process_setup_line(line, variant=variant)
531+
514532
# There are also a setup of built-in extensions defined in config.c.in which
515533
# aren't built using the Setup.* files and are part of the core libpython
516534
# distribution. Define extensions entries for these so downstream consumers
517535
# can register their PyInit_ functions.
518536
for name, init_fn in sorted(config_c_in.items()):
519537
log('adding in-core extension %s' % name)
520-
bi['extensions'][name] = [{
538+
bi['extensions'].setdefault(name, []).append({
521539
'in_core': True,
522540
'init_fn': init_fn,
523541
'links': [],
524542
'objs': [],
525543
'variant': 'default',
526-
}]
544+
})
527545

528546
# Any paths left in modules_objs are not part of any extension and are
529547
# instead part of the core distribution.
@@ -553,11 +571,11 @@ def build_cpython(client, image, platform, optimized=False):
553571
# TODO support bdb/gdbm toggle
554572
install_tools_archive(container, BUILD / ('bdb-%s.tar' % platform))
555573
install_tools_archive(container, BUILD / ('bzip2-%s.tar' % platform))
556-
# TODO support libedit/libreadline toggle
557574
install_tools_archive(container, BUILD / ('libedit-%s.tar' % platform))
558575
install_tools_archive(container, BUILD / ('libffi-%s.tar' % platform))
559576
install_tools_archive(container, BUILD / ('ncurses-%s.tar' % platform))
560577
install_tools_archive(container, BUILD / ('openssl-%s.tar' % platform))
578+
install_tools_archive(container, BUILD / ('readline-%s.tar' % platform))
561579
install_tools_archive(container, BUILD / ('sqlite-%s.tar' % platform))
562580
# tk requires a bunch of X11 stuff.
563581
#install_tools_archive(container, BUILD / ('tcltk-%s.tar' % platform))

cpython-linux/static-modules

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,10 @@ _uuid _uuidmodule.c -I/tools/deps/include/uuid -luuid
3232
_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c
3333
ossaudiodev ossaudiodev.c
3434
pyexpat pyexpat.c expat/xmlparse.c expat/xmlrole.c expat/xmltok.c -DHAVE_EXPAT_CONFIG_H=1 -DXML_POOR_ENTROPY=1 -DUSE_PYEXPAT_CAPI -IModules/expat
35-
readline readline.c -DUSE_LIBEDIT=1 -I/tools/deps/include -I/tools/deps/include/ncurses -L/tools/deps/lib -ledit -lncurses
36-
xxlimited xxlimited.c -DPy_LIMITED_API=0x03050000
35+
# readline variant needs to come first because libreadline is in /tools/deps and is
36+
# picked up by build. We /could/ make libedit first. But since we employ a hack to
37+
# coerce use of libedit on Linux, it seems prudent for the build system to pick
38+
# up readline.
39+
readline VARIANT=readline readline.c -I/tools/deps/include -I/tools/deps/include/ncurses -L/tools/deps/lib -lreadline -lncurses
40+
readline VARIANT=libedit readline-libedit.c -DUSE_LIBEDIT=1 -I/tools/deps/libedit/include -I/tools/deps/libedit/include/ncurses -L/tools/deps/libedit/lib -ledit -lncurses
41+
xxlimited xxlimited.c -DPy_LIMITED_API=0x03050000

pythonbuild/cpython.py

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
}
6868

6969

70-
def parse_setup_line(line: bytes):
70+
def parse_setup_line(line: bytes, variant: str):
7171
"""Parse a line in a ``Setup.*`` file."""
7272
if b'#' in line:
7373
line = line[:line.index(b'#')].rstrip()
@@ -89,7 +89,13 @@ def parse_setup_line(line: bytes):
8989
# Object files are named according to the basename: parent
9090
# directories they may happen to reside in are stripped out.
9191
source_path = pathlib.Path(word.decode('ascii'))
92-
obj_path = pathlib.Path('Modules/%s' % source_path.with_suffix('.o').name)
92+
93+
if variant:
94+
obj_path = pathlib.Path('Modules/VARIANT-%s-%s-%s' % (
95+
extension, variant, source_path.with_suffix('.o').name))
96+
else:
97+
obj_path = pathlib.Path('Modules/%s' % source_path.with_suffix('.o').name)
98+
9399
objs.add(obj_path)
94100

95101
# Arguments looking like link libraries are converted to library
@@ -105,6 +111,7 @@ def parse_setup_line(line: bytes):
105111
'posix_obj_paths': objs,
106112
'links': links,
107113
'frameworks': frameworks,
114+
'variant': variant or 'default',
108115
}
109116

110117

@@ -160,8 +167,82 @@ def derive_setup_local(static_modules_lines, cpython_source_archive, disabled=No
160167
dest_lines.append(line)
161168

162169
RE_DEFINE = re.compile(b'-D[^=]+=[^\s]+')
170+
RE_VARIANT = re.compile(b'VARIANT=([^\s]+)\s')
171+
172+
seen_variants = set()
163173

164174
for line in static_modules_lines:
175+
# We supplement the format to support declaring extension variants.
176+
# A variant results in multiple compiles of a given extension.
177+
# However, the CPython build system doesn't take kindly to this because
178+
# a) we can only have a single extension with a given name
179+
# b) it assumes the init function matches the extension name
180+
# c) attempting to link multiple variants into the same binary can often
181+
# result in duplicate symbols when variants use different libraries
182+
# implementing the same API.
183+
#
184+
# When we encounter a variant, we only pass the 1st variant through to
185+
# Setup.local. We then supplement the Makefile with rules to build
186+
# remaining variants.
187+
m = RE_VARIANT.search(line)
188+
if m:
189+
line = RE_VARIANT.sub(b'', line)
190+
variant = m.group(1)
191+
extension = line.split()[0]
192+
193+
cflags = []
194+
ldflags = []
195+
196+
for w in line.split()[1:]:
197+
if w.startswith((b'-I', b'-D')):
198+
cflags.append(w)
199+
elif w.startswith((b'-L', b'-l')):
200+
ldflags.append(w)
201+
elif w.endswith(b'.c'):
202+
pass
203+
else:
204+
raise ValueError('unexpected content in Setup variant line: %s' % w)
205+
206+
if extension in seen_variants:
207+
sources = [w for w in line.split() if w.endswith(b'.c')]
208+
object_files = []
209+
for source in sources:
210+
basename = os.path.basename(source)[:-2]
211+
212+
# Use a unique name to ensure there aren't collisions
213+
# across extension variants.
214+
object_file = b'Modules/VARIANT-%s-%s-%s.o' % (
215+
extension, variant, basename)
216+
object_files.append(object_file)
217+
make_lines.append(
218+
b'%s: $(srcdir)/Modules/%s; '
219+
b'$(CC) $(PY_BUILTIN_MODULE_CFLAGS) '
220+
b'%s -c $(srcdir)/Modules/%s -o %s' % (
221+
object_file, source, b' '.join(cflags),
222+
source, object_file))
223+
224+
extension_target = b'Modules/%s-VARIANT-%s$(EXT_SUFFIX)' % (
225+
extension, variant)
226+
227+
make_lines.append(b'%s: %s' % (
228+
extension_target, b' '.join(object_files)))
229+
make_lines.append(
230+
b'\t$(BLDSHARED) %s %s -o Modules/%s-VARIANT-%s$(EXT_SUFFIX)' %
231+
(b' '.join(object_files), b' '.join(ldflags),
232+
extension, variant))
233+
make_lines.append(
234+
b'\techo "%s" > Modules/VARIANT-%s-%s.data' % (
235+
line, extension, variant))
236+
237+
# Add dependencies to $(LIBRARY) target to force compilation of
238+
# extension variant.
239+
make_lines.append(b'$(LIBRARY): %s' % extension_target)
240+
241+
continue
242+
243+
else:
244+
seen_variants.add(extension)
245+
165246
# makesetup parses lines with = as extra config options. There appears
166247
# to be no easy way to define e.g. -Dfoo=bar in Setup.local. We hack
167248
# around this by detecting the syntax we'd like to support and move the

0 commit comments

Comments
 (0)