diff options
Diffstat (limited to 'windows-installer/construct.py')
-rw-r--r-- | windows-installer/construct.py | 192 |
1 files changed, 192 insertions, 0 deletions
diff --git a/windows-installer/construct.py b/windows-installer/construct.py new file mode 100644 index 000000000..f0724f5f4 --- /dev/null +++ b/windows-installer/construct.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +import contextlib +import ctypes +import os +import shutil +import struct +import subprocess +import sys +import tempfile +import time + +PYTHON_VERSION = (3, 7, 4) +PYTHON_BITNESS = 32 +PYWIN32_VERSION = 227 # do not forget to edit pywin32 dependency accordingly in setup.py +NSIS_VERSION = '3.04' + + +def main(): + build_path, repo_path, venv_path, venv_python = _prepare_environment() + + _copy_assets(build_path, repo_path) + + installer_cfg_path = _generate_pynsist_config(repo_path, build_path) + + _prepare_build_tools(venv_path, venv_python, repo_path) + _compile_wheels(repo_path, build_path, venv_python) + _build_installer(installer_cfg_path, venv_path) + + print('Done') + + +def _build_installer(installer_cfg_path, venv_path): + print('Build the installer') + subprocess.check_call([os.path.join(venv_path, 'Scripts', 'pynsist.exe'), installer_cfg_path]) + + +def _compile_wheels(repo_path, build_path, venv_python): + print('Compile wheels') + + wheels_path = os.path.join(build_path, 'wheels') + os.makedirs(wheels_path) + + certbot_packages = ['acme', 'certbot'] + # Uncomment following line to include all DNS plugins in the installer + # certbot_packages.extend([name for name in os.listdir(repo_path) if name.startswith('certbot-dns-')]) + wheels_project = [os.path.join(repo_path, package) for package in certbot_packages] + + with _prepare_constraints(repo_path) as constraints_file_path: + command = [venv_python, '-m', 'pip', 'wheel', '-w', wheels_path, '--constraint', constraints_file_path] + command.extend(wheels_project) + subprocess.check_call(command) + + +def _prepare_build_tools(venv_path, venv_python, repo_path): + print('Prepare build tools') + subprocess.check_call([sys.executable, '-m', 'venv', venv_path]) + subprocess.check_call([venv_python, os.path.join(repo_path, 'letsencrypt-auto-source', 'pieces', 'pipstrap.py')]) + subprocess.check_call([venv_python, os.path.join(repo_path, 'tools', 'pip_install.py'), 'pynsist']) + subprocess.check_call(['choco', 'upgrade', '--allow-downgrade', '-y', 'nsis', '--version', NSIS_VERSION]) + + +@contextlib.contextmanager +def _prepare_constraints(repo_path): + requirements = os.path.join(repo_path, 'letsencrypt-auto-source', 'pieces', 'dependency-requirements.txt') + constraints = subprocess.check_output( + [sys.executable, os.path.join(repo_path, 'tools', 'strip_hashes.py'), requirements], + universal_newlines=True) + workdir = tempfile.mkdtemp() + try: + constraints_file_path = os.path.join(workdir, 'constraints.txt') + with open(constraints_file_path, 'a') as file_h: + file_h.write(constraints) + file_h.write('pywin32=={0}'.format(PYWIN32_VERSION)) + yield constraints_file_path + finally: + shutil.rmtree(workdir) + + +def _copy_assets(build_path, repo_path): + print('Copy assets') + if os.path.exists(build_path): + os.rename(build_path, '{0}.{1}.bak'.format(build_path, int(time.time()))) + os.makedirs(build_path) + shutil.copy(os.path.join(repo_path, 'windows-installer', 'certbot.ico'), build_path) + shutil.copy(os.path.join(repo_path, 'windows-installer', 'run.bat'), build_path) + shutil.copy(os.path.join(repo_path, 'windows-installer', 'template.nsi'), build_path) + shutil.copy(os.path.join(repo_path, 'windows-installer', 'renew-up.ps1'), build_path) + shutil.copy(os.path.join(repo_path, 'windows-installer', 'renew-down.ps1'), build_path) + + +def _generate_pynsist_config(repo_path, build_path): + print('Generate pynsist configuration') + + pywin32_paths_file = os.path.join(build_path, 'pywin32_paths.py') + + # Pywin32 uses non-standard folders to hold its packages. We need to instruct pynsist bootstrap + # explicitly to add them into sys.path. This is done with a custom "pywin32_paths.py" that is + # referred in the pynsist configuration as an "extra_preamble". + # Reference example: https://github.com/takluyver/pynsist/tree/master/examples/pywebview + with open(pywin32_paths_file, 'w') as file_h: + file_h.write('''\ +pkgdir = os.path.join(os.path.dirname(installdir), 'pkgs') + +sys.path.extend([ + os.path.join(pkgdir, 'win32'), + os.path.join(pkgdir, 'win32', 'lib'), +]) + +# Preload pywintypes and pythoncom +pwt = os.path.join(pkgdir, 'pywin32_system32', 'pywintypes{0}{1}.dll') +pcom = os.path.join(pkgdir, 'pywin32_system32', 'pythoncom{0}{1}.dll') +import warnings +with warnings.catch_warnings(): + warnings.simplefilter("ignore") + import imp +imp.load_dynamic('pywintypes', pwt) +imp.load_dynamic('pythoncom', pcom) +'''.format(PYTHON_VERSION[0], PYTHON_VERSION[1])) + + installer_cfg_path = os.path.join(build_path, 'installer.cfg') + + certbot_pkg_path = os.path.join(repo_path, 'certbot') + certbot_version = subprocess.check_output([sys.executable, '-c', 'import certbot; print(certbot.__version__)'], + universal_newlines=True, cwd=certbot_pkg_path).strip() + + with open(installer_cfg_path, 'w') as file_h: + file_h.write('''\ +[Application] +name=Certbot +version={certbot_version} +icon=certbot.ico +publisher=Electronic Frontier Foundation +target=$INSTDIR\\run.bat + +[Build] +directory=nsis +nsi_template=template.nsi +installer_name=certbot-beta-installer-{installer_suffix}.exe + +[Python] +version={python_version} +bitness={python_bitness} + +[Include] +local_wheels=wheels\\*.whl +files=run.bat + renew-up.ps1 + renew-down.ps1 + +[Command certbot] +entry_point=certbot.main:main +extra_preamble=pywin32_paths.py +'''.format(certbot_version=certbot_version, + installer_suffix='win_amd64' if PYTHON_BITNESS == 64 else 'win32', + python_bitness=PYTHON_BITNESS, + python_version='.'.join([str(item) for item in PYTHON_VERSION]))) + + return installer_cfg_path + + +def _prepare_environment(): + print('Prepare environment') + try: + subprocess.check_output(['choco', '--version']) + except subprocess.CalledProcessError: + raise RuntimeError('Error: Chocolatey (https://chocolatey.org/) needs ' + 'to be installed to run this script.') + script_path = os.path.realpath(__file__) + repo_path = os.path.dirname(os.path.dirname(script_path)) + build_path = os.path.join(repo_path, 'windows-installer', 'build') + venv_path = os.path.join(build_path, 'venv-config') + venv_python = os.path.join(venv_path, 'Scripts', 'python.exe') + + return build_path, repo_path, venv_path, venv_python + + +if __name__ == '__main__': + if not os.name == 'nt': + raise RuntimeError('This script must be run under Windows.') + + if ctypes.windll.shell32.IsUserAnAdmin() == 0: + # Administrator privileges are required to properly install NSIS through Chocolatey + raise RuntimeError('This script must be run with administrator privileges.') + + if sys.version_info[:2] != PYTHON_VERSION[:2]: + raise RuntimeError('This script must be run with Python {0}' + .format('.'.join([str(item) for item in PYTHON_VERSION[0:2]]))) + + if struct.calcsize('P') * 8 != PYTHON_BITNESS: + raise RuntimeError('This script must be run with a {0} bit version of Python.' + .format(PYTHON_BITNESS)) + main() |