Added support for wheel building and uploading

To be used by the psycopg/psycopg2-wheels project.
This commit is contained in:
Daniele Varrazzo 2019-04-21 17:08:35 +01:00
parent 9eec303cf7
commit 637a990e09

View File

@ -18,7 +18,6 @@ from glob import glob
from pathlib import Path from pathlib import Path
from zipfile import ZipFile from zipfile import ZipFile
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from functools import lru_cache
from urllib.request import urlopen from urllib.request import urlopen
opt = None opt = None
@ -39,8 +38,7 @@ def main():
cmd() cmd()
@lru_cache() def setup_build_env():
def setup_env():
""" """
Set the environment variables according to the build environment Set the environment variables according to the build environment
""" """
@ -69,7 +67,7 @@ def setup_env():
if not (vc_dir() / r"bin\amd64\vcvars64.bat").exists(): if not (vc_dir() / r"bin\amd64\vcvars64.bat").exists():
logger.info("Fixing VS2010 Express and 64bit builds") logger.info("Fixing VS2010 Express and 64bit builds")
copy_file( copy_file(
clone_dir() / r"scripts\vcvars64-vs2010.bat", package_dir() / r"scripts\vcvars64-vs2010.bat",
vc_dir() / r"bin\amd64\vcvars64.bat", vc_dir() / r"bin\amd64\vcvars64.bat",
) )
@ -91,6 +89,17 @@ def step_install():
configure_sdk() configure_sdk()
configure_postgres() configure_postgres()
if is_wheel():
install_wheel_support()
def install_wheel_support():
"""
Install an up-to-date pip wheel package to build wheels.
"""
run_command([py_exe()] + "-m pip install --upgrade pip".split())
run_command([py_exe()] + "-m pip install wheel".split())
def configure_sdk(): def configure_sdk():
# The program rc.exe on 64bit with some versions look in the wrong path # The program rc.exe on 64bit with some versions look in the wrong path
@ -106,7 +115,9 @@ def configure_sdk():
def configure_postgres(): def configure_postgres():
# Change PostgreSQL config before service starts """
Set up PostgreSQL config before the service starts.
"""
logger.info("Configuring Postgres") logger.info("Configuring Postgres")
with (pg_data_dir() / 'postgresql.conf').open('a') as f: with (pg_data_dir() / 'postgresql.conf').open('a') as f:
# allow > 1 prepared transactions for test cases # allow > 1 prepared transactions for test cases
@ -144,18 +155,21 @@ def configure_postgres():
def run_openssl(args): def run_openssl(args):
"""Run the appveyor-installed openssl""" """Run the appveyor-installed openssl with some args."""
# https://www.appveyor.com/docs/windows-images-software/ # https://www.appveyor.com/docs/windows-images-software/
openssl = Path(r"C:\OpenSSL-v111-Win64") / 'bin' / 'openssl' openssl = Path(r"C:\OpenSSL-v111-Win64") / 'bin' / 'openssl'
return run_command([openssl] + args) return run_command([openssl] + args)
def step_build_script(): def step_build_script():
setup_env() setup_build_env()
build_openssl() build_openssl()
build_libpq() build_libpq()
build_psycopg() build_psycopg()
if is_wheel():
build_binary_packages()
def build_openssl(): def build_openssl():
top = base_dir() / 'openssl' top = base_dir() / 'openssl'
@ -305,14 +319,9 @@ $config->{openssl} = "%s";
def build_psycopg(): def build_psycopg():
os.chdir(clone_dir()) os.chdir(package_dir())
patch_package_name()
# Find the pg_config just built add_pg_config_path()
path = os.pathsep.join(
[str(base_dir() / r'postgresql\bin'), os.environ['PATH']]
)
setenv('PATH', path)
run_command( run_command(
[py_exe(), "setup.py", "build_ext", "--have-ssl"] [py_exe(), "setup.py", "build_ext", "--have-ssl"]
+ ["-l", "libpgcommon", "-l", "libpgport"] + ["-l", "libpgcommon", "-l", "libpgport"]
@ -320,25 +329,87 @@ def build_psycopg():
+ ['-I', base_dir() / r'openssl\include'] + ['-I', base_dir() / r'openssl\include']
) )
run_command([py_exe(), "setup.py", "build_py"]) run_command([py_exe(), "setup.py", "build_py"])
def patch_package_name():
"""Change the psycopg2 package name in the setup.py if required."""
conf = os.environ.get('CONFIGURATION', 'psycopg2')
if conf == 'psycopg2':
return
logger.info("changing package name to %s", conf)
with (package_dir() / 'setup.py').open() as f:
data = f.read()
# Replace the name of the package with what desired
rex = re.compile(r"""name=["']psycopg2["']""")
assert len(rex.findall(data)) == 1, rex.findall(data)
data = rex.sub(f'name="{conf}"', data)
with (package_dir() / 'setup.py').open('w') as f:
f.write(data)
def build_binary_packages():
"""Create wheel/exe binary packages."""
os.chdir(package_dir())
add_pg_config_path()
# Build .exe packages for whom still use them
if os.environ['CONFIGURATION'] == 'psycopg2':
run_command([py_exe(), 'setup.py', 'bdist_wininst', "-d", dist_dir()])
# Build .whl packages
run_command([py_exe(), 'setup.py', 'bdist_wheel', "-d", dist_dir()])
def step_after_build():
if not is_wheel():
install_built_package()
else:
install_binary_package()
def install_built_package():
"""Install the package just built by setup build."""
os.chdir(package_dir())
# Install the psycopg just built
add_pg_config_path()
run_command([py_exe(), "setup.py", "install"]) run_command([py_exe(), "setup.py", "install"])
shutil.rmtree("psycopg2.egg-info") shutil.rmtree("psycopg2.egg-info")
def install_binary_package():
"""Install the package from a packaged wheel."""
run_command(
[py_exe(), '-m', 'pip', 'install', '--no-index', '-f', dist_dir()]
+ [os.environ['CONFIGURATION']]
)
def add_pg_config_path():
"""Allow finding in the path the pg_config just built."""
pg_path = str(base_dir() / r'postgresql\bin')
if pg_path not in os.environ['PATH'].split(os.pathsep):
setenv('PATH', os.pathsep.join([pg_path, os.environ['PATH']]))
def step_before_test(): def step_before_test():
# Add PostgreSQL binaries to the path print_psycopg2_version()
setenv('PATH', os.pathsep.join([str(pg_bin_dir()), os.environ['PATH']]))
# Create and setup PostgreSQL database for the tests # Create and setup PostgreSQL database for the tests
run_command(['createdb', os.environ['PSYCOPG2_TESTDB']]) run_command([pg_bin_dir() / 'createdb', os.environ['PSYCOPG2_TESTDB']])
run_command( run_command(
['psql', '-d', os.environ['PSYCOPG2_TESTDB']] [pg_bin_dir() / 'psql', '-d', os.environ['PSYCOPG2_TESTDB']]
+ ['-c', "CREATE EXTENSION hstore"] + ['-c', "CREATE EXTENSION hstore"]
) )
def step_after_build(): def print_psycopg2_version():
# Print psycopg and libpq versions """Print psycopg2 and libpq versions installed."""
for expr in ( for expr in (
'psycopg2.__version__', 'psycopg2.__version__',
'psycopg2.__libpq_version__', 'psycopg2.__libpq_version__',
@ -349,6 +420,36 @@ def step_after_build():
def step_test_script(): def step_test_script():
check_libpq_version()
run_test_suite()
def check_libpq_version():
"""
Fail if the package installed is not using the expected libpq version.
"""
want_ver = tuple(map(int, os.environ['POSTGRES_VERSION'].split('_')))
want_ver = "%d%04d" % want_ver
got_ver = (
out_command(
[py_exe(), '-c']
+ ["import psycopg2; print(psycopg2.extensions.libpq_version())"]
)
.decode('ascii')
.rstrip()
)
assert want_ver == got_ver, "libpq version mismatch: %r != %r" % (
want_ver,
got_ver,
)
def run_test_suite():
# Remove this var, which would make badly a configured OpenSSL 1.1 work
os.environ.pop('OPENSSL_CONF', None)
# Run the unit test
os.chdir(package_dir())
run_command( run_command(
[py_exe(), '-c'] [py_exe(), '-c']
+ ["import tests; tests.unittest.main(defaultTest='tests.test_suite')"] + ["import tests; tests.unittest.main(defaultTest='tests.test_suite')"]
@ -356,6 +457,72 @@ def step_test_script():
) )
def step_on_success():
print_sha1_hashes()
if setup_ssh():
upload_packages()
def print_sha1_hashes():
"""
Print the packages sha1 so their integrity can be checked upon signing.
"""
logger.info("artifacts SHA1 hashes:")
os.chdir(package_dir() / 'dist')
run_command([which('sha1sum'), '-b', f'psycopg2-*/*'])
def setup_ssh():
"""
Configure ssh to upload built packages where they can be retrieved.
Return False if can't configure and upload shoould be skipped.
"""
# If we are not on the psycopg AppVeyor account, the environment variable
# REMOTE_KEY will not be decrypted. In that case skip uploading.
if os.environ['APPVEYOR_ACCOUNT_NAME'] != 'psycopg':
logger.warn("skipping artifact upload: you are not psycopg")
return False
pkey = os.environ.get('REMOTE_KEY', None)
if not pkey:
logger.warn("skipping artifact upload: no remote key")
return False
# Write SSH Private Key file from environment variable
pkey = pkey.replace(' ', '\n')
with (clone_dir() / 'id_rsa').open('w') as f:
f.write(
f"""\
-----BEGIN RSA PRIVATE KEY-----
{pkey}
-----END RSA PRIVATE KEY-----
"""
)
# Make a directory to please MinGW's version of ssh
ensure_dir(r"C:\MinGW\msys\1.0\home\appveyor\.ssh")
return True
def upload_packages():
# Upload built artifacts
logger.info("uploading artifacts")
ssh_cmd = r"C:\MinGW\msys\1.0\bin\ssh -i %s -o UserKnownHostsFile=%s" % (
clone_dir() / "id_rsa",
clone_dir() / 'known_hosts',
)
os.chdir(package_dir())
run_command(
[r"C:\MinGW\msys\1.0\bin\rsync", "-avr"]
+ ["-e", ssh_cmd, "dist/", "upload@initd.org:"]
)
def download(url, fn): def download(url, fn):
"""Download a file locally""" """Download a file locally"""
logger.info("downloading %s", url) logger.info("downloading %s", url)
@ -523,6 +690,42 @@ def build_dir():
return ensure_dir(rv) return ensure_dir(rv)
def package_dir():
"""
Return the directory containing the psycopg code checkout dir
Building psycopg is clone_dir(), building the wheel packages is a submodule.
"""
return clone_dir() / 'psycopg2' if is_wheel() else clone_dir()
def is_wheel():
"""
Return whether we are building the wheel packages or just the extension.
"""
project_name = os.environ['APPVEYOR_PROJECT_NAME']
if project_name == 'psycopg2':
return False
elif project_name == 'psycopg2-wheels':
return True
else:
raise Exception(f"unexpected project name: {project_name}")
def dist_dir():
return package_dir() / 'dist' / ('psycopg2-%s' % package_version())
def package_version():
with (package_dir() / 'setup.py').open() as f:
data = f.read()
m = re.search(
r"""^PSYCOPG_VERSION\s*=\s*['"](.*)['"]""", data, re.MULTILINE
)
return m.group(1)
def ensure_dir(dir): def ensure_dir(dir):
if not isinstance(dir, Path): if not isinstance(dir, Path):
dir = Path(dir) dir = Path(dir)