1import os 2import platform 3import re 4import shutil 5import stat 6import errno 7import subprocess 8import tempfile 9from abc import ABCMeta, abstractmethod 10from datetime import datetime, timedelta 11from distutils.spawn import find_executable 12 13from six.moves.urllib.parse import urlsplit 14import requests 15 16from .utils import call, get, untar, unzip 17 18uname = platform.uname() 19 20# the rootUrl for the firefox-ci deployment of Taskcluster 21FIREFOX_CI_ROOT_URL = 'https://firefox-ci-tc.services.mozilla.com' 22 23 24def _get_fileversion(binary, logger=None): 25 command = "(Get-Item '%s').VersionInfo.FileVersion" % binary.replace("'", "''") 26 try: 27 return call("powershell.exe", command).strip() 28 except (subprocess.CalledProcessError, OSError): 29 if logger is not None: 30 logger.warning("Failed to call %s in PowerShell" % command) 31 return None 32 33 34def handle_remove_readonly(func, path, exc): 35 excvalue = exc[1] 36 if func in (os.rmdir, os.remove) and excvalue.errno == errno.EACCES: 37 os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 0777 38 func(path) 39 else: 40 raise 41 42 43def get_ext(filename): 44 """Get the extension from a filename with special handling for .tar.foo""" 45 name, ext = os.path.splitext(filename) 46 if name.endswith(".tar"): 47 ext = ".tar%s" % ext 48 return ext 49 50 51def get_taskcluster_artifact(index, path): 52 TC_INDEX_BASE = FIREFOX_CI_ROOT_URL + "/api/index/v1/" 53 54 resp = get(TC_INDEX_BASE + "task/%s/artifacts/%s" % (index, path)) 55 resp.raise_for_status() 56 57 return resp 58 59 60class Browser(object): 61 __metaclass__ = ABCMeta 62 63 def __init__(self, logger): 64 self.logger = logger 65 66 @abstractmethod 67 def download(self, dest=None, channel=None, rename=None): 68 """Download a package or installer for the browser 69 :param dest: Directory in which to put the dowloaded package 70 :param channel: Browser channel to download 71 :param rename: Optional name for the downloaded package; the original 72 extension is preserved. 73 """ 74 return NotImplemented 75 76 @abstractmethod 77 def install(self, dest=None): 78 """Install the browser.""" 79 return NotImplemented 80 81 @abstractmethod 82 def install_webdriver(self, dest=None, channel=None, browser_binary=None): 83 """Install the WebDriver implementation for this browser.""" 84 return NotImplemented 85 86 @abstractmethod 87 def find_binary(self, venv_path=None, channel=None): 88 """Find the binary of the browser. 89 90 If the WebDriver for the browser is able to find the binary itself, this 91 method doesn't need to be implemented, in which case NotImplementedError 92 is suggested to be raised to prevent accidental use. 93 """ 94 return NotImplemented 95 96 @abstractmethod 97 def find_webdriver(self, channel=None): 98 """Find the binary of the WebDriver.""" 99 return NotImplemented 100 101 @abstractmethod 102 def version(self, binary=None, webdriver_binary=None): 103 """Retrieve the release version of the installed browser.""" 104 return NotImplemented 105 106 @abstractmethod 107 def requirements(self): 108 """Name of the browser-specific wptrunner requirements file""" 109 return NotImplemented 110 111 112class Firefox(Browser): 113 """Firefox-specific interface. 114 115 Includes installation, webdriver installation, and wptrunner setup methods. 116 """ 117 118 product = "firefox" 119 binary = "browsers/firefox/firefox" 120 requirements = "requirements_firefox.txt" 121 122 platform = { 123 "Linux": "linux", 124 "Windows": "win", 125 "Darwin": "macos" 126 }.get(uname[0]) 127 128 application_name = { 129 "stable": "Firefox.app", 130 "beta": "Firefox.app", 131 "nightly": "Firefox Nightly.app" 132 } 133 134 def platform_string_geckodriver(self): 135 if self.platform is None: 136 raise ValueError("Unable to construct a valid Geckodriver package name for current platform") 137 138 if self.platform in ("linux", "win"): 139 bits = "64" if uname[4] == "x86_64" else "32" 140 else: 141 bits = "" 142 143 return "%s%s" % (self.platform, bits) 144 145 def _get_dest(self, dest, channel): 146 if dest is None: 147 # os.getcwd() doesn't include the venv path 148 dest = os.path.join(os.getcwd(), "_venv") 149 150 dest = os.path.join(dest, "browsers", channel) 151 152 if not os.path.exists(dest): 153 os.makedirs(dest) 154 155 return dest 156 157 def download(self, dest=None, channel="nightly", rename=None): 158 product = { 159 "nightly": "firefox-nightly-latest-ssl", 160 "beta": "firefox-beta-latest-ssl", 161 "stable": "firefox-latest-ssl" 162 } 163 164 os_builds = { 165 ("linux", "x86"): "linux", 166 ("linux", "x86_64"): "linux64", 167 ("win", "x86"): "win", 168 ("win", "AMD64"): "win64", 169 ("macos", "x86_64"): "osx", 170 } 171 os_key = (self.platform, uname[4]) 172 173 if dest is None: 174 dest = self._get_dest(None, channel) 175 176 if channel not in product: 177 raise ValueError("Unrecognised release channel: %s" % channel) 178 179 if os_key not in os_builds: 180 raise ValueError("Unsupported platform: %s %s" % os_key) 181 182 url = "https://download.mozilla.org/?product=%s&os=%s&lang=en-US" % (product[channel], 183 os_builds[os_key]) 184 self.logger.info("Downloading Firefox from %s" % url) 185 resp = get(url) 186 187 filename = None 188 189 content_disposition = resp.headers.get('content-disposition') 190 if content_disposition: 191 filenames = re.findall("filename=(.+)", content_disposition) 192 if filenames: 193 filename = filenames[0] 194 195 if not filename: 196 filename = urlsplit(resp.url).path.rsplit("/", 1)[1] 197 198 if not filename: 199 filename = "firefox.tar.bz2" 200 201 if rename: 202 filename = "%s%s" % (rename, get_ext(filename)) 203 204 installer_path = os.path.join(dest, filename) 205 206 with open(installer_path, "wb") as f: 207 f.write(resp.content) 208 209 return installer_path 210 211 def install(self, dest=None, channel="nightly"): 212 """Install Firefox.""" 213 import mozinstall 214 215 dest = self._get_dest(dest, channel) 216 217 filename = os.path.basename(dest) 218 219 installer_path = self.download(dest, channel) 220 221 try: 222 mozinstall.install(installer_path, dest) 223 except mozinstall.mozinstall.InstallError: 224 if self.platform == "macos" and os.path.exists(os.path.join(dest, self.application_name.get(channel, "Firefox Nightly.app"))): 225 # mozinstall will fail if nightly is already installed in the venv because 226 # mac installation uses shutil.copy_tree 227 mozinstall.uninstall(os.path.join(dest, self.application_name.get(channel, "Firefox Nightly.app"))) 228 mozinstall.install(filename, dest) 229 else: 230 raise 231 232 os.remove(installer_path) 233 return self.find_binary_path(dest) 234 235 def find_binary_path(self, path=None, channel="nightly"): 236 """Looks for the firefox binary in the virtual environment""" 237 238 if path is None: 239 # os.getcwd() doesn't include the venv path 240 path = os.path.join(os.getcwd(), "_venv", "browsers", channel) 241 242 binary = None 243 244 if self.platform == "linux": 245 binary = find_executable("firefox", os.path.join(path, "firefox")) 246 elif self.platform == "win": 247 import mozinstall 248 try: 249 binary = mozinstall.get_binary(path, "firefox") 250 except mozinstall.InvalidBinary: 251 # ignore the case where we fail to get a binary 252 pass 253 elif self.platform == "macos": 254 binary = find_executable("firefox", os.path.join(path, self.application_name.get(channel, "Firefox Nightly.app"), 255 "Contents", "MacOS")) 256 257 return binary 258 259 def find_binary(self, venv_path=None, channel="nightly"): 260 if venv_path is None: 261 venv_path = os.path.join(os.getcwd(), "_venv") 262 263 path = os.path.join(venv_path, "browsers", channel) 264 binary = self.find_binary_path(path, channel) 265 266 if not binary and self.platform == "win": 267 winpaths = [os.path.expandvars("$SYSTEMDRIVE\\Program Files\\Mozilla Firefox"), 268 os.path.expandvars("$SYSTEMDRIVE\\Program Files (x86)\\Mozilla Firefox")] 269 for winpath in winpaths: 270 binary = self.find_binary_path(winpath, channel) 271 if binary is not None: 272 break 273 274 if not binary and self.platform == "macos": 275 macpaths = ["/Applications/Firefox Nightly.app/Contents/MacOS", 276 os.path.expanduser("~/Applications/Firefox Nightly.app/Contents/MacOS"), 277 "/Applications/Firefox Developer Edition.app/Contents/MacOS", 278 os.path.expanduser("~/Applications/Firefox Developer Edition.app/Contents/MacOS"), 279 "/Applications/Firefox.app/Contents/MacOS", 280 os.path.expanduser("~/Applications/Firefox.app/Contents/MacOS")] 281 return find_executable("firefox", os.pathsep.join(macpaths)) 282 283 if binary is None: 284 return find_executable("firefox") 285 286 return binary 287 288 def find_certutil(self): 289 path = find_executable("certutil") 290 if path is None: 291 return None 292 if os.path.splitdrive(os.path.normcase(path))[1].split(os.path.sep) == ["", "windows", "system32", "certutil.exe"]: 293 return None 294 return path 295 296 def find_webdriver(self, channel=None): 297 return find_executable("geckodriver") 298 299 def get_version_and_channel(self, binary): 300 version_string = call(binary, "--version").strip() 301 m = re.match(r"Mozilla Firefox (\d+\.\d+(?:\.\d+)?)(a|b)?", version_string) 302 if not m: 303 return None, "nightly" 304 version, status = m.groups() 305 channel = {"a": "nightly", "b": "beta"} 306 return version, channel.get(status, "stable") 307 308 def get_profile_bundle_url(self, version, channel): 309 if channel == "stable": 310 repo = "https://hg.mozilla.org/releases/mozilla-release" 311 tag = "FIREFOX_%s_RELEASE" % version.replace(".", "_") 312 elif channel == "beta": 313 repo = "https://hg.mozilla.org/releases/mozilla-beta" 314 major_version = version.split(".", 1)[0] 315 # For beta we have a different format for betas that are now in stable releases 316 # vs those that are not 317 tags = get("https://hg.mozilla.org/releases/mozilla-beta/json-tags").json()["tags"] 318 tags = {item["tag"] for item in tags} 319 end_tag = "FIREFOX_BETA_%s_END" % major_version 320 if end_tag in tags: 321 tag = end_tag 322 else: 323 tag = "tip" 324 else: 325 repo = "https://hg.mozilla.org/mozilla-central" 326 # Always use tip as the tag for nightly; this isn't quite right 327 # but to do better we need the actual build revision, which we 328 # can get if we have an application.ini file 329 tag = "tip" 330 331 return "%s/archive/%s.zip/testing/profiles/" % (repo, tag) 332 333 def install_prefs(self, binary, dest=None, channel=None): 334 if binary: 335 version, channel_ = self.get_version_and_channel(binary) 336 if channel is not None and channel != channel_: 337 # Beta doesn't always seem to have the b in the version string, so allow the 338 # manually supplied value to override the one from the binary 339 self.logger.warning("Supplied channel doesn't match binary, using supplied channel") 340 elif channel is None: 341 channel = channel_ 342 else: 343 version = None 344 345 if dest is None: 346 dest = os.curdir 347 348 dest = os.path.join(dest, "profiles", channel) 349 if version: 350 dest = os.path.join(dest, version) 351 have_cache = False 352 if os.path.exists(dest) and len(os.listdir(dest)) > 0: 353 if channel != "nightly": 354 have_cache = True 355 else: 356 now = datetime.now() 357 have_cache = (datetime.fromtimestamp(os.stat(dest).st_mtime) > 358 now - timedelta(days=1)) 359 360 # If we don't have a recent download, grab and extract the latest one 361 if not have_cache: 362 if os.path.exists(dest): 363 shutil.rmtree(dest) 364 os.makedirs(dest) 365 366 url = self.get_profile_bundle_url(version, channel) 367 368 self.logger.info("Installing test prefs from %s" % url) 369 try: 370 extract_dir = tempfile.mkdtemp() 371 unzip(get(url).raw, dest=extract_dir) 372 373 profiles = os.path.join(extract_dir, os.listdir(extract_dir)[0], 'testing', 'profiles') 374 for name in os.listdir(profiles): 375 path = os.path.join(profiles, name) 376 shutil.move(path, dest) 377 finally: 378 shutil.rmtree(extract_dir) 379 else: 380 self.logger.info("Using cached test prefs from %s" % dest) 381 382 return dest 383 384 def _latest_geckodriver_version(self): 385 """Get and return latest version number for geckodriver.""" 386 # This is used rather than an API call to avoid rate limits 387 tags = call("git", "ls-remote", "--tags", "--refs", 388 "https://github.com/mozilla/geckodriver.git") 389 release_re = re.compile(r".*refs/tags/v(\d+)\.(\d+)\.(\d+)") 390 latest_release = (0,0,0) 391 for item in tags.split("\n"): 392 m = release_re.match(item) 393 if m: 394 version = tuple(int(item) for item in m.groups()) 395 if version > latest_release: 396 latest_release = version 397 assert latest_release != (0,0,0) 398 return "v%s.%s.%s" % tuple(str(item) for item in latest_release) 399 400 def install_webdriver(self, dest=None, channel=None, browser_binary=None): 401 """Install latest Geckodriver.""" 402 if dest is None: 403 dest = os.getcwd() 404 405 if channel == "nightly": 406 path = self.install_geckodriver_nightly(dest) 407 if path is not None: 408 return path 409 else: 410 self.logger.warning("Nightly webdriver not found; falling back to release") 411 412 version = self._latest_geckodriver_version() 413 format = "zip" if uname[0] == "Windows" else "tar.gz" 414 self.logger.debug("Latest geckodriver release %s" % version) 415 url = ("https://github.com/mozilla/geckodriver/releases/download/%s/geckodriver-%s-%s.%s" % 416 (version, version, self.platform_string_geckodriver(), format)) 417 if format == "zip": 418 unzip(get(url).raw, dest=dest) 419 else: 420 untar(get(url).raw, dest=dest) 421 return find_executable(os.path.join(dest, "geckodriver")) 422 423 def install_geckodriver_nightly(self, dest): 424 self.logger.info("Attempting to install webdriver from nightly") 425 426 platform_bits = ("64" if uname[4] == "x86_64" else 427 ("32" if self.platform == "win" else "")) 428 tc_platform = "%s%s" % (self.platform, platform_bits) 429 430 archive_ext = ".zip" if uname[0] == "Windows" else ".tar.gz" 431 archive_name = "public/geckodriver%s" % archive_ext 432 433 try: 434 resp = get_taskcluster_artifact( 435 "gecko.v2.mozilla-central.latest.geckodriver.%s" % tc_platform, 436 archive_name) 437 except Exception: 438 self.logger.info("Geckodriver download failed") 439 return 440 441 if archive_ext == ".zip": 442 unzip(resp.raw, dest) 443 else: 444 untar(resp.raw, dest) 445 446 exe_ext = ".exe" if uname[0] == "Windows" else "" 447 path = os.path.join(dest, "geckodriver%s" % exe_ext) 448 449 self.logger.info("Extracted geckodriver to %s" % path) 450 451 return path 452 453 def version(self, binary=None, webdriver_binary=None): 454 """Retrieve the release version of the installed browser.""" 455 version_string = call(binary, "--version").strip() 456 m = re.match(r"Mozilla Firefox (.*)", version_string) 457 if not m: 458 return None 459 return m.group(1) 460 461 462class FirefoxAndroid(Browser): 463 """Android-specific Firefox interface.""" 464 465 product = "firefox_android" 466 requirements = "requirements_firefox.txt" 467 468 def download(self, dest=None, channel=None, rename=None): 469 if dest is None: 470 dest = os.pwd 471 472 473 resp = get_taskcluster_artifact( 474 "gecko.v2.mozilla-central.latest.mobile.android-x86_64-opt", 475 "public/build/geckoview-androidTest.apk") 476 477 filename = "geckoview-androidTest.apk" 478 if rename: 479 filename = "%s%s" % (rename, get_ext(filename)[1]) 480 apk_path = os.path.join(dest, filename) 481 482 with open(apk_path, "wb") as f: 483 f.write(resp.content) 484 485 return apk_path 486 487 def install(self, dest=None, channel=None): 488 return self.download(dest, channel) 489 490 def install_prefs(self, binary, dest=None, channel=None): 491 fx_browser = Firefox(self.logger) 492 return fx_browser.install_prefs(binary, dest, channel) 493 494 def find_binary(self, venv_path=None, channel=None): 495 raise NotImplementedError 496 497 def find_webdriver(self, channel=None): 498 raise NotImplementedError 499 500 def install_webdriver(self, dest=None, channel=None, browser_binary=None): 501 raise NotImplementedError 502 503 def version(self, binary=None, webdriver_binary=None): 504 return None 505 506 507class Chrome(Browser): 508 """Chrome-specific interface. 509 510 Includes webdriver installation, and wptrunner setup methods. 511 """ 512 513 product = "chrome" 514 requirements = "requirements_chrome.txt" 515 516 def download(self, dest=None, channel=None, rename=None): 517 raise NotImplementedError 518 519 def install(self, dest=None, channel=None): 520 raise NotImplementedError 521 522 def platform_string(self): 523 platform = { 524 "Linux": "linux", 525 "Windows": "win", 526 "Darwin": "mac" 527 }.get(uname[0]) 528 529 if platform is None: 530 raise ValueError("Unable to construct a valid Chrome package name for current platform") 531 532 if platform == "linux": 533 bits = "64" if uname[4] == "x86_64" else "32" 534 elif platform == "mac": 535 bits = "64" 536 elif platform == "win": 537 bits = "32" 538 539 return "%s%s" % (platform, bits) 540 541 def chromium_platform_string(self): 542 platform = { 543 "Linux": "Linux", 544 "Windows": "Win", 545 "Darwin": "Mac" 546 }.get(uname[0]) 547 548 if platform is None: 549 raise ValueError("Unable to construct a valid Chromium package name for current platform") 550 551 if (platform == "Linux" or platform == "Win") and uname[4] == "x86_64": 552 platform += "_x64" 553 554 return platform 555 556 def find_binary(self, venv_path=None, channel=None): 557 if uname[0] == "Linux": 558 name = "google-chrome" 559 if channel == "stable": 560 name += "-stable" 561 elif channel == "beta": 562 name += "-beta" 563 elif channel == "dev": 564 name += "-unstable" 565 # No Canary on Linux. 566 return find_executable(name) 567 if uname[0] == "Darwin": 568 if channel == "canary": 569 return "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary" 570 # All other channels share the same path on macOS. 571 return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" 572 if uname[0] == "Windows": 573 return os.path.expandvars(r"$SYSTEMDRIVE\Program Files (x86)\Google\Chrome\Application\chrome.exe") 574 self.logger.warning("Unable to find the browser binary.") 575 return None 576 577 def find_webdriver(self, channel=None): 578 return find_executable("chromedriver") 579 580 def _official_chromedriver_url(self, chrome_version): 581 # http://chromedriver.chromium.org/downloads/version-selection 582 parts = chrome_version.split(".") 583 assert len(parts) == 4 584 latest_url = "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_%s.%s.%s" % tuple(parts[:-1]) 585 try: 586 latest = get(latest_url).text.strip() 587 except requests.RequestException: 588 latest_url = "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_%s" % parts[0] 589 try: 590 latest = get(latest_url).text.strip() 591 except requests.RequestException: 592 return None 593 return "https://chromedriver.storage.googleapis.com/%s/chromedriver_%s.zip" % ( 594 latest, self.platform_string()) 595 596 def _chromium_chromedriver_url(self, chrome_version): 597 try: 598 # Try to find the Chromium build with the same revision. 599 omaha = get("https://omahaproxy.appspot.com/deps.json?version=" + chrome_version).json() 600 revision = omaha['chromium_base_position'] 601 url = "https://storage.googleapis.com/chromium-browser-snapshots/%s/%s/chromedriver_%s.zip" % ( 602 self.chromium_platform_string(), revision, self.platform_string()) 603 # Check the status without downloading the content (this is a streaming request). 604 get(url) 605 except requests.RequestException: 606 # Fall back to the tip-of-tree Chromium build. 607 revision_url = "https://storage.googleapis.com/chromium-browser-snapshots/%s/LAST_CHANGE" % ( 608 self.chromium_platform_string()) 609 revision = get(revision_url).text.strip() 610 url = "https://storage.googleapis.com/chromium-browser-snapshots/%s/%s/chromedriver_%s.zip" % ( 611 self.chromium_platform_string(), revision, self.platform_string()) 612 return url 613 614 def _latest_chromedriver_url(self, chrome_version): 615 # Remove channel suffixes (e.g. " dev"). 616 chrome_version = chrome_version.split(' ')[0] 617 return (self._official_chromedriver_url(chrome_version) or 618 self._chromium_chromedriver_url(chrome_version)) 619 620 def install_webdriver_by_version(self, version, dest=None): 621 assert version, "Cannot install ChromeDriver without Chrome version" 622 if dest is None: 623 dest = os.pwd 624 url = self._latest_chromedriver_url(version) 625 self.logger.info("Downloading ChromeDriver from %s" % url) 626 unzip(get(url).raw, dest) 627 chromedriver_dir = os.path.join( 628 dest, 'chromedriver_%s' % self.platform_string()) 629 if os.path.isfile(os.path.join(chromedriver_dir, "chromedriver")): 630 shutil.move(os.path.join(chromedriver_dir, "chromedriver"), dest) 631 shutil.rmtree(chromedriver_dir) 632 return find_executable("chromedriver", dest) 633 634 def install_webdriver(self, dest=None, channel=None, browser_binary=None): 635 if browser_binary is None: 636 browser_binary = self.find_binary(channel) 637 return self.install_webdriver_by_version( 638 self.version(browser_binary), dest) 639 640 def version(self, binary=None, webdriver_binary=None): 641 if not binary: 642 self.logger.warning("No browser binary provided.") 643 return None 644 645 if uname[0] == "Windows": 646 return _get_fileversion(binary, self.logger) 647 648 try: 649 version_string = call(binary, "--version").strip() 650 except subprocess.CalledProcessError: 651 self.logger.warning("Failed to call %s" % binary) 652 return None 653 m = re.match(r"(?:Google Chrome|Chromium) (.*)", version_string) 654 if not m: 655 self.logger.warning("Failed to extract version from: %s" % version_string) 656 return None 657 return m.group(1) 658 659 660class ChromeAndroidBase(Browser): 661 """A base class for ChromeAndroid and AndroidWebView. 662 663 On Android, WebView is based on Chromium open source project, and on some 664 versions of Android we share the library with Chrome. Therefore, we have 665 a very similar WPT runner implementation. 666 Includes webdriver installation. 667 """ 668 __metaclass__ = ABCMeta # This is an abstract class. 669 670 def __init__(self, logger): 671 super(ChromeAndroidBase, self).__init__(logger) 672 self.device_serial = None 673 674 def download(self, dest=None, channel=None, rename=None): 675 raise NotImplementedError 676 677 def install(self, dest=None, channel=None): 678 raise NotImplementedError 679 680 @abstractmethod 681 def find_binary(self, venv_path=None, channel=None): 682 raise NotImplementedError 683 684 def find_webdriver(self, channel=None): 685 return find_executable("chromedriver") 686 687 def install_webdriver(self, dest=None, channel=None, browser_binary=None): 688 if browser_binary is None: 689 browser_binary = self.find_binary(channel) 690 chrome = Chrome(self.logger) 691 return chrome.install_webdriver_by_version( 692 self.version(browser_binary), dest) 693 694 def version(self, binary=None, webdriver_binary=None): 695 if not binary: 696 self.logger.warning("No package name provided.") 697 return None 698 699 command = ['adb'] 700 if self.device_serial: 701 command.extend(['-s', self.device_serial]) 702 command.extend(['shell', 'dumpsys', 'package', binary]) 703 try: 704 output = call(*command) 705 except (subprocess.CalledProcessError, OSError): 706 self.logger.warning("Failed to call %s" % " ".join(command)) 707 return None 708 match = re.search(r'versionName=(.*)', output) 709 if not match: 710 self.logger.warning("Failed to find versionName") 711 return None 712 return match.group(1) 713 714 715class ChromeAndroid(ChromeAndroidBase): 716 """Chrome-specific interface for Android. 717 """ 718 719 product = "chrome_android" 720 requirements = "requirements_chrome_android.txt" 721 722 def find_binary(self, venv_path=None, channel=None): 723 if channel in ("beta", "dev", "canary"): 724 return "com.chrome." + channel 725 return "com.android.chrome" 726 727 728#TODO(aluo): This is largely copied from the AndroidWebView implementation. 729# Tests are not running for weblayer yet (crbug/1019521), this 730# initial implementation will help to reproduce and debug any issues. 731class AndroidWeblayer(ChromeAndroidBase): 732 """Weblayer-specific interface for Android.""" 733 734 product = "android_weblayer" 735 # TODO(aluo): replace this with weblayer version after tests are working. 736 requirements = "requirements_android_webview.txt" 737 738 def find_binary(self, venv_path=None, channel=None): 739 return "org.chromium.weblayer.shell" 740 741 742class AndroidWebview(ChromeAndroidBase): 743 """Webview-specific interface for Android. 744 745 Design doc: 746 https://docs.google.com/document/d/19cGz31lzCBdpbtSC92svXlhlhn68hrsVwSB7cfZt54o/view 747 """ 748 749 product = "android_webview" 750 requirements = "requirements_android_webview.txt" 751 752 def find_binary(self, venv_path=None, channel=None): 753 # Just get the current package name of the WebView provider. 754 # For WebView, it is not trivial to change the WebView provider, so 755 # we will just grab whatever is available. 756 # https://chromium.googlesource.com/chromium/src/+/HEAD/android_webview/docs/channels.md 757 command = ['adb'] 758 if self.device_serial: 759 command.extend(['-s', self.device_serial]) 760 command.extend(['shell', 'dumpsys', 'webviewupdate']) 761 try: 762 output = call(*command) 763 except (subprocess.CalledProcessError, OSError): 764 self.logger.warning("Failed to call %s" % " ".join(command)) 765 return None 766 m = re.search(r'^\s*Current WebView package \(name, version\): \((.*), ([0-9.]*)\)$', 767 output, re.M) 768 if m is None: 769 self.logger.warning("Unable to find current WebView package in dumpsys output") 770 return None 771 self.logger.warning("Final package name: " + m.group(1)) 772 return m.group(1) 773 774 775class ChromeiOS(Browser): 776 """Chrome-specific interface for iOS. 777 """ 778 779 product = "chrome_ios" 780 requirements = "requirements_chrome_ios.txt" 781 782 def download(self, dest=None, channel=None, rename=None): 783 raise NotImplementedError 784 785 def install(self, dest=None, channel=None): 786 raise NotImplementedError 787 788 def find_binary(self, venv_path=None, channel=None): 789 raise NotImplementedError 790 791 def find_webdriver(self, channel=None): 792 raise NotImplementedError 793 794 def install_webdriver(self, dest=None, channel=None, browser_binary=None): 795 raise NotImplementedError 796 797 def version(self, binary=None, webdriver_binary=None): 798 return None 799 800 801class Opera(Browser): 802 """Opera-specific interface. 803 804 Includes webdriver installation, and wptrunner setup methods. 805 """ 806 807 product = "opera" 808 requirements = "requirements_opera.txt" 809 810 @property 811 def binary(self): 812 if uname[0] == "Linux": 813 return "/usr/bin/opera" 814 # TODO Windows, Mac? 815 self.logger.warning("Unable to find the browser binary.") 816 return None 817 818 def download(self, dest=None, channel=None, rename=None): 819 raise NotImplementedError 820 821 def install(self, dest=None, channel=None): 822 raise NotImplementedError 823 824 def platform_string(self): 825 platform = { 826 "Linux": "linux", 827 "Windows": "win", 828 "Darwin": "mac" 829 }.get(uname[0]) 830 831 if platform is None: 832 raise ValueError("Unable to construct a valid Opera package name for current platform") 833 834 if platform == "linux": 835 bits = "64" if uname[4] == "x86_64" else "32" 836 elif platform == "mac": 837 bits = "64" 838 elif platform == "win": 839 bits = "32" 840 841 return "%s%s" % (platform, bits) 842 843 def find_binary(self, venv_path=None, channel=None): 844 raise NotImplementedError 845 846 def find_webdriver(self, channel=None): 847 return find_executable("operadriver") 848 849 def install_webdriver(self, dest=None, channel=None, browser_binary=None): 850 if dest is None: 851 dest = os.pwd 852 latest = get("https://api.github.com/repos/operasoftware/operachromiumdriver/releases/latest").json()["tag_name"] 853 url = "https://github.com/operasoftware/operachromiumdriver/releases/download/%s/operadriver_%s.zip" % (latest, 854 self.platform_string()) 855 unzip(get(url).raw, dest) 856 857 operadriver_dir = os.path.join(dest, "operadriver_%s" % self.platform_string()) 858 shutil.move(os.path.join(operadriver_dir, "operadriver"), dest) 859 shutil.rmtree(operadriver_dir) 860 861 path = find_executable("operadriver") 862 st = os.stat(path) 863 os.chmod(path, st.st_mode | stat.S_IEXEC) 864 return path 865 866 def version(self, binary=None, webdriver_binary=None): 867 """Retrieve the release version of the installed browser.""" 868 binary = binary or self.binary 869 try: 870 output = call(binary, "--version") 871 except subprocess.CalledProcessError: 872 self.logger.warning("Failed to call %s" % binary) 873 return None 874 m = re.search(r"[0-9\.]+( [a-z]+)?$", output.strip()) 875 if m: 876 return m.group(0) 877 878 879class EdgeChromium(Browser): 880 """MicrosoftEdge-specific interface.""" 881 platform = { 882 "Linux": "linux", 883 "Windows": "win", 884 "Darwin": "macos" 885 }.get(uname[0]) 886 product = "edgechromium" 887 edgedriver_name = "msedgedriver" 888 requirements = "requirements_edge_chromium.txt" 889 890 def download(self, dest=None, channel=None, rename=None): 891 raise NotImplementedError 892 893 def install(self, dest=None, channel=None): 894 raise NotImplementedError 895 896 def find_binary(self, venv_path=None, channel=None): 897 binary = None 898 if self.platform == "win": 899 binaryname = "msedge" 900 binary = find_executable(binaryname) 901 if not binary: 902 # Use paths from different Edge channels starting with Release\Beta\Dev\Canary 903 winpaths = [os.path.expanduser("~\\AppData\\Local\\Microsoft\\Edge\\Application"), 904 os.path.expandvars("$SYSTEMDRIVE\\Program Files\\Microsoft\\Edge\\Application"), 905 os.path.expandvars("$SYSTEMDRIVE\\Program Files\\Microsoft\\Edge Beta\\Application"), 906 os.path.expandvars("$SYSTEMDRIVE\\Program Files\\Microsoft\\Edge Dev\\Application"), 907 os.path.expandvars("$SYSTEMDRIVE\\Program Files (x86)\\Microsoft\\Edge\\Application"), 908 os.path.expandvars("$SYSTEMDRIVE\\Program Files (x86)\\Microsoft\\Edge Beta\\Application"), 909 os.path.expandvars("$SYSTEMDRIVE\\Program Files (x86)\\Microsoft\\Edge Dev\\Application"), 910 os.path.expanduser("~\\AppData\\Local\\Microsoft\\Edge SxS\\Application")] 911 return find_executable(binaryname, os.pathsep.join(winpaths)) 912 if self.platform == "macos": 913 binaryname = "Microsoft Edge Canary" 914 binary = find_executable(binaryname) 915 if not binary: 916 macpaths = ["/Applications/Microsoft Edge.app/Contents/MacOS", 917 os.path.expanduser("~/Applications/Microsoft Edge.app/Contents/MacOS"), 918 "/Applications/Microsoft Edge Canary.app/Contents/MacOS", 919 os.path.expanduser("~/Applications/Microsoft Edge Canary.app/Contents/MacOS")] 920 return find_executable("Microsoft Edge Canary", os.pathsep.join(macpaths)) 921 return binary 922 923 def find_webdriver(self, channel=None): 924 return find_executable("msedgedriver") 925 926 def install_webdriver(self, dest=None, channel=None, browser_binary=None): 927 if self.platform != "win" and self.platform != "macos": 928 raise ValueError("Only Windows and Mac platforms are currently supported") 929 930 if dest is None: 931 dest = os.pwd 932 933 if channel is None: 934 version_url = "https://msedgedriver.azureedge.net/LATEST_DEV" 935 else: 936 version_url = "https://msedgedriver.azureedge.net/LATEST_%s" % channel.upper() 937 version = get(version_url).text.strip() 938 939 if self.platform == "macos": 940 bits = "mac64" 941 edgedriver_path = os.path.join(dest, self.edgedriver_name) 942 else: 943 bits = "win64" if uname[4] == "x86_64" else "win32" 944 edgedriver_path = os.path.join(dest, "%s.exe" % self.edgedriver_name) 945 url = "https://msedgedriver.azureedge.net/%s/edgedriver_%s.zip" % (version, bits) 946 947 # cleanup existing Edge driver files to avoid access_denied errors when unzipping 948 if os.path.isfile(edgedriver_path): 949 # remove read-only attribute 950 os.chmod(edgedriver_path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 0777 951 print("Delete %s file" % edgedriver_path) 952 os.remove(edgedriver_path) 953 driver_notes_path = os.path.join(dest, "Driver_notes") 954 if os.path.isdir(driver_notes_path): 955 print("Delete %s folder" % driver_notes_path) 956 shutil.rmtree(driver_notes_path, ignore_errors=False, onerror=handle_remove_readonly) 957 958 self.logger.info("Downloading MSEdgeDriver from %s" % url) 959 unzip(get(url).raw, dest) 960 if os.path.isfile(edgedriver_path): 961 self.logger.info("Successfully downloaded MSEdgeDriver to %s" % edgedriver_path) 962 return find_executable(self.edgedriver_name, dest) 963 964 def version(self, binary=None, webdriver_binary=None): 965 if binary is None: 966 binary = self.find_binary() 967 if self.platform != "win": 968 try: 969 version_string = call(binary, "--version").strip() 970 except subprocess.CalledProcessError: 971 self.logger.warning("Failed to call %s" % binary) 972 return None 973 m = re.match(r"Microsoft Edge (.*) ", version_string) 974 if not m: 975 self.logger.warning("Failed to extract version from: %s" % version_string) 976 return None 977 return m.group(1) 978 else: 979 if binary is not None: 980 return _get_fileversion(binary, self.logger) 981 self.logger.warning("Failed to find Edge binary.") 982 return None 983 984 985class Edge(Browser): 986 """Edge-specific interface.""" 987 988 product = "edge" 989 requirements = "requirements_edge.txt" 990 991 def download(self, dest=None, channel=None, rename=None): 992 raise NotImplementedError 993 994 def install(self, dest=None, channel=None): 995 raise NotImplementedError 996 997 def find_binary(self, venv_path=None, channel=None): 998 raise NotImplementedError 999 1000 def find_webdriver(self, channel=None): 1001 return find_executable("MicrosoftWebDriver") 1002 1003 def install_webdriver(self, dest=None, channel=None, browser_binary=None): 1004 raise NotImplementedError 1005 1006 def version(self, binary=None, webdriver_binary=None): 1007 command = "(Get-AppxPackage Microsoft.MicrosoftEdge).Version" 1008 try: 1009 return call("powershell.exe", command).strip() 1010 except (subprocess.CalledProcessError, OSError): 1011 self.logger.warning("Failed to call %s in PowerShell" % command) 1012 return None 1013 1014 1015class EdgeWebDriver(Edge): 1016 product = "edge_webdriver" 1017 1018 1019class InternetExplorer(Browser): 1020 """Internet Explorer-specific interface.""" 1021 1022 product = "ie" 1023 requirements = "requirements_ie.txt" 1024 1025 def download(self, dest=None, channel=None, rename=None): 1026 raise NotImplementedError 1027 1028 def install(self, dest=None, channel=None): 1029 raise NotImplementedError 1030 1031 def find_binary(self, venv_path=None, channel=None): 1032 raise NotImplementedError 1033 1034 def find_webdriver(self, channel=None): 1035 return find_executable("IEDriverServer.exe") 1036 1037 def install_webdriver(self, dest=None, channel=None, browser_binary=None): 1038 raise NotImplementedError 1039 1040 def version(self, binary=None, webdriver_binary=None): 1041 return None 1042 1043 1044class Safari(Browser): 1045 """Safari-specific interface. 1046 1047 Includes installation, webdriver installation, and wptrunner setup methods. 1048 """ 1049 1050 product = "safari" 1051 requirements = "requirements_safari.txt" 1052 1053 def download(self, dest=None, channel=None, rename=None): 1054 raise NotImplementedError 1055 1056 def install(self, dest=None, channel=None): 1057 raise NotImplementedError 1058 1059 def find_binary(self, venv_path=None, channel=None): 1060 raise NotImplementedError 1061 1062 def find_webdriver(self, channel=None): 1063 path = None 1064 if channel == "preview": 1065 path = "/Applications/Safari Technology Preview.app/Contents/MacOS" 1066 return find_executable("safaridriver", path) 1067 1068 def install_webdriver(self, dest=None, channel=None, browser_binary=None): 1069 raise NotImplementedError 1070 1071 def version(self, binary=None, webdriver_binary=None): 1072 if webdriver_binary is None: 1073 self.logger.warning("Cannot find Safari version without safaridriver") 1074 return None 1075 # Use `safaridriver --version` to get the version. Example output: 1076 # "Included with Safari 12.1 (14607.1.11)" 1077 # "Included with Safari Technology Preview (Release 67, 13607.1.9.0.1)" 1078 # The `--version` flag was added in STP 67, so allow the call to fail. 1079 try: 1080 version_string = call(webdriver_binary, "--version").strip() 1081 except subprocess.CalledProcessError: 1082 self.logger.warning("Failed to call %s --version" % webdriver_binary) 1083 return None 1084 m = re.match(r"Included with Safari (.*)", version_string) 1085 if not m: 1086 self.logger.warning("Failed to extract version from: %s" % version_string) 1087 return None 1088 return m.group(1) 1089 1090 1091class Servo(Browser): 1092 """Servo-specific interface.""" 1093 1094 product = "servo" 1095 requirements = "requirements_servo.txt" 1096 1097 def platform_components(self): 1098 platform = { 1099 "Linux": "linux", 1100 "Windows": "win", 1101 "Darwin": "mac" 1102 }.get(uname[0]) 1103 1104 if platform is None: 1105 raise ValueError("Unable to construct a valid Servo package for current platform") 1106 1107 if platform == "linux": 1108 extension = ".tar.gz" 1109 decompress = untar 1110 elif platform == "win" or platform == "mac": 1111 raise ValueError("Unable to construct a valid Servo package for current platform") 1112 1113 return (platform, extension, decompress) 1114 1115 def _get(self, channel="nightly"): 1116 if channel != "nightly": 1117 raise ValueError("Only nightly versions of Servo are available") 1118 1119 platform, extension, _ = self.platform_components() 1120 url = "https://download.servo.org/nightly/%s/servo-latest%s" % (platform, extension) 1121 return get(url) 1122 1123 def download(self, dest=None, channel="nightly", rename=None): 1124 if dest is None: 1125 dest = os.pwd 1126 1127 resp = self._get(dest, channel) 1128 _, extension, _ = self.platform_components() 1129 1130 filename = rename if rename is not None else "servo-latest" 1131 with open(os.path.join(dest, "%s%s" % (filename, extension,)), "w") as f: 1132 f.write(resp.content) 1133 1134 def install(self, dest=None, channel="nightly"): 1135 """Install latest Browser Engine.""" 1136 if dest is None: 1137 dest = os.pwd 1138 1139 _, _, decompress = self.platform_components() 1140 1141 resp = self._get(channel) 1142 decompress(resp.raw, dest=dest) 1143 path = find_executable("servo", os.path.join(dest, "servo")) 1144 st = os.stat(path) 1145 os.chmod(path, st.st_mode | stat.S_IEXEC) 1146 return path 1147 1148 def find_binary(self, venv_path=None, channel=None): 1149 path = find_executable("servo", os.path.join(venv_path, "servo")) 1150 if path is None: 1151 path = find_executable("servo") 1152 return path 1153 1154 def find_webdriver(self, channel=None): 1155 return None 1156 1157 def install_webdriver(self, dest=None, channel=None, browser_binary=None): 1158 raise NotImplementedError 1159 1160 def version(self, binary=None, webdriver_binary=None): 1161 """Retrieve the release version of the installed browser.""" 1162 output = call(binary, "--version") 1163 m = re.search(r"Servo ([0-9\.]+-[a-f0-9]+)?(-dirty)?$", output.strip()) 1164 if m: 1165 return m.group(0) 1166 1167 1168class ServoWebDriver(Servo): 1169 product = "servodriver" 1170 1171 1172class Sauce(Browser): 1173 """Sauce-specific interface.""" 1174 1175 product = "sauce" 1176 requirements = "requirements_sauce.txt" 1177 1178 def download(self, dest=None, channel=None, rename=None): 1179 raise NotImplementedError 1180 1181 def install(self, dest=None, channel=None): 1182 raise NotImplementedError 1183 1184 def find_binary(self, venev_path=None, channel=None): 1185 raise NotImplementedError 1186 1187 def find_webdriver(self, channel=None): 1188 raise NotImplementedError 1189 1190 def install_webdriver(self, dest=None, channel=None, browser_binary=None): 1191 raise NotImplementedError 1192 1193 def version(self, binary=None, webdriver_binary=None): 1194 return None 1195 1196 1197class WebKit(Browser): 1198 """WebKit-specific interface.""" 1199 1200 product = "webkit" 1201 requirements = "requirements_webkit.txt" 1202 1203 def download(self, dest=None, channel=None, rename=None): 1204 raise NotImplementedError 1205 1206 def install(self, dest=None, channel=None): 1207 raise NotImplementedError 1208 1209 def find_binary(self, venv_path=None, channel=None): 1210 return None 1211 1212 def find_webdriver(self, channel=None): 1213 return None 1214 1215 def install_webdriver(self, dest=None, channel=None, browser_binary=None): 1216 raise NotImplementedError 1217 1218 def version(self, binary=None, webdriver_binary=None): 1219 return None 1220 1221 1222class WebKitGTKMiniBrowser(WebKit): 1223 1224 def find_binary(self, venv_path=None, channel=None): 1225 libexecpaths = ["/usr/libexec/webkit2gtk-4.0"] # Fedora path 1226 triplet = "x86_64-linux-gnu" 1227 # Try to use GCC to detect this machine triplet 1228 gcc = find_executable("gcc") 1229 if gcc: 1230 try: 1231 triplet = call(gcc, "-dumpmachine").strip() 1232 except subprocess.CalledProcessError: 1233 pass 1234 # Add Debian/Ubuntu path 1235 libexecpaths.append("/usr/lib/%s/webkit2gtk-4.0" % triplet) 1236 if channel == "nightly": 1237 libexecpaths.append("/opt/webkitgtk/nightly") 1238 return find_executable("MiniBrowser", os.pathsep.join(libexecpaths)) 1239 1240 def find_webdriver(self, channel=None): 1241 path = os.environ['PATH'] 1242 if channel == "nightly": 1243 path = "%s:%s" % (path, "/opt/webkitgtk/nightly") 1244 return find_executable("WebKitWebDriver", path) 1245 1246 def version(self, binary=None, webdriver_binary=None): 1247 if binary is None: 1248 return None 1249 try: # WebKitGTK MiniBrowser before 2.26.0 doesn't support --version 1250 output = call(binary, "--version").strip() 1251 except subprocess.CalledProcessError: 1252 return None 1253 # Example output: "WebKitGTK 2.26.1" 1254 if output: 1255 m = re.match(r"WebKitGTK (.+)", output) 1256 if not m: 1257 self.logger.warning("Failed to extract version from: %s" % output) 1258 return None 1259 return m.group(1) 1260 return None 1261 1262 1263class Epiphany(Browser): 1264 """Epiphany-specific interface.""" 1265 1266 product = "epiphany" 1267 requirements = "requirements_epiphany.txt" 1268 1269 def download(self, dest=None, channel=None, rename=None): 1270 raise NotImplementedError 1271 1272 def install(self, dest=None, channel=None): 1273 raise NotImplementedError 1274 1275 def find_binary(self, venv_path=None, channel=None): 1276 return find_executable("epiphany") 1277 1278 def find_webdriver(self, channel=None): 1279 return find_executable("WebKitWebDriver") 1280 1281 def install_webdriver(self, dest=None, channel=None, browser_binary=None): 1282 raise NotImplementedError 1283 1284 def version(self, binary=None, webdriver_binary=None): 1285 if binary is None: 1286 return None 1287 output = call(binary, "--version") 1288 if output: 1289 # Stable release output looks like: "Web 3.30.2" 1290 # Tech Preview output looks like "Web 3.31.3-88-g97db4f40f" 1291 return output.split()[1] 1292 return None 1293