├── .gitignore ├── .gitlab-ci.yml ├── LICENSE ├── README.md ├── chromedriver_binary ├── __init__.py └── utils.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | __pycache__/ 3 | dist/ 4 | *.egg-info* 5 | chromedriver_binary/chromedriver* 6 | *.pyc 7 | .* 8 | LICENSE.txt 9 | MANIFEST.in 10 | README.txt 11 | !.gitignore 12 | !.gitlab-ci.yml 13 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - build 3 | - test 4 | - deploy-github 5 | - deploy-pypi 6 | 7 | generate: 8 | stage: build 9 | image: python:3 10 | only: 11 | - schedule@kaiser/python-chromedriver-binary 12 | script: 13 | - apt-get update 14 | - apt-get install -y python3-venv sed ssh git 15 | - mkdir --mode=700 ~/.ssh/ 16 | - (umask 0377 && echo "$GITLAB_PRIVATE_KEY" > ~/.ssh/id_rsa 17 | && echo "iffgit.fz-juelich.de $GITLAB_HOST_KEY" >> ~/.ssh/known_hosts) 18 | - git config --global user.email "d.kaiser@fz-juelich.de" 19 | - git config --global user.name "Daniel Kaiser" 20 | - git remote set-url --push origin gitlab@iffgit.fz-juelich.de:kaiser/python-chromedriver-binary.git 21 | - python3 -m venv schedule-env 22 | - schedule-env/bin/python -m pip install requests 23 | - cp get_versions.py /tmp/ 24 | - for VERSION in `schedule-env/bin/python /tmp/get_versions.py`; do 25 | git checkout master; 26 | git checkout origin/schedule README.md setup.py; 27 | sed -i "s/@@CHROMEDRIVER_VERSION@@/${VERSION}/g" README.md; 28 | sed -i "s/@@CHROMEDRIVER_VERSION@@/${VERSION}/g" setup.py; 29 | git add README.md setup.py; 30 | git commit -m "Chromedriver version ${VERSION}"; 31 | git tag -a -m "v${VERSION}.0" "v${VERSION}.0"; 32 | git push -f origin master; 33 | git push -f origin --tags; 34 | done 35 | 36 | build: 37 | stage: build 38 | image: python:3 39 | only: 40 | - master@kaiser/python-chromedriver-binary 41 | - tags@kaiser/python-chromedriver-binary 42 | - chromedriver-binary-auto@kaiser/python-chromedriver-binary 43 | before_script: 44 | - python3 -m pip install setuptools 45 | script: 46 | - python3 setup.py sdist 47 | artifacts: 48 | paths: 49 | - dist/ 50 | expire_in: 1 week 51 | 52 | python3-test: 53 | stage: test 54 | image: python:3 55 | only: 56 | - master@kaiser/python-chromedriver-binary 57 | - tags@kaiser/python-chromedriver-binary 58 | script: 59 | - apt-get update 60 | - apt-get install -y python3-venv 61 | - apt-get install -y libglib2.0-0 libx11-6 libnss3 libx11-xcb1 libdbus-1-3 62 | - rm -rf chromedriver_binary 63 | - python3 -m venv env 64 | - env/bin/pip install dist/* 65 | - ls `env/bin/python3 -c "import chromedriver_binary; print(chromedriver_binary.chromedriver_filename)"` 66 | - VERSION=$(PATH=`env/bin/chromedriver-path` chromedriver -v | cut -d' ' -f2 | cut -d'.' -f1-4) 67 | - PY_VERS=$(env/bin/pip list --format=freeze | grep 'chromedriver-binary' | cut -d'=' -f3 | cut -d'.' -f1-4) 68 | - echo "Version $VERSION $PY_VERS" 69 | - test "$VERSION" = "$PY_VERS" 70 | dependencies: 71 | - build 72 | 73 | python2-test: 74 | stage: test 75 | image: python:2 76 | only: 77 | - master@kaiser/python-chromedriver-binary 78 | - tags@kaiser/python-chromedriver-binary 79 | script: 80 | - apt-get update 81 | - apt-get install -y virtualenv 82 | - apt-get install -y libglib2.0-0 libx11-6 libnss3 libx11-xcb1 libdbus-1-3 83 | - rm -rf chromedriver_binary 84 | - virtualenv env 85 | - env/bin/pip install --no-index dist/* 86 | - ls `env/bin/python2.7 -c "import chromedriver_binary; print(chromedriver_binary.chromedriver_filename)"` 87 | - VERSION=$(PATH=`env/bin/chromedriver-path` chromedriver -v | cut -d' ' -f2 | cut -d'.' -f1-4) 88 | - PY_VERS=$(env/bin/pip freeze | grep 'chromedriver-binary' | cut -d'=' -f3 | cut -d'.' -f1-4) 89 | - echo "Version $VERSION $PY_VERS" 90 | - test "$VERSION" = "$PY_VERS" 91 | dependencies: 92 | - build 93 | 94 | deploy-github: 95 | stage: deploy-github 96 | image: alpine 97 | only: 98 | - master@kaiser/python-chromedriver-binary 99 | - chromedriver-binary-auto@kaiser/python-chromedriver-binary 100 | - tags@kaiser/python-chromedriver-binary 101 | script: 102 | - apk add --no-cache openssh-client git 103 | - if [ -n "$CI_COMMIT_BRANCH" ]; then git checkout "$CI_COMMIT_BRANCH"; else git checkout master; fi 104 | - mkdir --mode=700 ~/.ssh/ 105 | - (umask 0377 && echo "$GITHUB_PRIVATE_KEY" > ~/.ssh/id_rsa 106 | && echo "github.com $GITHUB_HOST_KEY" >> ~/.ssh/known_hosts) 107 | - git config --global user.email "d.kaiser@fz-juelich.de" 108 | - git config --global user.name "Daniel Kaiser" 109 | - git remote add github git@github.com:danielkaiser/python-chromedriver-binary.git 110 | - git fetch github 111 | - if [ -n "$CI_COMMIT_BRANCH" ]; then git push -f github "$CI_COMMIT_BRANCH"; fi 112 | - if [ -n "$CI_COMMIT_TAG" ]; then git push -f github --tags; fi 113 | 114 | deploy-pypi: 115 | stage: deploy-pypi 116 | image: python:3 117 | only: 118 | - tags@kaiser/python-chromedriver-binary 119 | script: 120 | - rm -rf deploy && mkdir deploy && cd deploy 121 | - apt-get update 122 | - apt-get install -y python3-venv 123 | - python3 -m venv env 124 | - env/bin/python -m pip install --upgrade pip 125 | - env/bin/python -m pip install twine 126 | - echo "[distutils]" > ~/.pypirc 127 | - echo "index-servers =" >> ~/.pypirc 128 | - echo " pypi" >> ~/.pypirc 129 | - echo "[pypi]" >> ~/.pypirc 130 | - echo "username=$PYPIUSERNAME" >> ~/.pypirc 131 | - echo "password=$PYPIPASSWORD" >> ~/.pypirc 132 | - env/bin/twine upload --repository pypi ../dist/*.tar.gz 133 | dependencies: 134 | - build 135 | 136 | deploy-pypi-auto: 137 | stage: deploy-pypi 138 | image: python:3 139 | only: 140 | - chromedriver-binary-auto@kaiser/python-chromedriver-binary 141 | script: 142 | - rm -rf deploy && mkdir deploy && cd deploy 143 | - apt-get update 144 | - apt-get install -y python3-venv 145 | - python3 -m venv env 146 | - env/bin/python -m pip install --upgrade pip 147 | - env/bin/python -m pip install twine 148 | - echo "[distutils]" > ~/.pypirc 149 | - echo "index-servers =" >> ~/.pypirc 150 | - echo " pypi" >> ~/.pypirc 151 | - echo "[pypi]" >> ~/.pypirc 152 | - echo "username=$PYPIUSERNAME" >> ~/.pypirc 153 | - echo "password=$PYPIPASSWORD_AUTO" >> ~/.pypirc 154 | - env/bin/twine upload --repository pypi ../dist/*.tar.gz 155 | dependencies: 156 | - build 157 | 158 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Daniel Kaiser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chromedriver-binary 2 | Downloads and installs the [chromedriver](https://sites.google.com/a/chromium.org/chromedriver/) binary version 139.0.7225.0 for automated testing of webapps. The installer supports Linux, MacOS and Windows operating systems. 3 | 4 | Alternatively the package [chromedriver-binary-auto](https://pypi.org/project/chromedriver-binary-auto/) can be used to automatically detect the latest chromedriver version required for the installed Chrome/Chromium browser. 5 | 6 | ## Installation 7 | 8 | ### Latest and fixed versions 9 | 10 | #### From PyPI 11 | ``` 12 | pip install chromedriver-binary 13 | ``` 14 | 15 | #### From GitHub 16 | ``` 17 | pip install git+https://github.com/danielkaiser/python-chromedriver-binary.git 18 | ``` 19 | 20 | ### Automatically detected versions 21 | 22 | Please make sure to install Chrome or Chromium first and add the browser to the binary search path. 23 | 24 | #### From PyPI 25 | ``` 26 | pip install chromedriver-binary-auto 27 | ``` 28 | 29 | To redetect the required version and install the newest suitable chromedriver after the first installation simply reinstall the package using 30 | ``` 31 | pip install --upgrade --force-reinstall chromedriver-binary-auto 32 | ``` 33 | 34 | #### From GitHub 35 | ``` 36 | pip install git+https://github.com/danielkaiser/python-chromedriver-binary.git@chromedriver-binary-auto 37 | ``` 38 | 39 | If the installed chromedriver version does not match your browser's version please try to [empty pip's cache](https://pip.pypa.io/en/stable/cli/pip_cache/) or disable the cache during (re-)installation. 40 | 41 | ## Usage 42 | To use chromedriver just `import chromedriver_binary`. This will add the executable to your PATH so it will be found. You can also get the absolute filename of the binary with `chromedriver_binary.chromedriver_filename`. 43 | 44 | ### Example 45 | ``` 46 | from selenium import webdriver 47 | import chromedriver_binary # Adds chromedriver binary to path 48 | 49 | driver = webdriver.Chrome() 50 | driver.get("http://www.python.org") 51 | assert "Python" in driver.title 52 | ``` 53 | 54 | ### Exporting chromedriver binary path 55 | This package installs a small shell script `chromedriver-path` to easily set and export the PATH variable: 56 | ``` 57 | $ export PATH=$PATH:`chromedriver-path` 58 | ``` 59 | -------------------------------------------------------------------------------- /chromedriver_binary/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | This will add the executable to your PATH so it will be found. 4 | The filename of the binary is stored in `chromedriver_filename`. 5 | """ 6 | 7 | import os 8 | from . import utils 9 | 10 | 11 | def add_chromedriver_to_path(): 12 | """ 13 | Appends the directory of the chromedriver binary file to PATH. 14 | """ 15 | chromedriver_dir = os.path.abspath(os.path.dirname(__file__)) 16 | if 'PATH' not in os.environ: 17 | os.environ['PATH'] = chromedriver_dir 18 | elif chromedriver_dir not in os.environ['PATH']: 19 | os.environ['PATH'] = chromedriver_dir + utils.get_variable_separator() + os.environ['PATH'] 20 | 21 | 22 | chromedriver_filename = os.path.join(os.path.abspath(os.path.dirname(__file__)), utils.get_chromedriver_filename()) 23 | add_chromedriver_to_path() 24 | -------------------------------------------------------------------------------- /chromedriver_binary/utils.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | Helper functions for filename and URL generation. 4 | """ 5 | import json 6 | import sys 7 | import os 8 | import ssl 9 | import subprocess 10 | import re 11 | import platform 12 | 13 | try: 14 | from urllib.request import urlopen, URLError 15 | ssl_context = ssl.create_default_context() 16 | except ImportError: 17 | from urllib2 import urlopen, URLError 18 | ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS) 19 | 20 | __author__ = 'Daniel Kaiser ' 21 | 22 | 23 | def get_chromedriver_filename(): 24 | """ 25 | Returns the filename of the binary for the current platform. 26 | :return: Binary filename 27 | """ 28 | if sys.platform.startswith('win'): 29 | return 'chromedriver.exe' 30 | return 'chromedriver' 31 | 32 | 33 | def get_variable_separator(): 34 | """ 35 | Returns the environment variable separator for the current platform. 36 | :return: Environment variable separator 37 | """ 38 | if sys.platform.startswith('win'): 39 | return ';' 40 | return ':' 41 | 42 | 43 | def get_legacy_chromedriver_url(version): 44 | """ 45 | Generates the download URL for legacy releases 46 | :param version: chromedriver version string 47 | :return: Download URL for chromedriver 48 | """ 49 | base_url = 'https://chromedriver.storage.googleapis.com/' 50 | if sys.platform.startswith('linux') and sys.maxsize > 2 ** 32: 51 | _platform = 'linux' 52 | architecture = '64' 53 | elif sys.platform == 'darwin': 54 | _platform = 'mac' 55 | architecture = '64' 56 | if platform.machine() == 'arm64': 57 | if int(version.split('.')[0]) < 107: 58 | architecture += '_m1' 59 | else: 60 | architecture = '_arm64' 61 | elif sys.platform.startswith('win'): 62 | _platform = 'win' 63 | architecture = '32' 64 | else: 65 | raise RuntimeError('Could not determine chromedriver download URL for this platform.') 66 | return base_url + version + '/chromedriver_' + _platform + architecture + '.zip' 67 | 68 | 69 | def get_chromedriver_url(version): 70 | """ 71 | Generates the download URL for current platform, architecture and the given version. 72 | Supports Linux, macOS and Windows. 73 | :param version: chromedriver version string 74 | :return: Download URL for chromedriver 75 | """ 76 | if sys.platform.startswith('linux') and sys.maxsize > 2 ** 32: 77 | _platform = 'linux64' 78 | elif sys.platform == 'darwin' and platform.machine() == 'arm64': 79 | _platform = 'mac-arm64' 80 | elif sys.platform == 'darwin': 81 | _platform = 'mac-x64' 82 | elif sys.platform.startswith('win'): 83 | _platform = 'win' + '64' if sys.maxsize > 2 ** 32 else '32' 84 | else: 85 | raise RuntimeError('Could not determine chromedriver download URL for this platform.') 86 | response = urlopen("https://googlechromelabs.github.io/chrome-for-testing/latest-patch-versions-per-build-with-downloads.json", context=ssl_context) 87 | if int(version.split('.')[0]) >= 115: 88 | version = '.'.join(version.split('.')[:3]) # ensure major.minor.patch 89 | for p in json.load(response)["builds"][version]["downloads"]["chromedriver"]: 90 | if p["platform"] == _platform: 91 | return p["url"] 92 | else: 93 | return get_legacy_chromedriver_url(version) 94 | raise RuntimeError('Could not determine chromedriver download URL for this platform.') 95 | 96 | 97 | def find_binary_in_path(filename): 98 | """ 99 | Searches for a binary named `filename` in the current PATH. If an executable is found, its absolute path is returned 100 | else None. 101 | :param filename: Filename of the binary 102 | :return: Absolute path or None 103 | """ 104 | if 'PATH' not in os.environ: 105 | return None 106 | for directory in os.environ['PATH'].split(get_variable_separator()): 107 | binary = os.path.abspath(os.path.join(directory, filename)) 108 | if os.path.isfile(binary) and os.access(binary, os.X_OK): 109 | return binary 110 | return None 111 | 112 | 113 | def get_latest_legacy_release_for_version(version): 114 | """ 115 | Searches for the latest release (complete version string) for a given major `version` in the legacy storage. 116 | :param version: Major version number or None 117 | :return: Latest release for given version 118 | """ 119 | release_url = "https://chromedriver.storage.googleapis.com/LATEST_RELEASE" 120 | if version: 121 | release_url += '_{}'.format(version) 122 | try: 123 | response = urlopen(release_url, context=ssl_context) 124 | if response.getcode() != 200: 125 | raise URLError('Not Found') 126 | return response.read().decode('utf-8').strip() 127 | except URLError: 128 | raise RuntimeError('Failed to find release information: {}'.format(release_url)) 129 | 130 | 131 | def get_latest_release_for_version(version=None): 132 | """ 133 | Searches for the latest release (complete version string) for a given major `version`. If `version` is None 134 | the latest Stable release is returned. 135 | :param version: Major version number or None 136 | :return: Latest release for given version 137 | """ 138 | try: 139 | if version is None: 140 | response = urlopen("https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json", context=ssl_context) 141 | return json.load(response)["channels"]["Stable"]["version"] 142 | if int(version) < 113: 143 | return get_latest_legacy_release_for_version(version) 144 | response = urlopen("https://googlechromelabs.github.io/chrome-for-testing/latest-versions-per-milestone-with-downloads.json", context=ssl_context) 145 | return json.load(response)["milestones"][str(version)]["version"] 146 | except Exception: 147 | raise RuntimeError('Failed to find release information for version: {}'.format(version if version else "latest")) 148 | 149 | 150 | def get_chrome_major_version(): 151 | """ 152 | Detects the major version number of the installed chrome/chromium browser. 153 | :return: The browsers major version number or None 154 | """ 155 | browser_executables = ['google-chrome', 'chrome', 'chrome-browser', 'google-chrome-stable', 'chromium', 'chromium-browser'] 156 | if sys.platform == "darwin": 157 | browser_executables.insert(0, "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome") 158 | 159 | for browser_executable in browser_executables: 160 | try: 161 | version = subprocess.check_output([browser_executable, '--version']) 162 | return re.match(r'.*?((?P\d+)\.(\d+\.){2,3}\d+).*?', version.decode('utf-8')).group('major') 163 | except Exception: 164 | pass 165 | 166 | 167 | def check_version(binary, required_version): 168 | try: 169 | version = subprocess.check_output([binary, '-v']) 170 | version = re.match(r'.*?([\d.]+).*?', version.decode('utf-8'))[1] 171 | if version == required_version: 172 | return True 173 | except Exception: 174 | return False 175 | return False 176 | 177 | 178 | def get_chromedriver_path(): 179 | """ 180 | :return: path of the chromedriver binary 181 | """ 182 | return os.path.abspath(os.path.dirname(__file__)) 183 | 184 | 185 | def print_chromedriver_path(): 186 | """ 187 | Print the path of the chromedriver binary. 188 | """ 189 | print(get_chromedriver_path()) 190 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from setuptools.command.build_py import build_py 3 | from chromedriver_binary.utils import get_chromedriver_filename, get_chromedriver_url, find_binary_in_path, check_version 4 | 5 | import os 6 | import ssl 7 | import zipfile 8 | 9 | try: 10 | from io import BytesIO 11 | from urllib.request import urlopen, URLError 12 | ssl_context = ssl.create_default_context() 13 | except ImportError: 14 | from StringIO import StringIO as BytesIO 15 | from urllib2 import urlopen, URLError 16 | ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS) 17 | 18 | __author__ = 'Daniel Kaiser ' 19 | 20 | 21 | with open('README.md') as readme_file: 22 | long_description = readme_file.read() 23 | 24 | 25 | class DownloadChromedriver(build_py): 26 | def run(self): 27 | """ 28 | Downloads, unzips and installs chromedriver. 29 | If a chromedriver binary is found in PATH it will be copied, otherwise downloaded. 30 | """ 31 | chromedriver_version='139.0.7225.0' 32 | chromedriver_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'chromedriver_binary') 33 | chromedriver_filename = find_binary_in_path(get_chromedriver_filename()) 34 | if chromedriver_filename and check_version(chromedriver_filename, chromedriver_version): 35 | print("\nChromedriver already installed at {}...\n".format(chromedriver_filename)) 36 | new_filename = os.path.join(chromedriver_dir, get_chromedriver_filename()) 37 | self.copy_file(chromedriver_filename, new_filename) 38 | else: 39 | chromedriver_bin = get_chromedriver_filename() 40 | chromedriver_filename = os.path.join(chromedriver_dir, chromedriver_bin) 41 | if not os.path.isfile(chromedriver_filename) or not check_version(chromedriver_filename, chromedriver_version): 42 | print("\nDownloading Chromedriver...\n") 43 | if not os.path.isdir(chromedriver_dir): 44 | os.mkdir(chromedriver_dir) 45 | url = get_chromedriver_url(version=chromedriver_version) 46 | try: 47 | response = urlopen(url, context=ssl_context) 48 | if response.getcode() != 200: 49 | raise URLError('Not Found') 50 | except URLError: 51 | raise RuntimeError('Failed to download chromedriver archive: {}'.format(url)) 52 | archive = BytesIO(response.read()) 53 | with zipfile.ZipFile(archive) as zip_file: 54 | for filename in zip_file.namelist(): 55 | zip_file.extract(filename, chromedriver_dir) 56 | path_elements = os.path.split(filename) 57 | if len(path_elements) > 1: 58 | os.rename(os.path.join(chromedriver_dir, filename), os.path.join(chromedriver_dir, path_elements[-1])) 59 | else: 60 | print("\nChromedriver already installed at {}...\n".format(chromedriver_filename)) 61 | if not os.access(chromedriver_filename, os.X_OK): 62 | os.chmod(chromedriver_filename, 0o744) 63 | build_py.run(self) 64 | 65 | 66 | setup( 67 | name="chromedriver-binary", 68 | version="139.0.7225.0.0", 69 | author="Daniel Kaiser", 70 | author_email="daniel.kaiser94@gmail.com", 71 | description="Installer for chromedriver.", 72 | license="MIT", 73 | keywords="chromedriver chrome browser selenium splinter", 74 | url="https://github.com/danielkaiser/python-chromedriver-binary", 75 | packages=['chromedriver_binary'], 76 | package_data={ 77 | 'chromedriver_binary': ['chromedriver*'] 78 | }, 79 | entry_points={ 80 | 'console_scripts': ['chromedriver-path=chromedriver_binary.utils:print_chromedriver_path'], 81 | }, 82 | long_description_content_type='text/markdown', 83 | long_description=long_description, 84 | classifiers=[ 85 | "Development Status :: 5 - Production/Stable", 86 | "Topic :: Software Development :: Testing", 87 | "Topic :: System :: Installation/Setup", 88 | "Topic :: Software Development :: Libraries :: Python Modules", 89 | "License :: OSI Approved :: MIT License", 90 | ], 91 | cmdclass={'build_py': DownloadChromedriver} 92 | ) 93 | --------------------------------------------------------------------------------