diff options
author | Eric Snow <ericsnowcurrently@gmail.com> | 2023-03-14 16:05:54 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-03-14 16:05:54 (GMT) |
commit | 1ff81c0cb67215694f084e51c4d35ae53b9f5cf9 (patch) | |
tree | e8067aff7f52f7b17284caa3452b17a98a9ae1bc | |
parent | a703f743dbf2675948e59c44fa9d7112f7825100 (diff) | |
download | cpython-1ff81c0cb67215694f084e51c4d35ae53b9f5cf9.zip cpython-1ff81c0cb67215694f084e51c4d35ae53b9f5cf9.tar.gz cpython-1ff81c0cb67215694f084e51c4d35ae53b9f5cf9.tar.bz2 |
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
-rw-r--r-- | .github/workflows/build.yml | 3 | ||||
-rw-r--r-- | Lib/test/test_check_c_globals.py | 34 | ||||
-rw-r--r-- | Makefile.pre.in | 6 | ||||
-rw-r--r-- | Tools/c-analyzer/c_parser/preprocessor/common.py | 6 | ||||
-rw-r--r-- | Tools/c-analyzer/c_parser/preprocessor/gcc.py | 23 | ||||
-rw-r--r-- | Tools/c-analyzer/cpython/__main__.py | 62 | ||||
-rw-r--r-- | Tools/c-analyzer/cpython/_parser.py | 9 | ||||
-rw-r--r-- | Tools/c-analyzer/cpython/ignored.tsv | 1 |
8 files changed, 90 insertions, 54 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2241b0b..4e53282 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -111,6 +111,9 @@ jobs: run: make smelly - name: Check limited ABI symbols 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: name: 'Windows (x86)' diff --git a/Lib/test/test_check_c_globals.py b/Lib/test/test_check_c_globals.py deleted file mode 100644 index 670be52..0000000 --- a/Lib/test/test_check_c_globals.py +++ /dev/null @@ -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() diff --git a/Makefile.pre.in b/Makefile.pre.in index 1a1853b..5976216 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -2560,6 +2560,12 @@ distclean: clobber docclean smelly: all $(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 funny: find $(SUBDIRS) $(SUBDIRSTOO) \ diff --git a/Tools/c-analyzer/c_parser/preprocessor/common.py b/Tools/c-analyzer/c_parser/preprocessor/common.py index 4291a06..dbe1ede 100644 --- a/Tools/c-analyzer/c_parser/preprocessor/common.py +++ b/Tools/c-analyzer/c_parser/preprocessor/common.py @@ -115,15 +115,15 @@ def converted_error(tool, argv, filename): def convert_error(tool, argv, filename, stderr, rc): error = (stderr.splitlines()[0], rc) if (_expected := is_os_mismatch(filename, stderr)): - logger.debug(stderr.strip()) + logger.info(stderr.strip()) raise OSMismatchError(filename, _expected, argv, error, tool) elif (_missing := is_missing_dep(stderr)): - logger.debug(stderr.strip()) + logger.info(stderr.strip()) raise MissingDependenciesError(filename, (_missing,), argv, error, tool) elif '#error' in stderr: # XXX Ignore incompatible files. error = (stderr.splitlines()[1], rc) - logger.debug(stderr.strip()) + logger.info(stderr.strip()) raise ErrorDirectiveError(filename, argv, error, tool) else: # Try one more time, with stderr written to the terminal. diff --git a/Tools/c-analyzer/c_parser/preprocessor/gcc.py b/Tools/c-analyzer/c_parser/preprocessor/gcc.py index 7ef1a8a..24c1b0e 100644 --- a/Tools/c-analyzer/c_parser/preprocessor/gcc.py +++ b/Tools/c-analyzer/c_parser/preprocessor/gcc.py @@ -6,6 +6,11 @@ from . import common as _common TOOL = 'gcc' +META_FILES = { + '<built-in>', + '<command-line>', +} + # https://gcc.gnu.org/onlinedocs/cpp/Preprocessor-Output.html # flags: # 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 next two lines are consistent. - for expected in [ - f'# 1 "{reqfile}"', - '# 1 "<built-in>"', - '# 1 "<command-line>"', - ]: + firstlines = [ + f'# 0 "{reqfile}"', + '# 0 "<built-in>"', + '# 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) if 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. lno = 1 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. return @@ -174,8 +183,8 @@ def _parse_marker_line(line, reqfile=None): return None, None, None lno, origfile, flags = m.groups() lno = int(lno) + assert origfile not in META_FILES, (line,) 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 () if 1 in flags: diff --git a/Tools/c-analyzer/cpython/__main__.py b/Tools/c-analyzer/cpython/__main__.py index 2b9e423..fe7a167 100644 --- a/Tools/c-analyzer/cpython/__main__.py +++ b/Tools/c-analyzer/cpython/__main__.py @@ -1,5 +1,6 @@ import logging import sys +import textwrap from c_common.fsutil import expand_filenames, iter_files_by_suffix from c_common.scriptutil import ( @@ -26,6 +27,39 @@ from . import _analyzer, _builtin_types, _capi, _files, _parser, REPO_ROOT 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): if 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): filenames = _resolve_filenames(filenames) kwargs['get_file_preprocessor'] = _parser.get_preprocessor(log_err=print) - c_analyzer.cmd_check( - filenames, - relroot=REPO_ROOT, - _analyze=_analyzer.analyze, - _CHECKS=CHECKS, - file_maxsizes=_parser.MAX_SIZES, - **kwargs - ) + try: + c_analyzer.cmd_check( + filenames, + relroot=REPO_ROOT, + _analyze=_analyzer.analyze, + _CHECKS=CHECKS, + 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): diff --git a/Tools/c-analyzer/cpython/_parser.py b/Tools/c-analyzer/cpython/_parser.py index e776416..6da4fbb 100644 --- a/Tools/c-analyzer/cpython/_parser.py +++ b/Tools/c-analyzer/cpython/_parser.py @@ -106,15 +106,20 @@ glob dirname * ./Include/internal Modules/_decimal/**/*.c Modules/_decimal/libmpdec +Modules/_elementtree.c Modules/expat Modules/_hacl/*.c Modules/_hacl/include Modules/_hacl/*.h Modules/_hacl/include -Modules/_tkinter.c /usr/include/tcl8.6 Modules/md5module.c Modules/_hacl/include Modules/sha1module.c Modules/_hacl/include Modules/sha2module.c Modules/_hacl/include -Modules/tkappinit.c /usr/include/tcl 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@ ''')[1:] diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index 700ddf2..048112d 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -228,6 +228,7 @@ Modules/_xxinterpchannelsmodule.c - _channelid_end_recv - Modules/_xxinterpchannelsmodule.c - _channelid_end_send - Modules/_zoneinfo.c - DAYS_BEFORE_MONTH - Modules/_zoneinfo.c - DAYS_IN_MONTH - +Modules/_xxsubinterpretersmodule.c - no_exception - Modules/arraymodule.c - descriptors - Modules/arraymodule.c - emptybuf - Modules/cjkcodecs/_codecs_cn.c - _mapping_list - |