Use wheels instead for custom bdists
diff --git a/requirements.txt b/requirements.txt
index a1cc88c..e3208e6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,3 +4,4 @@
 cython>=0.23
 coverage>=4.0
 six>=1.10
+wheel>=0.29
diff --git a/setup.py b/setup.py
index f8450a7..7da070e 100644
--- a/setup.py
+++ b/setup.py
@@ -164,7 +164,7 @@
     'build_ext': commands.BuildExt,
     'gather': commands.Gather,
     'run_interop': commands.RunInterop,
-    'bdist_egg_grpc_custom': commands.BdistEggCustomName,
+    'bdist_wheel_grpc_custom': commands.BdistWheelCustomName,
 }
 
 # Ensure that package data is copied over before any commands have been run:
diff --git a/src/python/grpcio/commands.py b/src/python/grpcio/commands.py
index 774e7ad..1561bbf 100644
--- a/src/python/grpcio/commands.py
+++ b/src/python/grpcio/commands.py
@@ -41,12 +41,12 @@
 import traceback
 
 import setuptools
-from setuptools.command import bdist_egg
 from setuptools.command import build_ext
 from setuptools.command import build_py
 from setuptools.command import easy_install
 from setuptools.command import install
 from setuptools.command import test
+from wheel import bdist_wheel
 
 import support
 
@@ -59,6 +59,8 @@
 USE_GRPC_CUSTOM_BDIST = bool(int(os.environ.get(
     'GRPC_PYTHON_USE_CUSTOM_BDIST', '1')))
 
+GRPC_CUSTOM_BDIST_EXT = '.whl'
+
 CONF_PY_ADDENDUM = """
 extensions.append('sphinx.ext.napoleon')
 napoleon_google_docstring = True
@@ -74,46 +76,52 @@
 
 # TODO(atash): Remove this once PyPI has better Linux bdist support. See
 # https://bitbucket.org/pypa/pypi/issues/120/binary-wheels-for-linux-are-not-supported
-def _get_grpc_custom_bdist_egg(decorated_basename, target_egg_basename):
-  """Returns a string path to a .egg file for Linux to install.
+def _get_grpc_custom_bdist(decorated_basename, target_bdist_basename):
+  """Returns a string path to a bdist file for Linux to install.
 
-  If we can retrieve a pre-compiled egg from online, uses it. Else, emits a
+  If we can retrieve a pre-compiled bdist from online, uses it. Else, emits a
   warning and builds from source.
   """
+  # TODO(atash): somehow the name that's returned from `wheel` is different
+  # between different versions of 'wheel' (but from a compatibility standpoint,
+  # the names are compatible); we should have some way of determining name
+  # compatibility in the same way `wheel` does to avoid having to rename all of
+  # the custom wheels that we build/upload to GCS.
+
   # Break import style to ensure that setup.py has had a chance to install the
-  # relevant package eggs.
+  # relevant package.
   from six.moves.urllib import request
-  decorated_path = decorated_basename + '.egg'
+  decorated_path = decorated_basename + GRPC_CUSTOM_BDIST_EXT
   try:
     url = BINARIES_REPOSITORY + '/{target}'.format(target=decorated_path)
-    egg_data = request.urlopen(url).read()
+    bdist_data = request.urlopen(url).read()
   except IOError as error:
     raise CommandError(
-        '{}\n\nCould not find the bdist egg {}: {}'
+        '{}\n\nCould not find the bdist {}: {}'
             .format(traceback.format_exc(), decorated_path, error.message))
-  # Our chosen local egg path.
-  egg_path = target_egg_basename + '.egg'
+  # Our chosen local bdist path.
+  bdist_path = target_bdist_basename + GRPC_CUSTOM_BDIST_EXT
   try:
-    with open(egg_path, 'w') as egg_file:
-      egg_file.write(egg_data)
+    with open(bdist_path, 'w') as bdist_file:
+      bdist_file.write(bdist_data)
   except IOError as error:
     raise CommandError(
-        '{}\n\nCould not write grpcio egg: {}'
+        '{}\n\nCould not write grpcio bdist: {}'
             .format(traceback.format_exc(), error.message))
-  return egg_path
+  return bdist_path
 
 
-class EggNameMixin(object):
-  """Mixin for setuptools.Command classes to enable acquiring the egg name."""
+class WheelNameMixin(object):
+  """Mixin for setuptools.Command classes to enable acquiring the bdist name."""
 
-  def egg_name(self, with_custom):
+  def wheel_name(self, with_custom):
     """
     Args:
-      with_custom: Boolean describing whether or not to decorate the egg name
+      with_custom: Boolean describing whether or not to decorate the bdist name
         with custom gRPC-specific target information.
     """
-    egg_command = self.get_finalized_command('bdist_egg')
-    base = os.path.splitext(os.path.basename(egg_command.egg_output))[0]
+    wheel_command = self.get_finalized_command('bdist_wheel')
+    base = wheel_command.get_archive_basename()
     if with_custom:
       flavor = 'ucs2' if sys.maxunicode == 65535 else 'ucs4'
       return '{base}-{flavor}'.format(base=base, flavor=flavor)
@@ -121,7 +129,7 @@
       return base
 
 
-class Install(install.install, EggNameMixin):
+class Install(install.install, WheelNameMixin):
   """Custom Install command for gRPC Python.
 
   This is for bdist shims and whatever else we might need a custom install
@@ -147,15 +155,15 @@
     if self.use_grpc_custom_bdist:
       try:
         try:
-          egg_path = _get_grpc_custom_bdist_egg(self.egg_name(True),
-                                                self.egg_name(False))
+          bdist_path = _get_grpc_custom_bdist(self.wheel_name(True),
+                                            self.wheel_name(False))
         except CommandError as error:
           sys.stderr.write(
               '\nWARNING: Failed to acquire grpcio prebuilt binary:\n'
               '{}.\n\n'.format(error.message))
           raise
         try:
-          self._run_bdist_retrieval_install(egg_path)
+          self._run_bdist_retrieval_install(bdist_path)
         except Exception as error:
           # if anything else happens (and given how there's no way to really know
           # what's happening in setuptools here, I mean *anything*), warn the user
@@ -171,29 +179,19 @@
 
   # TODO(atash): Remove this once PyPI has better Linux bdist support. See
   # https://bitbucket.org/pypa/pypi/issues/120/binary-wheels-for-linux-are-not-supported
-  def _run_bdist_retrieval_install(self, bdist_egg):
-    easy_install = self.distribution.get_command_class('easy_install')
-    easy_install_command = easy_install(
-        self.distribution, args='x', root=self.root, record=self.record,
-    )
-    easy_install_command.ensure_finalized()
-    easy_install_command.always_copy_from = '.'
-    easy_install_command.package_index.scan(glob.glob('*.egg'))
-    arguments = [bdist_egg]
-    if setuptools.bootstrap_install_from:
-      args.insert(0, setuptools.bootstrap_install_from)
-    easy_install_command.args = arguments
-    easy_install_command.run()
-    setuptools.bootstrap_install_from = None
+  def _run_bdist_retrieval_install(self, bdist_path):
+    import pip
+    pip.main(['install', bdist_path])
 
 
-class BdistEggCustomName(bdist_egg.bdist_egg, EggNameMixin):
-  """Thin wrapper around the bdist_egg command to build with our custom name."""
+class BdistWheelCustomName(bdist_wheel.bdist_wheel, WheelNameMixin):
+  """Thin wrapper around the bdist command to build with our custom name."""
 
   def run(self):
-    bdist_egg.bdist_egg.run(self)
-    target = os.path.join(self.dist_dir, '{}.egg'.format(self.egg_name(True)))
-    shutil.move(self.get_outputs()[0], target)
+    bdist_wheel.bdist_wheel.run(self)
+    output = self.distribution.dist_files[-1][2]
+    target = os.path.join(self.dist_dir, '{}.whl'.format(self.wheel_name(True)))
+    shutil.move(output, target)
 
 
 class SphinxDocumentation(setuptools.Command):
diff --git a/tools/run_tests/build_artifact_python.sh b/tools/run_tests/build_artifact_python.sh
index 835fad8..f22ddd9 100755
--- a/tools/run_tests/build_artifact_python.sh
+++ b/tools/run_tests/build_artifact_python.sh
@@ -44,7 +44,7 @@
 ${SETARCH_CMD} python setup.py  \
     bdist_wheel                 \
     sdist                       \
-    bdist_egg_grpc_custom
+    bdist_wheel_grpc_custom
 
 mkdir -p artifacts