]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-81057: Add a CI Check for New Unsupported C Global Variables (gh-102506)
authorEric Snow <ericsnowcurrently@gmail.com>
Tue, 14 Mar 2023 16:05:54 +0000 (10:05 -0600)
committerGitHub <noreply@github.com>
Tue, 14 Mar 2023 16:05:54 +0000 (10:05 -0600)
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

.github/workflows/build.yml
Lib/test/test_check_c_globals.py [deleted file]
Makefile.pre.in
Tools/c-analyzer/c_parser/preprocessor/common.py
Tools/c-analyzer/c_parser/preprocessor/gcc.py
Tools/c-analyzer/cpython/__main__.py
Tools/c-analyzer/cpython/_parser.py
Tools/c-analyzer/cpython/ignored.tsv

index 2241b0b8aa409e0eb835a2f281e11d6e3984c572..4e5328282f122427b0464b6a91fce05d84051b3a 100644 (file)
@@ -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 (file)
index 670be52..0000000
+++ /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()
index 1a1853bf3d787120bec8d6fad8bf06d12fc1bc0f..59762165c1003670d7e65d4a5d9855287194d0fb 100644 (file)
@@ -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) \
index 4291a066337545aaf32bf708a535756ec3db8c01..dbe1edeef3852760de38ef4d13454d74e191edea 100644 (file)
@@ -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.
index 7ef1a8afc3b135eec9e82d2383d9de7ad2c62492..24c1b0e9b9d48c7be9adc4dcde536514b85c364b 100644 (file)
@@ -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:
index 2b9e4233b95ac46a24390aa7fe9384f40ba4a6d8..fe7a16726f45a9d6e1342621d9d2edb583e82a97 100644 (file)
@@ -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):
index e7764165d36c4c0be82d934c3e0075338256a8fd..6da4fbbf4224f17359615c9afaa5d61b04ba5310 100644 (file)
@@ -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:]
 
index 700ddf2851839e1099bc27289cf551a67ac825dc..048112dd9925557bc26d1b1ee7a27aad3147a822 100644 (file)
@@ -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   -