mirror of
https://github.com/python/cpython.git
synced 2025-09-26 10:19:53 +00:00
gh-81057: Add a CI Check for New Unsupported C Global Variables (gh-102506)
This will keep us from adding new unsupported (i.e. non-const) C global variables, which would break interpreter isolation. FYI, historically it is very uncommon for new global variables to get added. Furthermore, it is rare for new code to break the c-analyzer. So the check should almost always pass unnoticed. Note that I've removed test_check_c_globals. A test wasn't a great fit conceptually and was super slow on debug builds. A CI check is a better fit. This also resolves gh-100237. https://github.com/python/cpython/issues/81057
This commit is contained in:
parent
a703f743db
commit
1ff81c0cb6
8 changed files with 90 additions and 54 deletions
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
|
@ -111,6 +111,9 @@ jobs:
|
||||||
run: make smelly
|
run: make smelly
|
||||||
- name: Check limited ABI symbols
|
- name: Check limited ABI symbols
|
||||||
run: make check-limited-abi
|
run: make check-limited-abi
|
||||||
|
- name: Check for unsupported C global variables
|
||||||
|
if: github.event_name == 'pull_request' # $GITHUB_EVENT_NAME
|
||||||
|
run: make check-c-globals
|
||||||
|
|
||||||
build_win32:
|
build_win32:
|
||||||
name: 'Windows (x86)'
|
name: 'Windows (x86)'
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
import unittest
|
|
||||||
import test.test_tools
|
|
||||||
from test.support.warnings_helper import save_restore_warnings_filters
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: gh-92584: c-analyzer uses distutils which was removed in Python 3.12
|
|
||||||
raise unittest.SkipTest("distutils has been removed in Python 3.12")
|
|
||||||
|
|
||||||
|
|
||||||
test.test_tools.skip_if_missing('c-analyzer')
|
|
||||||
with test.test_tools.imports_under_tool('c-analyzer'):
|
|
||||||
# gh-95349: Save/restore warnings filters to leave them unchanged.
|
|
||||||
# Importing the c-analyzer imports docutils which imports pkg_resources
|
|
||||||
# which adds a warnings filter.
|
|
||||||
with save_restore_warnings_filters():
|
|
||||||
from cpython.__main__ import main
|
|
||||||
|
|
||||||
|
|
||||||
class ActualChecks(unittest.TestCase):
|
|
||||||
|
|
||||||
# XXX Also run the check in "make check".
|
|
||||||
#@unittest.expectedFailure
|
|
||||||
# Failing on one of the buildbots (see https://bugs.python.org/issue36876).
|
|
||||||
@unittest.skip('activate this once all the globals have been resolved')
|
|
||||||
def test_check_c_globals(self):
|
|
||||||
try:
|
|
||||||
main('check', {})
|
|
||||||
except NotImplementedError:
|
|
||||||
raise unittest.SkipTest('not supported on this host')
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# Test needs to be a package, so we can do relative imports.
|
|
||||||
unittest.main()
|
|
|
@ -2560,6 +2560,12 @@ distclean: clobber docclean
|
||||||
smelly: all
|
smelly: all
|
||||||
$(RUNSHARED) ./$(BUILDPYTHON) $(srcdir)/Tools/build/smelly.py
|
$(RUNSHARED) ./$(BUILDPYTHON) $(srcdir)/Tools/build/smelly.py
|
||||||
|
|
||||||
|
# Check if any unsupported C global variables have been added.
|
||||||
|
check-c-globals:
|
||||||
|
$(PYTHON_FOR_REGEN) $(srcdir)/Tools/c-analyzer/check-c-globals.py \
|
||||||
|
--format summary \
|
||||||
|
--traceback
|
||||||
|
|
||||||
# Find files with funny names
|
# Find files with funny names
|
||||||
funny:
|
funny:
|
||||||
find $(SUBDIRS) $(SUBDIRSTOO) \
|
find $(SUBDIRS) $(SUBDIRSTOO) \
|
||||||
|
|
|
@ -115,15 +115,15 @@ def converted_error(tool, argv, filename):
|
||||||
def convert_error(tool, argv, filename, stderr, rc):
|
def convert_error(tool, argv, filename, stderr, rc):
|
||||||
error = (stderr.splitlines()[0], rc)
|
error = (stderr.splitlines()[0], rc)
|
||||||
if (_expected := is_os_mismatch(filename, stderr)):
|
if (_expected := is_os_mismatch(filename, stderr)):
|
||||||
logger.debug(stderr.strip())
|
logger.info(stderr.strip())
|
||||||
raise OSMismatchError(filename, _expected, argv, error, tool)
|
raise OSMismatchError(filename, _expected, argv, error, tool)
|
||||||
elif (_missing := is_missing_dep(stderr)):
|
elif (_missing := is_missing_dep(stderr)):
|
||||||
logger.debug(stderr.strip())
|
logger.info(stderr.strip())
|
||||||
raise MissingDependenciesError(filename, (_missing,), argv, error, tool)
|
raise MissingDependenciesError(filename, (_missing,), argv, error, tool)
|
||||||
elif '#error' in stderr:
|
elif '#error' in stderr:
|
||||||
# XXX Ignore incompatible files.
|
# XXX Ignore incompatible files.
|
||||||
error = (stderr.splitlines()[1], rc)
|
error = (stderr.splitlines()[1], rc)
|
||||||
logger.debug(stderr.strip())
|
logger.info(stderr.strip())
|
||||||
raise ErrorDirectiveError(filename, argv, error, tool)
|
raise ErrorDirectiveError(filename, argv, error, tool)
|
||||||
else:
|
else:
|
||||||
# Try one more time, with stderr written to the terminal.
|
# Try one more time, with stderr written to the terminal.
|
||||||
|
|
|
@ -6,6 +6,11 @@ from . import common as _common
|
||||||
|
|
||||||
TOOL = 'gcc'
|
TOOL = 'gcc'
|
||||||
|
|
||||||
|
META_FILES = {
|
||||||
|
'<built-in>',
|
||||||
|
'<command-line>',
|
||||||
|
}
|
||||||
|
|
||||||
# https://gcc.gnu.org/onlinedocs/cpp/Preprocessor-Output.html
|
# https://gcc.gnu.org/onlinedocs/cpp/Preprocessor-Output.html
|
||||||
# flags:
|
# flags:
|
||||||
# 1 start of a new file
|
# 1 start of a new file
|
||||||
|
@ -75,11 +80,15 @@ def _iter_lines(text, reqfile, samefiles, cwd, raw=False):
|
||||||
|
|
||||||
# The first line is special.
|
# The first line is special.
|
||||||
# The next two lines are consistent.
|
# The next two lines are consistent.
|
||||||
for expected in [
|
firstlines = [
|
||||||
f'# 1 "{reqfile}"',
|
f'# 0 "{reqfile}"',
|
||||||
'# 1 "<built-in>"',
|
'# 0 "<built-in>"',
|
||||||
'# 1 "<command-line>"',
|
'# 0 "<command-line>"',
|
||||||
]:
|
]
|
||||||
|
if text.startswith('# 1 '):
|
||||||
|
# Some preprocessors emit a lineno of 1 for line-less entries.
|
||||||
|
firstlines = [l.replace('# 0 ', '# 1 ') for l in firstlines]
|
||||||
|
for expected in firstlines:
|
||||||
line = next(lines)
|
line = next(lines)
|
||||||
if line != expected:
|
if line != expected:
|
||||||
raise NotImplementedError((line, expected))
|
raise NotImplementedError((line, expected))
|
||||||
|
@ -121,7 +130,7 @@ def _iter_top_include_lines(lines, topfile, cwd,
|
||||||
# _parse_marker_line() that the preprocessor reported lno as 1.
|
# _parse_marker_line() that the preprocessor reported lno as 1.
|
||||||
lno = 1
|
lno = 1
|
||||||
for line in lines:
|
for line in lines:
|
||||||
if line == '# 1 "<command-line>" 2':
|
if line == '# 0 "<command-line>" 2' or line == '# 1 "<command-line>" 2':
|
||||||
# We're done with this top-level include.
|
# We're done with this top-level include.
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -174,8 +183,8 @@ def _parse_marker_line(line, reqfile=None):
|
||||||
return None, None, None
|
return None, None, None
|
||||||
lno, origfile, flags = m.groups()
|
lno, origfile, flags = m.groups()
|
||||||
lno = int(lno)
|
lno = int(lno)
|
||||||
|
assert origfile not in META_FILES, (line,)
|
||||||
assert lno > 0, (line, lno)
|
assert lno > 0, (line, lno)
|
||||||
assert origfile not in ('<built-in>', '<command-line>'), (line,)
|
|
||||||
flags = set(int(f) for f in flags.split()) if flags else ()
|
flags = set(int(f) for f in flags.split()) if flags else ()
|
||||||
|
|
||||||
if 1 in flags:
|
if 1 in flags:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
import textwrap
|
||||||
|
|
||||||
from c_common.fsutil import expand_filenames, iter_files_by_suffix
|
from c_common.fsutil import expand_filenames, iter_files_by_suffix
|
||||||
from c_common.scriptutil import (
|
from c_common.scriptutil import (
|
||||||
|
@ -26,6 +27,39 @@ from . import _analyzer, _builtin_types, _capi, _files, _parser, REPO_ROOT
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
CHECK_EXPLANATION = textwrap.dedent('''
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
Non-constant global variables are generally not supported
|
||||||
|
in the CPython repo. We use a tool to analyze the C code
|
||||||
|
and report if any unsupported globals are found. The tool
|
||||||
|
may be run manually with:
|
||||||
|
|
||||||
|
./python Tools/c-analyzer/check-c-globals.py --format summary [FILE]
|
||||||
|
|
||||||
|
Occasionally the tool is unable to parse updated code.
|
||||||
|
If this happens then add the file to the "EXCLUDED" list
|
||||||
|
in Tools/c-analyzer/cpython/_parser.py and create a new
|
||||||
|
issue for fixing the tool (and CC ericsnowcurrently
|
||||||
|
on the issue).
|
||||||
|
|
||||||
|
If the tool reports an unsupported global variable and
|
||||||
|
it is actually const (and thus supported) then first try
|
||||||
|
fixing the declaration appropriately in the code. If that
|
||||||
|
doesn't work then add the variable to the "should be const"
|
||||||
|
section of Tools/c-analyzer/cpython/ignored.tsv.
|
||||||
|
|
||||||
|
If the tool otherwise reports an unsupported global variable
|
||||||
|
then first try to make it non-global, possibly adding to
|
||||||
|
PyInterpreterState (for core code) or module state (for
|
||||||
|
extension modules). In an emergency, you can add the
|
||||||
|
variable to Tools/c-analyzer/cpython/globals-to-fix.tsv
|
||||||
|
to get CI passing, but doing so should be avoided. If
|
||||||
|
this course it taken, be sure to create an issue for
|
||||||
|
eliminating the global (and CC ericsnowcurrently).
|
||||||
|
''')
|
||||||
|
|
||||||
|
|
||||||
def _resolve_filenames(filenames):
|
def _resolve_filenames(filenames):
|
||||||
if filenames:
|
if filenames:
|
||||||
resolved = (_files.resolve_filename(f) for f in filenames)
|
resolved = (_files.resolve_filename(f) for f in filenames)
|
||||||
|
@ -123,14 +157,26 @@ def _cli_check(parser, **kwargs):
|
||||||
def cmd_check(filenames=None, **kwargs):
|
def cmd_check(filenames=None, **kwargs):
|
||||||
filenames = _resolve_filenames(filenames)
|
filenames = _resolve_filenames(filenames)
|
||||||
kwargs['get_file_preprocessor'] = _parser.get_preprocessor(log_err=print)
|
kwargs['get_file_preprocessor'] = _parser.get_preprocessor(log_err=print)
|
||||||
c_analyzer.cmd_check(
|
try:
|
||||||
filenames,
|
c_analyzer.cmd_check(
|
||||||
relroot=REPO_ROOT,
|
filenames,
|
||||||
_analyze=_analyzer.analyze,
|
relroot=REPO_ROOT,
|
||||||
_CHECKS=CHECKS,
|
_analyze=_analyzer.analyze,
|
||||||
file_maxsizes=_parser.MAX_SIZES,
|
_CHECKS=CHECKS,
|
||||||
**kwargs
|
file_maxsizes=_parser.MAX_SIZES,
|
||||||
)
|
**kwargs
|
||||||
|
)
|
||||||
|
except SystemExit as exc:
|
||||||
|
num_failed = exc.args[0] if getattr(exc, 'args', None) else None
|
||||||
|
if isinstance(num_failed, int):
|
||||||
|
if num_failed > 0:
|
||||||
|
sys.stderr.flush()
|
||||||
|
print(CHECK_EXPLANATION, flush=True)
|
||||||
|
raise # re-raise
|
||||||
|
except Exception:
|
||||||
|
sys.stderr.flush()
|
||||||
|
print(CHECK_EXPLANATION, flush=True)
|
||||||
|
raise # re-raise
|
||||||
|
|
||||||
|
|
||||||
def cmd_analyze(filenames=None, **kwargs):
|
def cmd_analyze(filenames=None, **kwargs):
|
||||||
|
|
|
@ -106,15 +106,20 @@ glob dirname
|
||||||
* ./Include/internal
|
* ./Include/internal
|
||||||
|
|
||||||
Modules/_decimal/**/*.c Modules/_decimal/libmpdec
|
Modules/_decimal/**/*.c Modules/_decimal/libmpdec
|
||||||
|
Modules/_elementtree.c Modules/expat
|
||||||
Modules/_hacl/*.c Modules/_hacl/include
|
Modules/_hacl/*.c Modules/_hacl/include
|
||||||
Modules/_hacl/*.h Modules/_hacl/include
|
Modules/_hacl/*.h Modules/_hacl/include
|
||||||
Modules/_tkinter.c /usr/include/tcl8.6
|
|
||||||
Modules/md5module.c Modules/_hacl/include
|
Modules/md5module.c Modules/_hacl/include
|
||||||
Modules/sha1module.c Modules/_hacl/include
|
Modules/sha1module.c Modules/_hacl/include
|
||||||
Modules/sha2module.c Modules/_hacl/include
|
Modules/sha2module.c Modules/_hacl/include
|
||||||
Modules/tkappinit.c /usr/include/tcl
|
|
||||||
Objects/stringlib/*.h Objects
|
Objects/stringlib/*.h Objects
|
||||||
|
|
||||||
|
# possible system-installed headers, just in case
|
||||||
|
Modules/_tkinter.c /usr/include/tcl8.6
|
||||||
|
Modules/_uuidmodule.c /usr/include/uuid
|
||||||
|
Modules/nismodule.c /usr/include/tirpc
|
||||||
|
Modules/tkappinit.c /usr/include/tcl
|
||||||
|
|
||||||
# @end=tsv@
|
# @end=tsv@
|
||||||
''')[1:]
|
''')[1:]
|
||||||
|
|
||||||
|
|
|
@ -228,6 +228,7 @@ Modules/_xxinterpchannelsmodule.c - _channelid_end_recv -
|
||||||
Modules/_xxinterpchannelsmodule.c - _channelid_end_send -
|
Modules/_xxinterpchannelsmodule.c - _channelid_end_send -
|
||||||
Modules/_zoneinfo.c - DAYS_BEFORE_MONTH -
|
Modules/_zoneinfo.c - DAYS_BEFORE_MONTH -
|
||||||
Modules/_zoneinfo.c - DAYS_IN_MONTH -
|
Modules/_zoneinfo.c - DAYS_IN_MONTH -
|
||||||
|
Modules/_xxsubinterpretersmodule.c - no_exception -
|
||||||
Modules/arraymodule.c - descriptors -
|
Modules/arraymodule.c - descriptors -
|
||||||
Modules/arraymodule.c - emptybuf -
|
Modules/arraymodule.c - emptybuf -
|
||||||
Modules/cjkcodecs/_codecs_cn.c - _mapping_list -
|
Modules/cjkcodecs/_codecs_cn.c - _mapping_list -
|
||||||
|
|
Can't render this file because it has a wrong number of fields in line 4.
|
Loading…
Add table
Add a link
Reference in a new issue