@@ -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 {
220302Future <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+ }
0 commit comments