Create targets for UPB release

PiperOrigin-RevId: 441496547
diff --git a/python/BUILD b/python/BUILD
index ac97113..ac96b6c 100644
--- a/python/BUILD
+++ b/python/BUILD
@@ -24,13 +24,154 @@
 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 # begin:github_only
-load("//bazel:py_proto_library.bzl", "py_proto_library")
-load("//bazel:py_extension.bzl", "py_extension")
-load("@rules_python//python:packaging.bzl", "py_wheel")
+load("//python:py_extension.bzl", "py_extension")
 # end:github_only
 
+load("@bazel_skylib//lib:selects.bzl", "selects")
+load("@bazel_skylib//rules:common_settings.bzl", "bool_flag", "string_flag")
+load("//bazel:build_defs.bzl", "UPB_DEFAULT_COPTS")
+
 licenses(["notice"])
 
+package(
+    default_visibility = ["//python/dist:__pkg__"],
+)
+
+LIMITED_API_FLAG_SELECT = {
+    ":limited_api_3.7": ["-DPy_LIMITED_API=0x03070000"],
+    ":limited_api_3.10": ["-DPy_LIMITED_API=0x030a0000"],
+    "//conditions:default": [],
+}
+
+bool_flag(
+    name = "limited_api",
+    build_setting_default = False,
+)
+
+string_flag(
+    name = "python_version",
+    build_setting_default = "system",
+    values = [
+        "system",
+        "37",
+        "38",
+        "39",
+        "310",
+    ],
+)
+
+config_setting(
+    name = "limited_api_3.7",
+    flag_values = {
+        ":limited_api": "True",
+        ":python_version": "37",
+    },
+)
+
+config_setting(
+    name = "full_api_3.7_win32",
+    flag_values = {
+        ":limited_api": "False",
+        ":python_version": "37",
+    },
+    values = {"cpu": "win32"},
+)
+
+config_setting(
+    name = "full_api_3.7_win64",
+    flag_values = {
+        ":limited_api": "False",
+        ":python_version": "37",
+    },
+    values = {"cpu": "win64"},
+)
+
+selects.config_setting_group(
+    name = "full_api_3.7",
+    match_any = [
+        ":full_api_3.7_win32",
+        ":full_api_3.7_win64",
+    ],
+)
+
+config_setting(
+    name = "full_api_3.8_win32",
+    flag_values = {
+        ":limited_api": "False",
+        ":python_version": "38",
+    },
+    values = {"cpu": "win32"},
+)
+
+config_setting(
+    name = "full_api_3.8_win64",
+    flag_values = {
+        ":limited_api": "False",
+        ":python_version": "38",
+    },
+    values = {"cpu": "win64"},
+)
+
+selects.config_setting_group(
+    name = "full_api_3.8",
+    match_any = [
+        ":full_api_3.8_win32",
+        ":full_api_3.8_win64",
+    ],
+)
+
+config_setting(
+    name = "full_api_3.9_win32",
+    flag_values = {
+        ":limited_api": "False",
+        ":python_version": "39",
+    },
+    values = {"cpu": "win32"},
+)
+
+config_setting(
+    name = "full_api_3.9_win64",
+    flag_values = {
+        ":limited_api": "False",
+        ":python_version": "39",
+    },
+    values = {"cpu": "win64"},
+)
+
+selects.config_setting_group(
+    name = "full_api_3.9",
+    match_any = [
+        "full_api_3.9_win32",
+        ":full_api_3.9_win64",
+    ],
+)
+
+config_setting(
+    name = "limited_api_3.10_win32",
+    flag_values = {
+        ":limited_api": "True",
+        ":python_version": "310",
+    },
+    values = {"cpu": "win32"},
+)
+
+config_setting(
+    name = "limited_api_3.10_win64",
+    flag_values = {
+        ":limited_api": "True",
+        ":python_version": "310",
+    },
+    values = {"cpu": "win64"},
+)
+
+selects.config_setting_group(
+    name = "limited_api_3.10",
+    match_any = [
+        ":limited_api_3.10_win32",
+        ":limited_api_3.10_win64",
+    ],
+)
+
 py_extension(
     name = "_message",
     srcs = [
@@ -50,10 +191,15 @@
         "message.h",
         "protobuf.c",
         "protobuf.h",
-        "python.h",
+        "python_api.h",
         "repeated.c",
         "repeated.h",
     ],
+    copts = UPB_DEFAULT_COPTS + select(LIMITED_API_FLAG_SELECT) + [
+        # The Python API requires patterns that are ISO C incompatible, like
+        # casts between function pointers and object pointers.
+        "-Wno-pedantic",
+    ],
     deps = [
         "//:descriptor_upb_proto_reflection",
         "//:reflection",
@@ -68,97 +214,13 @@
 
 py_extension(
     name = "_api_implementation",
-    srcs = ["api_implementation.c"],
-)
-
-# begin:github_only
-
-py_test(
-    name = "minimal_test",
     srcs = [
-        "minimal_test.py",
+        "api_implementation.c",
+        "python_api.h",
     ],
-    imports = ["."],
-    legacy_create_init = False,
-    deps = [
-        "//python:message_ext",
-        "@com_google_protobuf//:python_common_test_protos",
-        "@com_google_protobuf//:python_specific_test_protos",
-        "@com_google_protobuf//:python_srcs",
+    copts = UPB_DEFAULT_COPTS + select(LIMITED_API_FLAG_SELECT) + [
+        # The Python API requires patterns that are ISO C incompatible, like
+        # casts between function pointers and object pointers.
+        "-Wno-pedantic",
     ],
 )
-
-# Copy the extensions into the location recognized by Python.
-# .abi3.so indicates use of the limited API, and cross-version ABI compatibility.
-EXT_SUFFIX = ".abi3.so"
-
-genrule(
-    name = "copy_message",
-    srcs = [":_message"],
-    outs = ["google/protobuf/pyext/_message" + EXT_SUFFIX],
-    cmd = "cp $< $@",
-)
-
-genrule(
-    name = "copy_api_implementation",
-    srcs = [":_api_implementation"],
-    outs = ["google/protobuf/internal/_api_implementation" + EXT_SUFFIX],
-    cmd = "cp $< $@",
-    visibility = ["//python:__subpackages__"],
-)
-
-filegroup(
-    name = "extension_files",
-    srcs = [
-        "google/protobuf/pyext/_message" + EXT_SUFFIX,
-        "google/protobuf/internal/_api_implementation" + EXT_SUFFIX,
-    ],
-)
-
-py_library(
-    name = "message_ext",
-    data = [":extension_files"],
-    imports = ["."],
-    visibility = ["//python:__subpackages__"],
-)
-
-py_proto_library(
-    name = "well_known_proto_pb2",
-    deps = [
-        "@com_google_protobuf//:any_proto",
-        "@com_google_protobuf//:api_proto",
-        "@com_google_protobuf//:compiler_plugin_proto",
-        "@com_google_protobuf//:descriptor_proto",
-        "@com_google_protobuf//:duration_proto",
-        "@com_google_protobuf//:empty_proto",
-        "@com_google_protobuf//:field_mask_proto",
-        "@com_google_protobuf//:source_context_proto",
-        "@com_google_protobuf//:struct_proto",
-        "@com_google_protobuf//:timestamp_proto",
-        "@com_google_protobuf//:type_proto",
-        "@com_google_protobuf//:wrappers_proto",
-    ],
-)
-
-py_wheel(
-    name = "binary_wheel",
-    abi = "abi3",
-    distribution = "protobuf",
-    # TODO(https://github.com/protocolbuffers/upb/issues/502): we need to make
-    # this a select() that is calculated from the platform we are actually
-    # building on.
-    platform = "manylinux2014_x86_64",
-    python_tag = "cp36",
-    strip_path_prefixes = ["python/"],
-    version = "4.20.0",
-    deps = [
-        ":extension_files",
-        ":well_known_proto_pb2",
-        # TODO(https://github.com/protocolbuffers/upb/issues/503): currently
-        # this includes the unit tests.  We should filter these out so we are
-        # only distributing true source files.
-        "@com_google_protobuf//:python_srcs",
-    ],
-)
-
-# end:github_only
diff --git a/python/api_implementation.c b/python/api_implementation.c
index 9f96b93..744f2be 100644
--- a/python/api_implementation.c
+++ b/python/api_implementation.c
@@ -25,7 +25,7 @@
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-#include <Python.h>
+#include "python/python_api.h"
 
 static struct PyModuleDef module_def = {
     PyModuleDef_HEAD_INIT,
diff --git a/python/descriptor.h b/python/descriptor.h
index 6cb6c1c..581e1c7 100644
--- a/python/descriptor.h
+++ b/python/descriptor.h
@@ -30,7 +30,7 @@
 
 #include <stdbool.h>
 
-#include "python/python.h"
+#include "python/python_api.h"
 #include "upb/def.h"
 
 typedef enum {
diff --git a/python/dist/BUILD.bazel b/python/dist/BUILD.bazel
new file mode 100644
index 0000000..33e6287
--- /dev/null
+++ b/python/dist/BUILD.bazel
@@ -0,0 +1,152 @@
+# Copyright (c) 2009-2022, Google LLC
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#     * Redistributions of source code must retain the above copyright
+#       notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above copyright
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the distribution.
+#     * Neither the name of Google LLC nor the
+#       names of its contributors may be used to endorse or promote products
+#       derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL Google LLC BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+load("//bazel:py_proto_library.bzl", "py_proto_library")
+load(":dist.bzl", "py_dist", "py_dist_module")
+load("@bazel_skylib//lib:selects.bzl", "selects")
+load("@com_google_protobuf//:protobuf_version.bzl", "PROTOBUF_VERSION")
+load("@rules_python//python:packaging.bzl", "py_wheel")
+
+licenses(["notice"])
+
+py_dist_module(
+    name = "message_mod",
+    extension = "//python:_message_binary",
+    module_name = "google.protobuf.pyext._message",
+)
+
+py_dist_module(
+    name = "api_implementation_mod",
+    extension = "//python:_api_implementation_binary",
+    module_name = "google.protobuf.internal.api_implementation",
+)
+
+py_proto_library(
+    name = "well_known_proto_py_pb2",
+    deps = [
+        "@com_google_protobuf//:any_proto",
+        "@com_google_protobuf//:api_proto",
+        "@com_google_protobuf//:compiler_plugin_proto",
+        "@com_google_protobuf//:descriptor_proto",
+        "@com_google_protobuf//:duration_proto",
+        "@com_google_protobuf//:empty_proto",
+        "@com_google_protobuf//:field_mask_proto",
+        "@com_google_protobuf//:source_context_proto",
+        "@com_google_protobuf//:struct_proto",
+        "@com_google_protobuf//:timestamp_proto",
+        "@com_google_protobuf//:type_proto",
+        "@com_google_protobuf//:wrappers_proto",
+    ],
+)
+
+config_setting(
+    name = "aarch64_cpu",
+    values = {"cpu": "linux-aarch_64"},
+)
+
+config_setting(
+    name = "x86_64_cpu",
+    values = {"cpu": "linux-x86_64"},
+)
+
+config_setting(
+    name = "osx-x86_64_cpu",
+    values = {"cpu": "osx-x86_64"},
+)
+
+config_setting(
+    name = "win32_cpu",
+    values = {"cpu": "win32"},
+)
+
+config_setting(
+    name = "win64_cpu",
+    values = {"cpu": "win64"},
+)
+
+py_wheel(
+    name = "binary_wheel",
+    abi = select({
+        "//python:full_api_3.7": "cp37m",
+        "//python:full_api_3.8": "cp38",
+        "//python:full_api_3.9": "cp39",
+        "//conditions:default": "abi3",
+    }),
+    distribution = "protobuf",
+    platform = select({
+        ":x86_64_cpu": "manylinux2014_x86_64",
+        ":aarch64_cpu": "manylinux2014_aarch64",
+        ":osx-x86_64_cpu": "macosx_10_9_universal",
+        ":win32_cpu": "win32",
+        ":win64_cpu": "win_amd64",
+        "//conditions:default": "any",
+    }),
+    python_tag = selects.with_or({
+        ("//python:limited_api_3.7", "//python:full_api_3.7"): "cp37",
+        "//python:full_api_3.8": "cp38",
+        "//python:full_api_3.9": "cp39",
+        "//python:limited_api_3.10": "cp310",
+        "//conditions:default": "system",
+    }),
+    strip_path_prefixes = ["python/"],
+    version = PROTOBUF_VERSION,
+    deps = [
+        ":message_mod",
+        ":api_implementation_mod",
+        ":well_known_proto_py_pb2",
+        #TODO(https://github.com/protocolbuffers/upb/issues/503): currently
+        # this includes the unit tests.  We should filter these out so we are
+        # only distributing true source files.
+        "@com_google_protobuf//:python_srcs",
+    ],
+)
+
+py_dist(
+    name = "dist",
+    binary_wheel = ":binary_wheel",
+    full_api_cpus = [
+        "win32",
+        "win64",
+    ],
+    # Windows needs version-specific wheels until 3.10.
+    full_api_versions = [
+        "37",
+        "38",
+        "39",
+    ],
+    # Limited API: these wheels will satisfy any Python version >= the
+    # given version.
+    #
+    # Technically the limited API doesn't have the functions we need until
+    # 3.10, but on Linux we can get away with using 3.7 (see ../python_api.h for
+    # details).
+    limited_api_wheels = {
+        "win32": "310",
+        "win64": "310",
+        "linux-x86_64": "37",
+        "linux-aarch_64": "37",
+    },
+    tags = ["manual"],
+)
diff --git a/python/dist/dist.bzl b/python/dist/dist.bzl
new file mode 100644
index 0000000..6760a38
--- /dev/null
+++ b/python/dist/dist.bzl
@@ -0,0 +1,145 @@
+"""Rules to create python distribution files and properly name them"""
+
+load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
+load("@system_python//:version.bzl", "SYSTEM_PYTHON_VERSION")
+
+def _get_suffix(limited_api, python_version, cpu):
+    suffix = "pyd" if ("win" in cpu) else "so"
+
+    if limited_api == True:
+        if "win" not in cpu:
+            suffix = "abi3." + suffix
+        return "." + suffix
+
+    if "win32" in cpu or "win64" in cpu:
+        if "win32" in cpu:
+            abi = "win32"
+        elif "win64" in cpu:
+            abi = "win_amd64"
+        else:
+            fail("Unsupported CPU: " + cpu)
+        return ".cp{}-{}.{}".format(python_version, abi, suffix)
+
+    if python_version == "system":
+        python_version = SYSTEM_PYTHON_VERSION
+        if int(python_version) < 38:
+            python_version += "m"
+        abis = {
+            "darwin": "darwin",
+            "osx-x86_64": "darwin",
+            "osx-aarch_64": "darwin",
+            "linux-aarch_64": "aarch64-linux-gnu",
+            "linux-x86_64": "x86_64-linux-gnu",
+            "k8": "x86_64-linux-gnu",
+        }
+
+        return ".cpython-{}-{}.{}".format(python_version, abis[cpu], suffix)
+
+    fail("Unsupported combination of flags")
+
+def _py_dist_module_impl(ctx):
+    base_filename = ctx.attr.module_name.replace(".", "/")
+    suffix = _get_suffix(
+        limited_api = ctx.attr._limited_api[BuildSettingInfo].value,
+        python_version = ctx.attr._python_version[BuildSettingInfo].value,
+        cpu = ctx.var["TARGET_CPU"],
+    )
+    filename = base_filename + suffix
+    file = ctx.actions.declare_file(filename)
+    src = ctx.attr.extension[DefaultInfo].files.to_list()[0]
+    ctx.actions.run(
+        executable = "cp",
+        arguments = [src.path, file.path],
+        inputs = [src],
+        outputs = [file],
+    )
+    return [
+        DefaultInfo(files = depset([file])),
+    ]
+
+_py_dist_module_rule = rule(
+    output_to_genfiles = True,
+    implementation = _py_dist_module_impl,
+    fragments = ["cpp"],
+    attrs = {
+        "module_name": attr.string(mandatory = True),
+        "extension": attr.label(
+            mandatory = True,
+            providers = [CcInfo],
+        ),
+        "_limited_api": attr.label(default = "//python:limited_api"),
+        "_python_version": attr.label(default = "//python:python_version"),
+        "_cc_toolchain": attr.label(
+            default = "@bazel_tools//tools/cpp:current_cc_toolchain",
+        ),
+    },
+)
+
+def py_dist_module(name, module_name, extension):
+    file_rule = name + "_file"
+    _py_dist_module_rule(
+        name = file_rule,
+        module_name = module_name,
+        extension = extension,
+    )
+
+    # TODO(haberman): needed?
+    native.py_library(
+        name = name,
+        data = [":" + file_rule],
+        imports = ["."],
+    )
+
+def _py_dist_transition_impl(settings, attr):
+    _ignore = (settings)  # @unused
+    transitions = []
+
+    for cpu, version in attr.limited_api_wheels.items():
+        transitions.append({
+            "//command_line_option:cpu": cpu,
+            "//python:python_version": version,
+            "//python:limited_api": True,
+        })
+
+    for version in attr.full_api_versions:
+        for cpu in attr.full_api_cpus:
+            transitions.append({
+                "//command_line_option:cpu": cpu,
+                "//python:python_version": version,
+                "//python:limited_api": False,
+            })
+
+    return transitions
+
+_py_dist_transition = transition(
+    implementation = _py_dist_transition_impl,
+    inputs = [],
+    outputs = [
+        "//command_line_option:cpu",
+        "//python:python_version",
+        "//python:limited_api",
+    ],
+)
+
+def _py_dist_impl(ctx):
+    return [
+        DefaultInfo(files = depset(
+            transitive = [dep[DefaultInfo].files for dep in ctx.attr.binary_wheel],
+        )),
+    ]
+
+py_dist = rule(
+    implementation = _py_dist_impl,
+    attrs = {
+        "binary_wheel": attr.label(
+            mandatory = True,
+            cfg = _py_dist_transition,
+        ),
+        "limited_api_wheels": attr.string_dict(),
+        "full_api_versions": attr.string_list(),
+        "full_api_cpus": attr.string_list(),
+        "_allowlist_function_transition": attr.label(
+            default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
+        ),
+    },
+)
diff --git a/python/extension_dict.h b/python/extension_dict.h
index f8a8fc4..a21822c 100644
--- a/python/extension_dict.h
+++ b/python/extension_dict.h
@@ -30,7 +30,7 @@
 
 #include <stdbool.h>
 
-#include "python/python.h"
+#include "python/python_api.h"
 
 PyObject* PyUpb_ExtensionDict_New(PyObject* msg);
 
diff --git a/python/map.h b/python/map.h
index 0c03ed0..aaa4e20 100644
--- a/python/map.h
+++ b/python/map.h
@@ -30,7 +30,7 @@
 
 #include <stdbool.h>
 
-#include "python/python.h"
+#include "python/python_api.h"
 #include "upb/def.h"
 
 // Creates a new repeated field stub for field `f` of message object `parent`.
diff --git a/python/protobuf.h b/python/protobuf.h
index 0b87646..262f33e 100644
--- a/python/protobuf.h
+++ b/python/protobuf.h
@@ -31,7 +31,7 @@
 #include <stdbool.h>
 
 #include "python/descriptor.h"
-#include "python/python.h"
+#include "python/python_api.h"
 #include "upb/table_internal.h"
 
 
diff --git a/python/py_extension.bzl b/python/py_extension.bzl
new file mode 100644
index 0000000..2fea049
--- /dev/null
+++ b/python/py_extension.bzl
@@ -0,0 +1,70 @@
+"""Macro to support py_extension
+"""
+
+def py_extension(name, srcs, copts, deps = []):
+    """Creates a C++ library to extend python
+
+    Args:
+      name: Name of the target
+      srcs: List of source files to create the target
+      copts: List of C++ compile options to use
+      deps: Libraries that the target depends on
+    """
+
+    version_script = name + "_version_script.lds"
+    symbol = "PyInit_" + name
+    native.genrule(
+        name = "gen_" + version_script,
+        outs = [version_script],
+        cmd = "echo 'message { global: " + symbol + "; local: *; };' > $@",
+    )
+
+    native.cc_binary(
+        name = name + "_binary",
+        srcs = srcs,
+        copts = copts,
+        linkopts = select({
+            "//python/dist:osx-x86_64_cpu": ["-undefined", "dynamic_lookup"],
+            "//conditions:default": [],
+        }),
+        linkshared = True,
+        linkstatic = True,
+        deps = deps + [
+            ":" + version_script,
+        ] + select({
+            "//python:limited_api_3.7": ["@python-3.7.0//:python_headers"],
+            "//python:full_api_3.7_win32": ["@nuget_python_i686_3.7.0//:python_full_api"],
+            "//python:full_api_3.7_win64": ["@nuget_python_x86-64_3.7.0//:python_full_api"],
+            "//python:full_api_3.8_win32": ["@nuget_python_i686_3.8.0//:python_full_api"],
+            "//python:full_api_3.8_win64": ["@nuget_python_x86-64_3.8.0//:python_full_api"],
+            "//python:full_api_3.9_win32": ["@nuget_python_i686_3.9.0//:python_full_api"],
+            "//python:full_api_3.9_win64": ["@nuget_python_x86-64_3.9.0//:python_full_api"],
+            "//python:limited_api_3.10_win32": ["@nuget_python_i686_3.10.0//:python_limited_api"],
+            "//python:limited_api_3.10_win64": ["@nuget_python_x86-64_3.10.0//:python_limited_api"],
+            "//conditions:default": ["@system_python//:python_headers"],
+        }),
+    )
+
+    EXT_SUFFIX = ".abi3.so"
+
+    module_name_map = {
+        "_message": "pyext",
+        "_api_implementation": "internal",
+    }
+
+    output_file = "google/protobuf/" + module_name_map[name] + "/" + name + EXT_SUFFIX
+
+    native.genrule(
+        name = "copy" + name,
+        srcs = [":" + name + "_binary"],
+        outs = [output_file],
+        cmd = "cp $< $@",
+        visibility = ["//python:__subpackages__"],
+    )
+
+    native.py_library(
+        name = name,
+        data = [output_file],
+        imports = ["."],
+        visibility = ["//python:__subpackages__"],
+    )
diff --git a/python/python.h b/python/python_api.h
similarity index 61%
rename from python/python.h
rename to python/python_api.h
index 7eef3e5..e8a8fcf 100644
--- a/python/python.h
+++ b/python/python_api.h
@@ -28,15 +28,33 @@
 #ifndef PYUPB_PYTHON_H__
 #define PYUPB_PYTHON_H__
 
-// We restrict ourselves to the limited API, so that we will be ABI-compatible
-// with any version of Python >= 3.6.1  (3.6.1 introduce PySlice_Unpack())
-#define Py_LIMITED_API 0x03060100
-#include <Python.h>
+// We restrict ourselves to the limited API, so that a single build can be
+// ABI-compatible with a wide range of Python versions.
+//
+// The build system will define Py_LIMITED_API as appropriate (see BUILD). We
+// only want to define it for our distribution packages, since we can do some
+// extra assertions when Py_LIMITED_API is not defined.  Also Py_LIMITED_API is
+// incompatible with Py_DEBUG.
 
-// This function was not officially added to the limited API until Python 3.10.
-// But in practice it has been stable since Python 3.1.  See:
+// #define Py_LIMITED_API <val>  // Defined by build system when appropriate.
+
+#include "Python.h"
+
+// Ideally we could restrict ourselves to the limited API of 3.7, but this is
+// a very important function that was not officially added to the limited API
+// until 3.10.  Without this function, there is no way of getting data from a
+// Python `str` object without a copy.
+//
+// While this function was not *officially* added to the limited API until
+// Python 3.10, In practice it has been stable since Python 3.1.
 //   https://bugs.python.org/issue41784
+//
+// On Linux, ELF lets us get away with using this function with the limited
+// API prior to 3.10.
+
+#if defined(__linux__) && defined(Py_LIMITED_API) && Py_LIMITED_API < 0x03100000
 PyAPI_FUNC(const char*)
     PyUnicode_AsUTF8AndSize(PyObject* unicode, Py_ssize_t* size);
+#endif
 
 #endif  // PYUPB_PYTHON_H__
diff --git a/python/repeated.h b/python/repeated.h
index 9cde3ea..5d74bd2 100644
--- a/python/repeated.h
+++ b/python/repeated.h
@@ -30,7 +30,7 @@
 
 #include <stdbool.h>
 
-#include "python/python.h"
+#include "python/python_api.h"
 #include "upb/def.h"
 
 // Creates a new repeated field stub for field `f` of message object `parent`.