• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2023 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5from __future__ import annotations
6
7import abc
8import urllib.parse
9from typing import (TYPE_CHECKING, Dict, Final, Iterable, Optional, Tuple, Type,
10                    Union)
11
12from crossbench.browsers.downloader import DMGArchiveHelper, Downloader
13from crossbench.browsers.firefox.version import FirefoxVersion
14
15if TYPE_CHECKING:
16  from crossbench.browsers.version import BrowserVersion
17  from crossbench.path import AnyPathLike, LocalPath
18  from crossbench.plt.base import Platform
19
20
21_PLATFORM_NAME_LOOKUP: Final[Dict[Tuple[str, str], str]] = {
22    ("win", "ia32"): "win32",
23    ("win", "x64"): "win64",
24    ("win", "arm64"): "win-aarch64",
25    ("linux", "x64"): "linux-x86-64",
26    ("linux", "ia32"): "linux-i686",
27    ("macos", "x64"): "mac",
28    ("macos", "arm64"): "mac"
29}
30
31
32class FirefoxDownloader(Downloader):
33  # TODO: support nightly versions as well
34  STORAGE_URL: str = "https://ftp.mozilla.org/pub/firefox/releases/"
35
36  @classmethod
37  def _get_loader_cls(cls,
38                      browser_platform: Platform) -> Type[FirefoxDownloader]:
39    if browser_platform.is_macos:
40      return FirefoxDownloaderMacOS
41    if browser_platform.is_linux:
42      return FirefoxDownloaderLinux
43    if browser_platform.is_win:
44      return FirefoxDownloaderWin
45    raise ValueError("Downloading Firefox is not supported "
46                     f"{browser_platform.name} {browser_platform.machine}")
47
48  @classmethod
49  def is_valid_version(cls, path_or_identifier: str) -> bool:
50    return FirefoxVersion.is_valid_unique(path_or_identifier)
51
52  @classmethod
53  def _is_valid(cls, path_or_identifier: AnyPathLike,
54                browser_platform: Platform) -> bool:
55    if cls.is_valid_version(str(path_or_identifier)):
56      return True
57    path = browser_platform.path(path_or_identifier)
58    return (browser_platform.exists(path) and
59            path.name.endswith(cls.ARCHIVE_SUFFIX))
60
61  def __init__(self, version_identifier: Union[str,
62                                               LocalPath], browser_type: str,
63               platform_name: str, browser_platform: Platform):
64    assert not browser_type
65    assert not platform_name
66    firefox_platform_name = _PLATFORM_NAME_LOOKUP.get(browser_platform.key)
67    if not firefox_platform_name:
68      raise ValueError(
69          "Unsupported macOS architecture for downloading Firefox: "
70          f"got={browser_platform.machine}")
71    super().__init__(version_identifier, "firefox", firefox_platform_name,
72                     browser_platform)
73
74  def _parse_version(self, version_identifier: str) -> BrowserVersion:
75    return FirefoxVersion.parse(version_identifier)
76
77  def _requested_version_validation(self) -> None:
78    pass
79
80  def _find_archive_url(self) -> Tuple[BrowserVersion, Optional[str]]:
81    # Quick probe for complete versions
82    if self._requested_version.is_complete:
83      return self._find_exact_archive_url()
84    raise NotImplementedError("Only full-release versions supported.")
85
86  def _find_exact_archive_url(self) -> Tuple[BrowserVersion, Optional[str]]:
87    folder_url = (
88        f"{self.STORAGE_URL}{self._requested_version.parts_str}/mac/en-GB")
89    return tuple(self._archive_urls(folder_url, self._requested_version))[0]
90
91  def _download_archive(self, archive_url: str, tmp_dir: LocalPath) -> None:
92    self._browser_platform.download_to(
93        archive_url, tmp_dir / f"archive.{self.ARCHIVE_SUFFIX}")
94    archive_candidates = list(tmp_dir.glob("*"))
95    assert len(archive_candidates) == 1, (
96        f"Download tmp dir contains more than one file: {tmp_dir}"
97        f"{archive_candidates}")
98    candidate = archive_candidates[0]
99    assert not self._archive_path.exists(), (
100        f"Archive was already downloaded: {self._archive_path}")
101    candidate.replace(self._archive_path)
102
103  @abc.abstractmethod
104  def _install_archive(self, archive_path: LocalPath) -> None:
105    pass
106
107
108class FirefoxDownloaderLinux(FirefoxDownloader):
109  ARCHIVE_SUFFIX: str = ".tar.bz2"
110
111  @classmethod
112  def is_valid(cls, path_or_identifier: AnyPathLike,
113               browser_platform: Platform) -> bool:
114    return cls._is_valid(path_or_identifier, browser_platform)
115
116  def _installed_app_path(self) -> LocalPath:
117    # TODO: support local vs remote
118    return self._extracted_path() / "firefox-bin"
119
120  def _archive_urls(
121      self, folder_url: str,
122      version: BrowserVersion) -> Iterable[Tuple[BrowserVersion, str]]:
123    return ((version, f"{folder_url}/firefox-{version.parts_str}.tar.bz2"),)
124
125  def _install_archive(self, archive_path: LocalPath) -> None:
126    raise NotImplementedError("Missing linux support")
127
128
129class FirefoxDownloaderMacOS(FirefoxDownloader):
130  ARCHIVE_SUFFIX: str = ".dmg"
131  MIN_MAC_ARM64_MILESTONE: Final[int] = 84
132
133  @classmethod
134  def is_valid(cls, path_or_identifier: AnyPathLike,
135               browser_platform: Platform) -> bool:
136    return cls._is_valid(path_or_identifier, browser_platform)
137
138  def _requested_version_validation(self) -> None:
139    major_version: int = self._requested_version.major
140    if (self._browser_platform.is_macos and self._browser_platform.is_arm64 and
141        major_version < self.MIN_MAC_ARM64_MILESTONE):
142      raise ValueError(
143          "Native Mac arm64/m1 Firefox version is available with v84, "
144          f"but requested {major_version}.")
145
146  def _download_archive(self, archive_url: str, tmp_dir: LocalPath) -> None:
147    assert self._browser_platform.is_macos
148    if self._browser_platform.is_arm64 and (self._requested_version
149                                            < self.MIN_MAC_ARM64_MILESTONE):
150      raise ValueError(
151          "Firefox Arm64 Apple Silicon is only available starting with "
152          f"{self.MIN_MAC_ARM64_MILESTONE}, "
153          f"but requested {self._requested_version} is too old.")
154    super()._download_archive(archive_url, tmp_dir)
155
156  def _archive_urls(
157      self, folder_url: str,
158      version: BrowserVersion) -> Iterable[Tuple[BrowserVersion, str]]:
159    archive_name = urllib.parse.quote(f"Firefox {version.parts_str}.dmg")
160    return ((version, f"{folder_url}/{archive_name}"),)
161
162  def _extracted_path(self) -> LocalPath:
163    # TODO: support local vs remote
164    return self._installed_app_path()
165
166  def _installed_app_path(self) -> LocalPath:
167    return self._out_dir / f"Firefox {self._requested_version}.app"
168
169  def _install_archive(self, archive_path: LocalPath) -> None:
170    extracted_path = self._extracted_path()
171    DMGArchiveHelper.extract(self.host_platform, archive_path, extracted_path)
172    assert extracted_path.exists()
173
174
175class FirefoxDownloaderWin(FirefoxDownloader):
176
177  @classmethod
178  def is_valid(cls, path_or_identifier: AnyPathLike,
179               browser_platform: Platform) -> bool:
180    return False
181
182  def _install_archive(self, archive_path: LocalPath) -> None:
183    raise NotImplementedError("Missing windows support")
184