Skip to content

Commit 7f942f2

Browse files
authored
Add support for firefox mac installer. Update web_ui pubspec for http.wq (flutter#17044)
* Add support for firefox mac installer. Update web_ui pubspec for http.wq * Addressed review comment 'final'
1 parent 4f4fdbf commit 7f942f2

File tree

4 files changed

+164
-23
lines changed

4 files changed

+164
-23
lines changed

lib/web_ui/dev/common.dart

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ abstract class PlatformBinding {
4848
int getChromeBuild(YamlMap chromeLock);
4949
String getChromeDownloadUrl(String version);
5050
String getFirefoxDownloadUrl(String version);
51+
String getFirefoxDownloadFilename(String version);
5152
String getChromeExecutablePath(io.Directory versionDir);
5253
String getFirefoxExecutablePath(io.Directory versionDir);
5354
String getFirefoxLatestVersionUrl();
@@ -75,7 +76,12 @@ class _WindowsBinding implements PlatformBinding {
7576

7677
@override
7778
String getFirefoxDownloadUrl(String version) =>
78-
'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/win64/en-US/firefox-${version}.exe';
79+
'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/win64/en-US/'
80+
'${getFirefoxDownloadFilename(version)}';
81+
82+
@override
83+
String getFirefoxDownloadFilename(String version) =>
84+
'firefox-${version}.exe';
7985

8086
@override
8187
String getFirefoxExecutablePath(io.Directory versionDir) =>
@@ -110,7 +116,12 @@ class _LinuxBinding implements PlatformBinding {
110116

111117
@override
112118
String getFirefoxDownloadUrl(String version) =>
113-
'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/linux-x86_64/en-US/firefox-${version}.tar.bz2';
119+
'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/linux-x86_64/en-US/'
120+
'${getFirefoxDownloadFilename(version)}';
121+
122+
@override
123+
String getFirefoxDownloadFilename(String version) =>
124+
'firefox-${version}.tar.bz2';
114125

115126
@override
116127
String getFirefoxExecutablePath(io.Directory versionDir) =>
@@ -150,12 +161,16 @@ class _MacBinding implements PlatformBinding {
150161

151162
@override
152163
String getFirefoxDownloadUrl(String version) =>
153-
'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/mac/en-US/firefox-${version}.dmg';
164+
'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/mac/en-US/'
165+
'${getFirefoxDownloadFilename(version)}';
154166

155167
@override
156-
String getFirefoxExecutablePath(io.Directory versionDir) {
157-
throw UnimplementedError();
158-
}
168+
String getFirefoxDownloadFilename(String version) =>
169+
'Firefox ${version}.dmg';
170+
171+
@override
172+
String getFirefoxExecutablePath(io.Directory versionDir) =>
173+
path.join(versionDir.path, 'Firefox.app','Contents','MacOS', 'firefox');
159174

160175
@override
161176
String getFirefoxLatestVersionUrl() =>

lib/web_ui/dev/firefox.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,20 @@ class Firefox extends Browser {
4747
// https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#Browser
4848
//
4949
var dir = createTempDir();
50+
bool isMac = Platform.isMacOS;
5051
var args = [
5152
url.toString(),
5253
'--headless',
5354
'-width $kMaxScreenshotWidth',
5455
'-height $kMaxScreenshotHeight',
55-
'-new-window',
56-
'-new-instance',
56+
isMac ? '--new-window' : '-new-window',
57+
isMac ? '--new-instance' : '-new-instance',
5758
'--start-debugger-server $kDevtoolsPort',
5859
];
5960

6061
final Process process =
61-
await Process.start(installation.executable, args);
62+
await Process.start(installation.executable, args,
63+
workingDirectory: dir);
6264

6365
remoteDebuggerCompleter.complete(
6466
getRemoteDebuggerUrl(Uri.parse('http://localhost:$kDevtoolsPort')));

lib/web_ui/dev/firefox_installer.dart

Lines changed: 137 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,9 @@ Future<BrowserInstallation> getOrInstallFirefox(
6767
}) async {
6868
// These tests are aimed to run only on the Linux containers in Cirrus.
6969
// Therefore Firefox installation is implemented only for Linux now.
70-
if (!io.Platform.isLinux) {
71-
throw UnimplementedError();
70+
if (!io.Platform.isLinux && !io.Platform.isMacOS) {
71+
throw UnimplementedError('Firefox Installer is only supported on Linux '
72+
'and Mac operating systems');
7273
}
7374

7475
infoLog ??= io.stdout;
@@ -129,7 +130,9 @@ class FirefoxInstaller {
129130
}
130131

131132
static Future<FirefoxInstaller> latest() async {
132-
final String latestVersion = await fetchLatestFirefoxVersion();
133+
final String latestVersion = io.Platform.isLinux
134+
? await fetchLatestFirefoxVersionLinux()
135+
: await fetchLatestFirefoxVersionMacOS();
133136
return FirefoxInstaller(version: latestVersion);
134137
}
135138

@@ -169,11 +172,15 @@ class FirefoxInstaller {
169172
/// Install the browser by downloading from the web.
170173
Future<void> install() async {
171174
final io.File downloadedFile = await _download();
172-
await _uncompress(downloadedFile);
175+
if (io.Platform.isLinux) {
176+
await _uncompress(downloadedFile);
177+
} else if (io.Platform.isMacOS) {
178+
await _mountDmgAndCopy(downloadedFile);
179+
}
173180
downloadedFile.deleteSync();
174181
}
175182

176-
/// Downloads the browser version from web.
183+
/// Downloads the browser version from web into a target file.
177184
/// See [version].
178185
Future<io.File> _download() async {
179186
if (versionDir.existsSync()) {
@@ -188,13 +195,17 @@ class FirefoxInstaller {
188195
));
189196

190197
final io.File downloadedFile =
191-
io.File(path.join(versionDir.path, 'firefox-${version}.tar.bz2'));
192-
await download.stream.pipe(downloadedFile.openWrite());
198+
io.File(path.join(versionDir.path, PlatformBinding.instance.getFirefoxDownloadFilename(version)));
199+
io.IOSink sink = downloadedFile.openWrite();
200+
await download.stream.pipe(sink);
201+
await sink.flush();
202+
await sink.close();
193203

194204
return downloadedFile;
195205
}
196206

197-
/// Uncompress the downloaded browser files.
207+
/// Uncompress the downloaded browser files for operating systems that
208+
/// use a zip archive.
198209
/// See [version].
199210
Future<void> _uncompress(io.File downloadedFile) async {
200211
final io.ProcessResult unzipResult = await io.Process.run('tar', <String>[
@@ -212,6 +223,77 @@ class FirefoxInstaller {
212223
}
213224
}
214225

226+
/// Mounts the dmg file using hdiutil, copies content of the volume to
227+
/// target path and then unmounts dmg ready for deletion.
228+
Future<void> _mountDmgAndCopy(io.File dmgFile) async {
229+
String volumeName = await _hdiUtilMount(dmgFile);
230+
231+
final String sourcePath = '$volumeName/Firefox.app';
232+
final String targetPath = path.dirname(dmgFile.path);
233+
try {
234+
io.ProcessResult installResult = await io.Process.run('cp', <String>[
235+
'-r',
236+
sourcePath,
237+
targetPath,
238+
]);
239+
if (installResult.exitCode != 0) {
240+
throw BrowserInstallerException(
241+
'Failed to copy Firefox disk image contents from '
242+
'$sourcePath to $targetPath.\n'
243+
'Exit code ${installResult.exitCode}.\n'
244+
'${installResult.stderr}');
245+
}
246+
} finally {
247+
await _hdiUtilUnmount(volumeName);
248+
}
249+
}
250+
251+
Future<String> _hdiUtilMount(io.File dmgFile) async {
252+
io.ProcessResult mountResult = await io.Process.run('hdiutil', <String>[
253+
'attach',
254+
'-readonly',
255+
'${dmgFile.path}',
256+
]);
257+
if (mountResult.exitCode != 0) {
258+
throw BrowserInstallerException(
259+
'Failed to mount Firefox disk image ${dmgFile.path}.\n'
260+
'Exit code ${mountResult.exitCode}.\n${mountResult.stderr}');
261+
}
262+
263+
List<String> processOutput = mountResult.stdout.split('\n');
264+
String volumePath = _volumeFromMountResult(processOutput);
265+
if (volumePath == null) {
266+
throw BrowserInstallerException(
267+
'Failed to parse mount dmg result ${processOutput.join('\n')}.\n'
268+
'Expected /Volumes/{volume name}');
269+
}
270+
return volumePath;
271+
}
272+
273+
// Parses volume from mount result.
274+
// Output is of form: {devicename} /Volumes/{name}.
275+
String _volumeFromMountResult(List<String> lines) {
276+
for (String line in lines) {
277+
int pos = line.indexOf('/Volumes');
278+
if (pos != -1) {
279+
return line.substring(pos);
280+
}
281+
}
282+
return null;
283+
}
284+
285+
Future<void> _hdiUtilUnmount(String volumeName) async {
286+
io.ProcessResult unmountResult = await io.Process.run('hdiutil', <String>[
287+
'unmount',
288+
'$volumeName',
289+
]);
290+
if (unmountResult.exitCode != 0) {
291+
throw BrowserInstallerException(
292+
'Failed to unmount Firefox disk image ${volumeName}.\n'
293+
'Exit code ${unmountResult.exitCode}. ${unmountResult.stderr}');
294+
}
295+
}
296+
215297
void close() {
216298
client.close();
217299
}
@@ -220,17 +302,22 @@ class FirefoxInstaller {
220302
Future<String> _findSystemFirefoxExecutable() async {
221303
final io.ProcessResult which =
222304
await io.Process.run('which', <String>['firefox']);
223-
224-
if (which.exitCode != 0) {
305+
bool found = which.exitCode != 0;
306+
const String fireFoxDefaultInstallPath =
307+
'/Applications/Firefox.app/Contents/MacOS/firefox';
308+
if (!found) {
309+
if (io.Platform.isMacOS &&
310+
io.File(fireFoxDefaultInstallPath).existsSync()) {
311+
return Future.value(fireFoxDefaultInstallPath);
312+
}
225313
throw BrowserInstallerException(
226314
'Failed to locate system Firefox installation.');
227315
}
228-
229316
return which.stdout;
230317
}
231318

232-
/// Fetches the latest available Chrome build version.
233-
Future<String> fetchLatestFirefoxVersion() async {
319+
/// Fetches the latest available Firefox build version on Linux.
320+
Future<String> fetchLatestFirefoxVersionLinux() async {
234321
final RegExp forFirefoxVersion = RegExp("firefox-[0-9.]\+[0-9]");
235322
final io.HttpClientRequest request = await io.HttpClient()
236323
.getUrl(Uri.parse(PlatformBinding.instance.getFirefoxLatestVersionUrl()));
@@ -243,3 +330,40 @@ Future<String> fetchLatestFirefoxVersion() async {
243330

244331
return version.substring(version.lastIndexOf('-') + 1);
245332
}
333+
334+
/// Fetches the latest available Firefox build version on Mac OS.
335+
Future<String> fetchLatestFirefoxVersionMacOS() async {
336+
final RegExp forFirefoxVersion = RegExp("firefox\/releases\/[0-9.]\+[0-9]");
337+
final io.HttpClientRequest request = await io.HttpClient()
338+
.getUrl(Uri.parse(PlatformBinding.instance.getFirefoxLatestVersionUrl()));
339+
request.followRedirects = false;
340+
// We will parse the HttpHeaders to find the redirect location.
341+
final io.HttpClientResponse response = await request.close();
342+
343+
final String location = response.headers.value('location');
344+
final String version = forFirefoxVersion.stringMatch(location);
345+
return version.substring(version.lastIndexOf('/') + 1);
346+
}
347+
348+
Future<BrowserInstallation> getInstaller({String requestedVersion = 'latest'}) async {
349+
FirefoxInstaller installer;
350+
try {
351+
installer = requestedVersion == 'latest'
352+
? await FirefoxInstaller.latest()
353+
: FirefoxInstaller(version: requestedVersion);
354+
355+
if (installer.isInstalled) {
356+
print('Installation was skipped because Firefox version '
357+
'${installer.version} is already installed.');
358+
} else {
359+
print('Installing Firefox version: ${installer.version}');
360+
await installer.install();
361+
final BrowserInstallation installation = installer.getInstallation();
362+
print(
363+
'Installations complete. To launch it run ${installation.executable}');
364+
}
365+
return installer.getInstallation();
366+
} finally {
367+
installer?.close();
368+
}
369+
}

lib/web_ui/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ dependencies:
88
meta: 1.1.7
99

1010
dev_dependencies:
11-
http: 0.12.0+2
11+
http: 0.12.0+4
1212
image: 2.1.4
1313
mockito: 4.1.1
1414
path: 1.6.4

0 commit comments

Comments
 (0)