diff --git a/contrib/installers/macosx/build-mpkg.sh b/contrib/installers/macosx/build-mpkg.sh
index 4d5866dda9f4142f70dc83f0f2a3e7e3a0bbea76..50dd81a8227ecc498ede52cf62d4aef1cce4ed51 100755
--- a/contrib/installers/macosx/build-mpkg.sh
+++ b/contrib/installers/macosx/build-mpkg.sh
@@ -3,11 +3,11 @@
 # Builds RBTools installers for macOS.
 #
 # This will build an installer that users can download and use on
-# Snow Leopard and higher.
+# El Capitan or higher.
 #
-# This package ships both Python 2.6 and 2.7 modules, in order to be
-# compatible with any custom or third-party scripts that want to use the
-# API.
+# This package ships modules for all versions of Python supported in modern
+# versions of macOS, in order to be compatible with any custom or third-party
+# scripts that want to use the API.
 #
 # By default, this will attempt to sign the installer with the official
 # certificate. This requires that the private key for the certificate exists
@@ -25,26 +25,29 @@ if test ! -e "$PWD/setup.py"; then
     exit 1
 fi
 
-which python2.7 >/dev/null || {
-    echo "python2.7 could not be found." >&2
-    exit 1
-}
 
-which pip2.6 >/dev/null || {
-    echo "pip2.6 could not be found." >&2
-    exit 1
-}
+# As of macOS High Sierra (10.13), only Python 2.7 is available natively.
+# Python 3.x is not available.
+PYTHON_VERSIONS=(
+    2.7
+)
+
+for pyver in "${PYTHON_VERSIONS[@]}"; do
+    which python${pyver} >/dev/null || {
+        echo "python${pyver} could not be found." >&2
+        exit 1
+    }
+
+    which pip${pyver} >/dev/null || {
+        echo "pip${pyver} could not be found." >&2
+        exit 1
+    }
+done
 
-which pip2.7 >/dev/null || {
-    echo "pip2.7 could not be found." >&2
-    exit 1
-}
 
 PACKAGE_NAME=RBTools
 IDENTIFIER=org.reviewboard.rbtools
 
-PYTHON_VERSIONS=(2.6 2.7)
-
 # Figure out the version of the package.
 VERSION=`PYTHONPATH=. python -c 'import rbtools; print rbtools.get_package_version()'`
 
@@ -82,34 +85,32 @@ $MKDIR -p $PKG_SRC $PKG_PYBUILD $PKG_BUILD $PKG_RESOURCES $PKG_DEST
 # Install RBTools and dependencies.
 #
 # We start off by building a wheel distribution, which we can build in
-# "release" package mode, fixing egg filenames. We'll build one per Python
-# version, in order to ensure we're packaging version-specific dependencies
-# correctly. Then we install that using pip, ensuring we have modern packages
-# with all dependencies installed.
+# "release" package mode. It's a universal wheel, so we'll build just one.
+# Then we install that using pip for each version of Python, ensuring we
+# have modern packages with all dependencies installed.
+echo
+echo
+echo == Building Wheel package ==
+echo
+/usr/bin/python2.7 ./setup.py release bdist_wheel \
+    -b ${PKG_PYBUILD}/build \
+    -d ${PKG_PYBUILD}/dist
+
+RBTOOLS_PACKAGE_FILENAME=${PKG_PYBUILD}/dist/RBTools-*-py2.py3-none-any.whl
+
 for pyver in "${PYTHON_VERSIONS[@]}"; do
-    echo
-    echo
-    echo == Building package for Python ${pyver} ==
-    echo
-    /usr/bin/python${pyver} ./setup.py release bdist_wheel \
-        -b ${PKG_PYBUILD}/build \
-        -d ${PKG_PYBUILD}/dist
-
-    RBTOOLS_PY2_FILENAME=${PKG_PYBUILD}/dist/RBTools-*-py2-none-any.whl
-
-    # Build the package, and set the PYTHONPATH to the location used for
-    # the upstream Python installer builds, so that if upstream Python is
-    # installed, we'll make use of the newer modules there (like pip) instead
-    # of the system Python.
+    # Set the PYTHONPATH to the location used for the upstream Python installer
+    # builds, so that if upstream Python is installed, we'll make use of the
+    # newer modules there (like pip) instead of the system Python.
     #
-    # We're also going to invoke pip as a module, to avoid having to figure
-    # out the right file path.
+    # We're also going to invoke pip as a module, to avoid having to figure out
+    # the right file path.
     PYTHONPATH=/Library/Frameworks/Python.framework/Versions/${pyver}/lib/python${pyver}/site-packages \
     /usr/bin/python${pyver} -m pip.__main__ install \
         $PIP_INSTALL_ARGS \
-        $RBTOOLS_PY2_FILENAME
+        $RBTOOLS_PACKAGE_FILENAME
 
-    rm $RBTOOLS_PY2_FILENAME
+    rm $RBTOOLS_PACKAGE_FILENAME
 done
 
 # Fix up the /usr/local/bin/rbt script to try to use the version of Python in
@@ -119,8 +120,8 @@ done
 # If the user has a custom Python installed (from the official Python
 # installers or from Homebrew), this will favor those.
 #
-# If they have Python 3 as the default for "python", then they're going to
-# have a bad time.
+# If they have Python 3 as the default for "python" (which must be an explicit
+# choice on their end), then they're going to have a bad time.
 echo "#!/usr/bin/env python
 
 # -*- coding: utf-8 -*-
diff --git a/contrib/installers/windows/build-installer.bat b/contrib/installers/windows/build-installer.bat
index ea18b438030424dad512573a30f223616a343b12..e470659fcc28150cd17e99230b0fb4aceec54cab 100644
--- a/contrib/installers/windows/build-installer.bat
+++ b/contrib/installers/windows/build-installer.bat
@@ -3,9 +3,8 @@
 ::
 :: This will fetch Python and build a WiX package for RBTools.
 ::
-:: This can be run in an automated fashion, but it must be run manually the
-:: first time in order to handle the installation of Portable Python (since
-:: there's no way to do a minimal silent install).
+:: This requires that curl for Windows (and its ca-bundle) are properly
+:: installed and working in order to download files.
 ::
 @echo off
 setlocal
@@ -39,9 +38,9 @@ call :SetMSBuildPath || goto :Abort
 ::-------------------------------------------------------------------------
 :: Dependencies
 ::-------------------------------------------------------------------------
-set PYTHON_VERSION=2.7.13
-set PYTHON_URL=http://python.org/ftp/python/2.7.13/python-2.7.13.msi
-set PYTHON_MD5=0f057ab4490e63e528eaa4a70df711d9
+set PYTHON_VERSION=2.7.15
+set PYTHON_URL=https://www.python.org/ftp/python/%PYTHON_VERSION%/python-%PYTHON_VERSION%.msi
+set PYTHON_MD5=023e49c9fba54914ebc05c4662a93ffe
 set PYTHON_DEP=%DEPS_DIR%\python-%PYTHON_VERSION%
 
 
@@ -300,8 +299,7 @@ set _dest=%~2
 
 echo Downloading %_url% to %_dest%...
 
-PowerShell -Command ^
-    "(New-Object Net.WebClient).DownloadFile('%_url%', '%_dest%')" || exit /B 1
+curl "%_url%" -o "%_dest%" || exit /B 1
 
 echo Downloaded %_url%
 
@@ -324,14 +322,16 @@ PowerShell -Command ^
  "$file = [System.IO.File]::ReadAllBytes('%_filename%');"^
  "$hash = [System.BitConverter]::ToString($md5.ComputeHash($file));"^
  "$hash = $hash.toLower().Replace('-', '');"^
+ "Write-Host '%_filename% has hash $hash.;"^
  "if ($hash -eq '%_expected_hash%') {"^
  "    exit 0;"^
  "} else {"^
  "    Write-Host 'Invalid checksum for %_filename%.';"^
- "    Write-Host 'Got'$hash'; expected %_expected_hash%.';"^
  "    exit 1;"^
  "}" || exit /B 1
 
+echo Hash verified.
+
 goto :EOF
 
 
diff --git a/contrib/installers/windows/scripts/get-version.py b/contrib/installers/windows/scripts/get-version.py
index d4d7350121975ab9b7c52c2a2eedb740256b3aaf..ae1291db5b9aa8a6c52a7895a0537dfef579bdc2 100644
--- a/contrib/installers/windows/scripts/get-version.py
+++ b/contrib/installers/windows/scripts/get-version.py
@@ -5,4 +5,7 @@ from rbtools import VERSION
 
 # MSI files only use the first 3 version fields, and has no concept of
 # alphas/betas/RCs/patch levels.
-print('%s.%s.%s' % (VERSION[0], VERSION[1], VERSION[2]))
+if VERSION[2] == 0:
+    print('%s.%s' % VERSION[:2])
+else:
+    print('%s.%s.%s' % VERSION[:3])
diff --git a/contrib/internal/release.py b/contrib/internal/release.py
deleted file mode 100755
index 3b5bb57ba65d238d87fd26b4b6446c2945ca8ffc..0000000000000000000000000000000000000000
--- a/contrib/internal/release.py
+++ /dev/null
@@ -1,234 +0,0 @@
-#!/usr/bin/env python
-#
-# Performs a release of RBTools. This can only be run by the core
-# developers with release permissions.
-#
-from __future__ import print_function, unicode_literals
-
-import hashlib
-import mimetools
-import os
-import shutil
-import subprocess
-import sys
-import tempfile
-import urllib2
-
-from fabazon.s3 import S3Bucket
-
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
-from rbtools import __version__, __version_info__, is_release  # NoQA E402
-
-
-PY_VERSIONS = ["2.6", "2.7"]
-
-LATEST_PY_VERSION = PY_VERSIONS[-1]
-
-PACKAGE_NAME = 'RBTools'
-
-RELEASES_BUCKET_NAME = 'downloads.reviewboard.org'
-RELEASES_BUCKET_KEY = '/releases/%s/%s.%s/' % (PACKAGE_NAME,
-                                               __version_info__[0],
-                                               __version_info__[1])
-
-RBWEBSITE_API_URL = 'http://www.reviewboard.org/api/'
-RELEASES_API_URL = '%sproducts/rbtools/releases/' % RBWEBSITE_API_URL
-
-
-built_files = []
-
-
-def load_config():
-    filename = os.path.join(os.path.expanduser('~'), '.rbwebsiterc')
-
-    if not os.path.exists(filename):
-        sys.stderr.write("A .rbwebsiterc file must exist in the form of:\n")
-        sys.stderr.write("\n")
-        sys.stderr.write("USERNAME = '<username>'\n")
-        sys.stderr.write("PASSWORD = '<password>'\n")
-        sys.exit(1)
-
-    user_config = {}
-
-    try:
-        exec(open(filename).read(), user_config)
-    except SyntaxError as e:
-        sys.stderr.write('Syntax error in config file: %s\n'
-                         'Line %i offset %i\n'
-                         % (filename, e.lineno, e.offset))
-        sys.exit(1)
-
-    auth_handler = urllib2.HTTPBasicAuthHandler()
-    auth_handler.add_password(realm='Web API',
-                              uri=RBWEBSITE_API_URL,
-                              user=user_config['USERNAME'],
-                              passwd=user_config['PASSWORD'])
-    opener = urllib2.build_opener(auth_handler)
-    urllib2.install_opener(opener)
-
-
-def execute(cmdline):
-    if isinstance(cmdline, list):
-        print(">>> %s" % subprocess.list2cmdline(cmdline))
-    else:
-        print(">>> %s" % cmdline)
-
-    p = subprocess.Popen(cmdline,
-                         shell=True,
-                         stdout=subprocess.PIPE)
-
-    s = ''
-
-    for data in p.stdout.readlines():
-        s += data
-        sys.stdout.write(data)
-
-    rc = p.wait()
-
-    if rc != 0:
-        print("!!! Error invoking command.")
-        sys.exit(1)
-
-    return s
-
-
-def run_setup(target, pyver=LATEST_PY_VERSION):
-    execute("python%s ./setup.py release %s" % (pyver, target))
-
-
-def clone_git_tree(git_dir):
-    new_git_dir = tempfile.mkdtemp(prefix='rbtools-release.')
-
-    os.chdir(new_git_dir)
-    execute('git clone %s .' % git_dir)
-
-    return new_git_dir
-
-
-def build_targets():
-    for pyver in PY_VERSIONS:
-        run_setup('bdist_egg', pyver)
-        built_files.append(('dist/%s-%s-py%s.egg'
-                            % (PACKAGE_NAME, __version__, pyver),
-                            'application/octet-stream'))
-
-    run_setup('sdist')
-    built_files.append(('dist/%s-%s.tar.gz' % (PACKAGE_NAME, __version__),
-                        'application/x-tar'))
-
-
-def build_checksums():
-    sha_filename = 'dist/%s-%s.sha256sum' % (PACKAGE_NAME, __version__)
-    out_f = open(sha_filename, 'w')
-
-    for filename, mimetype in built_files:
-        m = hashlib.sha256()
-
-        in_f = open(filename, 'r')
-        m.update(in_f.read())
-        in_f.close()
-
-        out_f.write('%s  %s\n' % (m.hexdigest(), os.path.basename(filename)))
-
-    out_f.close()
-    built_files.append((sha_filename, 'text/plain'))
-
-
-def upload_files():
-    bucket = S3Bucket(RELEASES_BUCKET_NAME)
-
-    for filename, mimetype in built_files:
-        bucket.upload(filename,
-                      '%s/%s' % (RELEASES_BUCKET_KEY,
-                                 filename.split('/')[-1]),
-                      mimetype=mimetype,
-                      public=True)
-
-    bucket.upload_directory_index(RELEASES_BUCKET_KEY)
-
-    # This may be a new directory, so rebuild the parent as well.
-    parent_key = '/'.join(RELEASES_BUCKET_KEY.split('/')[:-2])
-    bucket.upload_directory_index(parent_key)
-
-
-def tag_release():
-    execute("git tag release-%s" % __version__)
-
-
-def register_release():
-    if __version_info__[4] == 'final':
-        run_setup("register")
-
-    scm_revision = execute(['git rev-parse', 'release-%s' % __version__])
-
-    data = {
-        'major_version': __version_info__[0],
-        'minor_version': __version_info__[1],
-        'micro_version': __version_info__[2],
-        'patch_version': __version_info__[3],
-        'release_type': __version_info__[4],
-        'release_num': __version_info__[5],
-        'scm_revision': scm_revision,
-    }
-
-    boundary = mimetools.choose_boundary()
-    content = ''
-
-    for key, value in data.iteritems():
-        content += '--%s\r\n' % boundary
-        content += 'Content-Disposition: form-data; name="%s"\r\n' % key
-        content += '\r\n'
-        content += str(value) + '\r\n'
-
-    content += '--%s--\r\n' % boundary
-    content += '\r\n'
-
-    headers = {
-        'Content-Type': 'multipart/form-data; boundary=%s' % boundary,
-        'Content-Length': str(len(content)),
-    }
-
-    print('Posting release to reviewboard.org')
-    try:
-        f = urllib2.urlopen(urllib2.Request(url=RELEASES_API_URL, data=content,
-                                            headers=headers))
-        f.read()
-    except urllib2.HTTPError as e:
-        print("Error uploading. Got HTTP code %d:" % e.code)
-        print(e.read())
-    except urllib2.URLError as e:
-        try:
-            print("Error uploading. Got URL error:" % e.code)
-            print(e.read())
-        except AttributeError:
-            pass
-
-
-def main():
-    if not os.path.exists("setup.py"):
-        sys.stderr.write("This must be run from the root of the "
-                         "RBTools tree.\n")
-        sys.exit(1)
-
-    load_config()
-
-    if not is_release():
-        sys.stderr.write("This version is not listed as a release.\n")
-        sys.exit(1)
-
-    cur_dir = os.getcwd()
-    git_dir = clone_git_tree(cur_dir)
-
-    build_targets()
-    build_checksums()
-    upload_files()
-
-    os.chdir(cur_dir)
-    shutil.rmtree(git_dir)
-
-    tag_release()
-    register_release()
-
-
-if __name__ == "__main__":
-    main()
diff --git a/setup.cfg b/setup.cfg
index 2e90b83b066679f4b978e3530078cff9baaa1211..276adc37c125ba94c741eb9285d67caff1465cef 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,3 +1,7 @@
+[bdist_wheel]
+universal = 1
+
+
 [flake8]
 ignore = E121,E125,E129,E241
 exclude = appdirs.py, lib, docs, bin
diff --git a/setup.py b/setup.py
index 630a2bfd3a1e6cc56e8a8620a003eb3f86cc24ce..b51ae584666f13dbf210524f1acb9f2b244d3989 100755
--- a/setup.py
+++ b/setup.py
@@ -32,24 +32,6 @@ from setuptools import setup, find_packages
 from rbtools import get_package_version, is_release, VERSION
 
 
-PACKAGE_NAME = 'RBTools'
-
-if is_release():
-    download_url = "http://downloads.reviewboard.org/releases/%s/%s.%s/" % \
-                   (PACKAGE_NAME, VERSION[0], VERSION[1])
-else:
-    download_url = "http://downloads.reviewboard.org/nightlies/"
-
-
-install_requires = [
-    'backports.shutil_get_terminal_size',
-    'colorama',
-    'six>=1.8.0',
-    'texttable',
-    'tqdm',
-]
-
-
 # Make sure this is a version of Python we are compatible with. This should
 # prevent people on older versions from unintentionally trying to install
 # the source tarball, and failing.
@@ -71,12 +53,12 @@ elif sys.hexversion < 0x02070000:
         'Please install RBTools 0.7.x or upgrade Python to at least '
         '2.7.x.\n' % get_package_version())
     sys.exit(1)
-    install_requires.append('argparse')
 elif 0x03000000 <= sys.hexversion < 0x03050000:
     sys.stderr.write(
         'RBTools %s is incompatible with your version of Python.\n'
         'Please use either Python 2.7 or 3.5+.\n'
         % get_package_version())
+    sys.exit(1)
 
 
 rb_commands = [
@@ -112,33 +94,60 @@ scm_clients = [
     'tfs = rbtools.clients.tfs:TFSClient',
 ]
 
-setup(name=PACKAGE_NAME,
-      version=get_package_version(),
-      license="MIT",
-      description="Command line tools for use with Review Board",
-      entry_points={
-          'console_scripts': [
-              'rbt = rbtools.commands.main:main',
-          ],
-          'rbtools_commands': rb_commands,
-          'rbtools_scm_clients': scm_clients,
-      },
-      install_requires=install_requires,
-      dependency_links=[
-          download_url,
-      ],
-      packages=find_packages(),
-      include_package_data=True,
-      maintainer="Christian Hammond",
-      maintainer_email="chipx86@chipx86.com",
-      url="http://www.reviewboard.org/",
-      download_url=download_url,
-      classifiers=[
-          "Development Status :: 4 - Beta",
-          "Environment :: Console",
-          "Intended Audience :: Developers",
-          "License :: OSI Approved :: MIT License",
-          "Operating System :: OS Independent",
-          "Programming Language :: Python",
-          "Topic :: Software Development",
-      ])
+
+PACKAGE_NAME = 'RBTools'
+
+with open('README.md') as fp:
+    long_description = fp.read()
+
+
+setup(
+    name=PACKAGE_NAME,
+    version=get_package_version(),
+    license='MIT',
+    description=(
+        'Command line tools and API for working with code and document '
+        'reviews on Review Board'
+    ),
+    long_description=long_description,
+    long_description_content_type='text/markdown',
+    author='Beanbag, Inc.',
+    author_email='reviewboard@googlegroups.com',
+    entry_points={
+        'console_scripts': [
+            'rbt = rbtools.commands.main:main',
+        ],
+        'rbtools_commands': rb_commands,
+        'rbtools_scm_clients': scm_clients,
+    },
+    install_requires=[
+        'backports.shutil_get_terminal_size',
+        'colorama',
+        'six>=1.8.0',
+        'texttable',
+        'tqdm',
+    ],
+    packages=find_packages(exclude=['tests']),
+    include_package_data=True,
+    url='https://www.reviewboard.org/downloads/rbtools/',
+    download_url=('https://downloads.reviewboard.org/releases/%s/%s.%s/'
+                  % (PACKAGE_NAME, VERSION[0], VERSION[1])),
+    python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*',
+    classifiers=[
+        'Development Status :: 5 - Production/Stable',
+        'Environment :: Console',
+        'Framework :: Review Board',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: MIT License',
+        'Natural Language :: English',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3',
+        'Programming Language :: Python :: 3.5',
+        'Programming Language :: Python :: 3.6',
+        'Topic :: Software Development',
+        'Topic :: Software Development :: Quality Assurance',
+    ],
+)
