mirror of
https://github.com/servo/servo.git
synced 2025-07-03 05:23:38 +01:00
625 lines
19 KiB
Python
625 lines
19 KiB
Python
import logging
|
|
import os
|
|
import platform
|
|
import re
|
|
import shutil
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from abc import ABCMeta, abstractmethod
|
|
from ConfigParser import RawConfigParser
|
|
from datetime import datetime, timedelta
|
|
from distutils.spawn import find_executable
|
|
from io import BytesIO
|
|
|
|
from utils import call, get, untar, unzip
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
uname = platform.uname()
|
|
|
|
|
|
class Browser(object):
|
|
__metaclass__ = ABCMeta
|
|
|
|
@abstractmethod
|
|
def install(self, dest=None):
|
|
"""Install the browser."""
|
|
return NotImplemented
|
|
|
|
@abstractmethod
|
|
def install_webdriver(self, dest=None):
|
|
"""Install the WebDriver implementation for this browser."""
|
|
return NotImplemented
|
|
|
|
@abstractmethod
|
|
def find_binary(self):
|
|
"""Find the binary of the browser.
|
|
|
|
If the WebDriver for the browser is able to find the binary itself, this
|
|
method doesn't need to be implemented, in which case NotImplementedError
|
|
is suggested to be raised to prevent accidental use.
|
|
"""
|
|
return NotImplemented
|
|
|
|
@abstractmethod
|
|
def find_webdriver(self):
|
|
"""Find the binary of the WebDriver."""
|
|
return NotImplemented
|
|
|
|
@abstractmethod
|
|
def version(self, binary=None):
|
|
"""Retrieve the release version of the installed browser."""
|
|
return NotImplemented
|
|
|
|
@abstractmethod
|
|
def requirements(self):
|
|
"""Name of the browser-specific wptrunner requirements file"""
|
|
return NotImplemented
|
|
|
|
|
|
class Firefox(Browser):
|
|
"""Firefox-specific interface.
|
|
|
|
Includes installation, webdriver installation, and wptrunner setup methods.
|
|
"""
|
|
|
|
product = "firefox"
|
|
binary = "browsers/firefox/firefox"
|
|
platform_ini = "browsers/firefox/platform.ini"
|
|
requirements = "requirements_firefox.txt"
|
|
|
|
def platform_string_geckodriver(self):
|
|
platform = {
|
|
"Linux": "linux",
|
|
"Windows": "win",
|
|
"Darwin": "macos"
|
|
}.get(uname[0])
|
|
|
|
if platform is None:
|
|
raise ValueError("Unable to construct a valid Geckodriver package name for current platform")
|
|
|
|
if platform in ("linux", "win"):
|
|
bits = "64" if uname[4] == "x86_64" else "32"
|
|
else:
|
|
bits = ""
|
|
|
|
return "%s%s" % (platform, bits)
|
|
|
|
def install(self, dest=None):
|
|
"""Install Firefox."""
|
|
|
|
from mozdownload import FactoryScraper
|
|
import mozinstall
|
|
|
|
platform = {
|
|
"Linux": "linux",
|
|
"Windows": "win",
|
|
"Darwin": "mac"
|
|
}.get(uname[0])
|
|
|
|
if platform is None:
|
|
raise ValueError("Unable to construct a valid Firefox package name for current platform")
|
|
|
|
if dest is None:
|
|
# os.getcwd() doesn't include the venv path
|
|
dest = os.path.join(os.getcwd(), "_venv")
|
|
|
|
dest = os.path.join(dest, "browsers")
|
|
|
|
filename = FactoryScraper("daily", branch="mozilla-central", destination=dest).download()
|
|
|
|
try:
|
|
mozinstall.install(filename, dest)
|
|
except mozinstall.mozinstall.InstallError:
|
|
if platform == "mac" and os.path.exists(os.path.join(dest, "Firefox Nightly.app")):
|
|
# mozinstall will fail if nightly is already installed in the venv because
|
|
# mac installation uses shutil.copy_tree
|
|
mozinstall.uninstall(os.path.join(dest, "Firefox Nightly.app"))
|
|
mozinstall.install(filename, dest)
|
|
else:
|
|
raise
|
|
|
|
os.remove(filename)
|
|
return self.find_binary_path(dest)
|
|
|
|
def find_binary_path(self, path=None):
|
|
"""Looks for the firefox binary in the virtual environment"""
|
|
|
|
platform = {
|
|
"Linux": "linux",
|
|
"Windows": "win",
|
|
"Darwin": "mac"
|
|
}.get(uname[0])
|
|
|
|
if path is None:
|
|
#os.getcwd() doesn't include the venv path
|
|
path = os.path.join(os.getcwd(), "_venv", "browsers")
|
|
|
|
binary = None
|
|
|
|
if platform == "linux":
|
|
binary = find_executable("firefox", os.path.join(path, "firefox"))
|
|
elif platform == "win":
|
|
import mozinstall
|
|
binary = mozinstall.get_binary(path, "firefox")
|
|
elif platform == "mac":
|
|
binary = find_executable("firefox", os.path.join(path, "Firefox Nightly.app", "Contents", "MacOS"))
|
|
|
|
return binary
|
|
|
|
def find_binary(self, venv_path=None):
|
|
if venv_path is None:
|
|
venv_path = os.path.join(os.getcwd(), "_venv")
|
|
|
|
binary = self.find_binary_path(os.path.join(venv_path, "browsers"))
|
|
|
|
if not binary and uname[0] == "Darwin":
|
|
macpaths = ["/Applications/FirefoxNightly.app/Contents/MacOS",
|
|
os.path.expanduser("~/Applications/FirefoxNightly.app/Contents/MacOS"),
|
|
"/Applications/Firefox Developer Edition.app/Contents/MacOS",
|
|
os.path.expanduser("~/Applications/Firefox Developer Edition.app/Contents/MacOS"),
|
|
"/Applications/Firefox.app/Contents/MacOS",
|
|
os.path.expanduser("~/Applications/Firefox.app/Contents/MacOS")]
|
|
return find_executable("firefox", os.pathsep.join(macpaths))
|
|
|
|
if binary is None:
|
|
return find_executable("firefox")
|
|
|
|
return binary
|
|
|
|
def find_certutil(self):
|
|
path = find_executable("certutil")
|
|
if path is None:
|
|
return None
|
|
if os.path.splitdrive(path)[1].split(os.path.sep) == ["", "Windows", "system32", "certutil.exe"]:
|
|
return None
|
|
return path
|
|
|
|
def find_webdriver(self):
|
|
return find_executable("geckodriver")
|
|
|
|
def get_version_and_channel(self, binary):
|
|
version_string = call(binary, "--version").strip()
|
|
m = re.match(r"Mozilla Firefox (\d+\.\d+(?:\.\d+)?)(a|b)?", version_string)
|
|
if not m:
|
|
return None, "nightly"
|
|
version, status = m.groups()
|
|
channel = {"a": "nightly", "b": "beta"}
|
|
return version, channel.get(status, "stable")
|
|
|
|
def get_profile_bundle_url(self, version, channel):
|
|
if channel == "stable":
|
|
repo = "https://hg.mozilla.org/releases/mozilla-release"
|
|
tag = "FIREFOX_%s_RELEASE" % version.replace(".", "_")
|
|
else:
|
|
repo = "https://hg.mozilla.org/mozilla-central"
|
|
if channel == "beta":
|
|
tag = "FIREFOX_%s_BETA" % version.split(".", 1)[0]
|
|
else:
|
|
# Always use tip as the tag for nightly; this isn't quite right
|
|
# but to do better we need the actual build revision, which we
|
|
# can get if we have an application.ini file
|
|
tag = "tip"
|
|
|
|
return "%s/archive/%s.zip/testing/profiles/" % (repo, tag)
|
|
|
|
def install_prefs(self, binary, dest=None):
|
|
version, channel = self.get_version_and_channel(binary)
|
|
if dest is None:
|
|
dest = os.pwd
|
|
|
|
dest = os.path.join(dest, "profiles", channel, version)
|
|
have_cache = False
|
|
if os.path.exists(dest):
|
|
if channel != "nightly":
|
|
have_cache = True
|
|
else:
|
|
now = datetime.now()
|
|
have_cache = (datetime.fromtimestamp(os.stat(dest).st_mtime) >
|
|
now - timedelta(days=1))
|
|
|
|
# If we don't have a recent download, grab and extract the latest one
|
|
if not have_cache:
|
|
if os.path.exists(dest):
|
|
shutil.rmtree(dest)
|
|
os.makedirs(dest)
|
|
|
|
url = self.get_profile_bundle_url(version, channel)
|
|
|
|
print("Installing test prefs from %s" % url)
|
|
try:
|
|
extract_dir = tempfile.mkdtemp()
|
|
unzip(get(url).raw, dest=extract_dir)
|
|
|
|
profiles = os.path.join(extract_dir, os.listdir(extract_dir)[0], 'testing', 'profiles')
|
|
for name in os.listdir(profiles):
|
|
path = os.path.join(profiles, name)
|
|
shutil.move(path, dest)
|
|
finally:
|
|
shutil.rmtree(extract_dir)
|
|
else:
|
|
print("Using cached test prefs from %s" % dest)
|
|
|
|
return dest
|
|
|
|
def _latest_geckodriver_version(self):
|
|
"""Get and return latest version number for geckodriver."""
|
|
# This is used rather than an API call to avoid rate limits
|
|
tags = call("git", "ls-remote", "--tags", "--refs",
|
|
"https://github.com/mozilla/geckodriver.git")
|
|
release_re = re.compile(".*refs/tags/v(\d+)\.(\d+)\.(\d+)")
|
|
latest_release = 0
|
|
for item in tags.split("\n"):
|
|
m = release_re.match(item)
|
|
if m:
|
|
version = [int(item) for item in m.groups()]
|
|
if version > latest_release:
|
|
latest_release = version
|
|
assert latest_release != 0
|
|
return "v%s.%s.%s" % tuple(str(item) for item in latest_release)
|
|
|
|
def install_webdriver(self, dest=None):
|
|
"""Install latest Geckodriver."""
|
|
if dest is None:
|
|
dest = os.getcwd()
|
|
|
|
version = self._latest_geckodriver_version()
|
|
format = "zip" if uname[0] == "Windows" else "tar.gz"
|
|
logger.debug("Latest geckodriver release %s" % version)
|
|
url = ("https://github.com/mozilla/geckodriver/releases/download/%s/geckodriver-%s-%s.%s" %
|
|
(version, version, self.platform_string_geckodriver(), format))
|
|
if format == "zip":
|
|
unzip(get(url).raw, dest=dest)
|
|
else:
|
|
untar(get(url).raw, dest=dest)
|
|
return find_executable(os.path.join(dest, "geckodriver"))
|
|
|
|
def version(self, binary=None):
|
|
"""Retrieve the release version of the installed browser."""
|
|
binary = binary or self.find_binary()
|
|
version_string = call(binary, "--version").strip()
|
|
m = re.match(r"Mozilla Firefox (.*)", version_string)
|
|
if not m:
|
|
return None
|
|
return m.group(1)
|
|
|
|
|
|
class Chrome(Browser):
|
|
"""Chrome-specific interface.
|
|
|
|
Includes webdriver installation, and wptrunner setup methods.
|
|
"""
|
|
|
|
product = "chrome"
|
|
requirements = "requirements_chrome.txt"
|
|
|
|
@property
|
|
def binary(self):
|
|
if uname[0] == "Linux":
|
|
return "/usr/bin/google-chrome"
|
|
if uname[0] == "Darwin":
|
|
return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
|
# TODO Windows?
|
|
logger.warn("Unable to find the browser binary.")
|
|
return None
|
|
|
|
def install(self, dest=None):
|
|
raise NotImplementedError
|
|
|
|
def platform_string(self):
|
|
platform = {
|
|
"Linux": "linux",
|
|
"Windows": "win",
|
|
"Darwin": "mac"
|
|
}.get(uname[0])
|
|
|
|
if platform is None:
|
|
raise ValueError("Unable to construct a valid Chrome package name for current platform")
|
|
|
|
if platform == "linux":
|
|
bits = "64" if uname[4] == "x86_64" else "32"
|
|
elif platform == "mac":
|
|
bits = "64"
|
|
elif platform == "win":
|
|
bits = "32"
|
|
|
|
return "%s%s" % (platform, bits)
|
|
|
|
def find_binary(self):
|
|
raise NotImplementedError
|
|
|
|
def find_webdriver(self):
|
|
return find_executable("chromedriver")
|
|
|
|
def install_webdriver(self, dest=None):
|
|
if dest is None:
|
|
dest = os.pwd
|
|
latest = get("http://chromedriver.storage.googleapis.com/LATEST_RELEASE").text.strip()
|
|
url = "http://chromedriver.storage.googleapis.com/%s/chromedriver_%s.zip" % (latest,
|
|
self.platform_string())
|
|
unzip(get(url).raw, dest)
|
|
|
|
path = find_executable("chromedriver", dest)
|
|
st = os.stat(path)
|
|
os.chmod(path, st.st_mode | stat.S_IEXEC)
|
|
return path
|
|
|
|
def version(self, binary=None):
|
|
binary = binary or self.binary
|
|
try:
|
|
version_string = call(binary, "--version").strip()
|
|
except subprocess.CalledProcessError:
|
|
logger.warn("Failed to call %s", binary)
|
|
return None
|
|
m = re.match(r"Google Chrome (.*)", version_string)
|
|
if not m:
|
|
logger.warn("Failed to extract version from: s%", version_string)
|
|
return None
|
|
return m.group(1)
|
|
|
|
|
|
class ChromeAndroid(Browser):
|
|
"""Chrome-specific interface for Android.
|
|
|
|
Includes webdriver installation.
|
|
"""
|
|
|
|
product = "chrome_android"
|
|
requirements = "requirements_chrome_android.txt"
|
|
|
|
def install(self, dest=None):
|
|
raise NotImplementedError
|
|
|
|
def find_binary(self):
|
|
raise NotImplementedError
|
|
|
|
def find_webdriver(self):
|
|
return find_executable("chromedriver")
|
|
|
|
def install_webdriver(self, dest=None):
|
|
chrome = Chrome()
|
|
return chrome.install_webdriver(dest)
|
|
|
|
def version(self, binary):
|
|
return None
|
|
|
|
|
|
class Opera(Browser):
|
|
"""Opera-specific interface.
|
|
|
|
Includes webdriver installation, and wptrunner setup methods.
|
|
"""
|
|
|
|
product = "opera"
|
|
requirements = "requirements_opera.txt"
|
|
|
|
@property
|
|
def binary(self):
|
|
if uname[0] == "Linux":
|
|
return "/usr/bin/opera"
|
|
# TODO Windows, Mac?
|
|
logger.warn("Unable to find the browser binary.")
|
|
return None
|
|
|
|
def install(self, dest=None):
|
|
raise NotImplementedError
|
|
|
|
def platform_string(self):
|
|
platform = {
|
|
"Linux": "linux",
|
|
"Windows": "win",
|
|
"Darwin": "mac"
|
|
}.get(uname[0])
|
|
|
|
if platform is None:
|
|
raise ValueError("Unable to construct a valid Opera package name for current platform")
|
|
|
|
if platform == "linux":
|
|
bits = "64" if uname[4] == "x86_64" else "32"
|
|
elif platform == "mac":
|
|
bits = "64"
|
|
elif platform == "win":
|
|
bits = "32"
|
|
|
|
return "%s%s" % (platform, bits)
|
|
|
|
def find_binary(self):
|
|
raise NotImplementedError
|
|
|
|
def find_webdriver(self):
|
|
return find_executable("operadriver")
|
|
|
|
def install_webdriver(self, dest=None):
|
|
if dest is None:
|
|
dest = os.pwd
|
|
latest = get("https://api.github.com/repos/operasoftware/operachromiumdriver/releases/latest").json()["tag_name"]
|
|
url = "https://github.com/operasoftware/operachromiumdriver/releases/download/%s/operadriver_%s.zip" % (latest,
|
|
self.platform_string())
|
|
unzip(get(url).raw, dest)
|
|
|
|
operadriver_dir = os.path.join(dest, "operadriver_%s" % self.platform_string())
|
|
shutil.move(os.path.join(operadriver_dir, "operadriver"), dest)
|
|
shutil.rmtree(operadriver_dir)
|
|
|
|
path = find_executable("operadriver")
|
|
st = os.stat(path)
|
|
os.chmod(path, st.st_mode | stat.S_IEXEC)
|
|
return path
|
|
|
|
def version(self, binary):
|
|
"""Retrieve the release version of the installed browser."""
|
|
binary = binary or self.binary
|
|
try:
|
|
output = call(binary, "--version")
|
|
except subprocess.CalledProcessError:
|
|
logger.warn("Failed to call %s", binary)
|
|
return None
|
|
return re.search(r"[0-9\.]+( [a-z]+)?$", output.strip()).group(0)
|
|
|
|
|
|
class Edge(Browser):
|
|
"""Edge-specific interface."""
|
|
|
|
product = "edge"
|
|
requirements = "requirements_edge.txt"
|
|
|
|
def install(self, dest=None):
|
|
raise NotImplementedError
|
|
|
|
def find_binary(self):
|
|
raise NotImplementedError
|
|
|
|
def find_webdriver(self):
|
|
return find_executable("MicrosoftWebDriver")
|
|
|
|
def install_webdriver(self, dest=None):
|
|
raise NotImplementedError
|
|
|
|
def version(self, binary):
|
|
return None
|
|
|
|
|
|
class InternetExplorer(Browser):
|
|
"""Internet Explorer-specific interface."""
|
|
|
|
product = "ie"
|
|
requirements = "requirements_ie.txt"
|
|
|
|
def install(self, dest=None):
|
|
raise NotImplementedError
|
|
|
|
def find_binary(self):
|
|
raise NotImplementedError
|
|
|
|
def find_webdriver(self):
|
|
return find_executable("IEDriverServer.exe")
|
|
|
|
def install_webdriver(self, dest=None):
|
|
raise NotImplementedError
|
|
|
|
def version(self, binary):
|
|
return None
|
|
|
|
|
|
class Safari(Browser):
|
|
"""Safari-specific interface.
|
|
|
|
Includes installation, webdriver installation, and wptrunner setup methods.
|
|
"""
|
|
|
|
product = "safari"
|
|
requirements = "requirements_safari.txt"
|
|
|
|
def install(self, dest=None):
|
|
raise NotImplementedError
|
|
|
|
def find_binary(self):
|
|
raise NotImplementedError
|
|
|
|
def find_webdriver(self):
|
|
return find_executable("safaridriver")
|
|
|
|
def install_webdriver(self):
|
|
raise NotImplementedError
|
|
|
|
def version(self, binary):
|
|
return None
|
|
|
|
|
|
class Servo(Browser):
|
|
"""Servo-specific interface."""
|
|
|
|
product = "servo"
|
|
requirements = "requirements_servo.txt"
|
|
|
|
def platform_components(self):
|
|
platform = {
|
|
"Linux": "linux",
|
|
"Windows": "win",
|
|
"Darwin": "mac"
|
|
}.get(uname[0])
|
|
|
|
if platform is None:
|
|
raise ValueError("Unable to construct a valid Servo package for current platform")
|
|
|
|
if platform == "linux":
|
|
extension = ".tar.gz"
|
|
decompress = untar
|
|
elif platform == "win" or platform == "mac":
|
|
raise ValueError("Unable to construct a valid Servo package for current platform")
|
|
|
|
return (platform, extension, decompress)
|
|
|
|
def install(self, dest=None):
|
|
"""Install latest Browser Engine."""
|
|
if dest is None:
|
|
dest = os.pwd
|
|
|
|
platform, extension, decompress = self.platform_components()
|
|
url = "https://download.servo.org/nightly/%s/servo-latest%s" % (platform, extension)
|
|
|
|
decompress(get(url).raw, dest=dest)
|
|
path = find_executable("servo", os.path.join(dest, "servo"))
|
|
st = os.stat(path)
|
|
os.chmod(path, st.st_mode | stat.S_IEXEC)
|
|
return path
|
|
|
|
def find_binary(self):
|
|
return find_executable("servo")
|
|
|
|
def find_webdriver(self):
|
|
return None
|
|
|
|
def install_webdriver(self, dest=None):
|
|
raise NotImplementedError
|
|
|
|
def version(self, binary):
|
|
"""Retrieve the release version of the installed browser."""
|
|
output = call(binary, "--version")
|
|
return re.search(r"[0-9\.]+( [a-z]+)?$", output.strip()).group(0)
|
|
|
|
|
|
class Sauce(Browser):
|
|
"""Sauce-specific interface."""
|
|
|
|
product = "sauce"
|
|
requirements = "requirements_sauce.txt"
|
|
|
|
def install(self, dest=None):
|
|
raise NotImplementedError
|
|
|
|
def find_binary(self):
|
|
raise NotImplementedError
|
|
|
|
def find_webdriver(self):
|
|
raise NotImplementedError
|
|
|
|
def install_webdriver(self, dest=None):
|
|
raise NotImplementedError
|
|
|
|
def version(self, binary):
|
|
return None
|
|
|
|
|
|
class WebKit(Browser):
|
|
"""WebKit-specific interface."""
|
|
|
|
product = "webkit"
|
|
requirements = "requirements_webkit.txt"
|
|
|
|
def install(self, dest=None):
|
|
raise NotImplementedError
|
|
|
|
def find_binary(self, path=None):
|
|
return None
|
|
|
|
def find_webdriver(self):
|
|
return None
|
|
|
|
def install_webdriver(self):
|
|
raise NotImplementedError
|
|
|
|
def version(self, binary):
|
|
return None
|