Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions lib/web_ui/dev/common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ abstract class PlatformBinding {
int getChromeBuild(YamlMap chromeLock);
String getChromeDownloadUrl(String version);
String getFirefoxDownloadUrl(String version);
String getFirefoxDownloadFilename(String version);
String getChromeExecutablePath(io.Directory versionDir);
String getFirefoxExecutablePath(io.Directory versionDir);
String getFirefoxLatestVersionUrl();
Expand Down Expand Up @@ -75,7 +76,12 @@ class _WindowsBinding implements PlatformBinding {

@override
String getFirefoxDownloadUrl(String version) =>
'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/win64/en-US/firefox-${version}.exe';
'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/win64/en-US/'
'${getFirefoxDownloadFilename(version)}';

@override
String getFirefoxDownloadFilename(String version) =>
'firefox-${version}.exe';

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

@override
String getFirefoxDownloadUrl(String version) =>
'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/linux-x86_64/en-US/firefox-${version}.tar.bz2';
'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/linux-x86_64/en-US/'
'${getFirefoxDownloadFilename(version)}';

@override
String getFirefoxDownloadFilename(String version) =>
'firefox-${version}.tar.bz2';

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

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

@override
String getFirefoxExecutablePath(io.Directory versionDir) {
throw UnimplementedError();
}
String getFirefoxDownloadFilename(String version) =>
'Firefox ${version}.dmg';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: is there a space between name and version?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes


@override
String getFirefoxExecutablePath(io.Directory versionDir) =>
path.join(versionDir.path, 'Firefox.app','Contents','MacOS', 'firefox');

@override
String getFirefoxLatestVersionUrl() =>
Expand Down
8 changes: 5 additions & 3 deletions lib/web_ui/dev/firefox.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,20 @@ class Firefox extends Browser {
// https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#Browser
//
var dir = createTempDir();
bool isMac = Platform.isMacOS;
var args = [
url.toString(),
'--headless',
'-width $kMaxScreenshotWidth',
'-height $kMaxScreenshotHeight',
'-new-window',
'-new-instance',
isMac ? '--new-window' : '-new-window',
isMac ? '--new-instance' : '-new-instance',
'--start-debugger-server $kDevtoolsPort',
];

final Process process =
await Process.start(installation.executable, args);
await Process.start(installation.executable, args,
workingDirectory: dir);

remoteDebuggerCompleter.complete(
getRemoteDebuggerUrl(Uri.parse('http://localhost:$kDevtoolsPort')));
Expand Down
150 changes: 137 additions & 13 deletions lib/web_ui/dev/firefox_installer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ Future<BrowserInstallation> getOrInstallFirefox(
}) async {
// These tests are aimed to run only on the Linux containers in Cirrus.
// Therefore Firefox installation is implemented only for Linux now.
if (!io.Platform.isLinux) {
throw UnimplementedError();
if (!io.Platform.isLinux && !io.Platform.isMacOS) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you!

throw UnimplementedError('Firefox Installer is only supported on Linux '
'and Mac operating systems');
}

infoLog ??= io.stdout;
Expand Down Expand Up @@ -129,7 +130,9 @@ class FirefoxInstaller {
}

static Future<FirefoxInstaller> latest() async {
final String latestVersion = await fetchLatestFirefoxVersion();
final String latestVersion = io.Platform.isLinux
? await fetchLatestFirefoxVersionLinux()
: await fetchLatestFirefoxVersionMacOS();
return FirefoxInstaller(version: latestVersion);
}

Expand Down Expand Up @@ -169,11 +172,15 @@ class FirefoxInstaller {
/// Install the browser by downloading from the web.
Future<void> install() async {
final io.File downloadedFile = await _download();
await _uncompress(downloadedFile);
if (io.Platform.isLinux) {
await _uncompress(downloadedFile);
} else if (io.Platform.isMacOS) {
await _mountDmgAndCopy(downloadedFile);
}
downloadedFile.deleteSync();
}

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

final io.File downloadedFile =
io.File(path.join(versionDir.path, 'firefox-${version}.tar.bz2'));
await download.stream.pipe(downloadedFile.openWrite());
io.File(path.join(versionDir.path, PlatformBinding.instance.getFirefoxDownloadFilename(version)));
io.IOSink sink = downloadedFile.openWrite();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! So if we don't do this, we are leaking.

I think we made this error in a few more places :) https://github.com/flutter/engine/blob/master/lib/web_ui/dev/chrome_installer.dart#L190

I'll fix them!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

await download.stream.pipe(sink);
await sink.flush();
await sink.close();

return downloadedFile;
}

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

/// Mounts the dmg file using hdiutil, copies content of the volume to
/// target path and then unmounts dmg ready for deletion.
Future<void> _mountDmgAndCopy(io.File dmgFile) async {
String volumeName = await _hdiUtilMount(dmgFile);

final String sourcePath = '$volumeName/Firefox.app';
final String targetPath = path.dirname(dmgFile.path);
try {
io.ProcessResult installResult = await io.Process.run('cp', <String>[
'-r',
sourcePath,
targetPath,
]);
if (installResult.exitCode != 0) {
throw BrowserInstallerException(
'Failed to copy Firefox disk image contents from '
'$sourcePath to $targetPath.\n'
'Exit code ${installResult.exitCode}.\n'
'${installResult.stderr}');
}
} finally {
await _hdiUtilUnmount(volumeName);
}
}

Future<String> _hdiUtilMount(io.File dmgFile) async {
io.ProcessResult mountResult = await io.Process.run('hdiutil', <String>[
'attach',
'-readonly',
'${dmgFile.path}',
]);
if (mountResult.exitCode != 0) {
throw BrowserInstallerException(
'Failed to mount Firefox disk image ${dmgFile.path}.\n'
'Exit code ${mountResult.exitCode}.\n${mountResult.stderr}');
}

List<String> processOutput = mountResult.stdout.split('\n');
String volumePath = _volumeFromMountResult(processOutput);
if (volumePath == null) {
throw BrowserInstallerException(
'Failed to parse mount dmg result ${processOutput.join('\n')}.\n'
'Expected /Volumes/{volume name}');
}
return volumePath;
}

// Parses volume from mount result.
// Output is of form: {devicename} /Volumes/{name}.
String _volumeFromMountResult(List<String> lines) {
for (String line in lines) {
int pos = line.indexOf('/Volumes');
if (pos != -1) {
return line.substring(pos);
}
}
return null;
}

Future<void> _hdiUtilUnmount(String volumeName) async {
io.ProcessResult unmountResult = await io.Process.run('hdiutil', <String>[
'unmount',
'$volumeName',
]);
if (unmountResult.exitCode != 0) {
throw BrowserInstallerException(
'Failed to unmount Firefox disk image ${volumeName}.\n'
'Exit code ${unmountResult.exitCode}. ${unmountResult.stderr}');
}
}

void close() {
client.close();
}
Expand All @@ -220,17 +302,22 @@ class FirefoxInstaller {
Future<String> _findSystemFirefoxExecutable() async {
final io.ProcessResult which =
await io.Process.run('which', <String>['firefox']);

if (which.exitCode != 0) {
bool found = which.exitCode != 0;
const String fireFoxDefaultInstallPath =
'/Applications/Firefox.app/Contents/MacOS/firefox';
if (!found) {
if (io.Platform.isMacOS &&
io.File(fireFoxDefaultInstallPath).existsSync()) {
return Future.value(fireFoxDefaultInstallPath);
}
throw BrowserInstallerException(
'Failed to locate system Firefox installation.');
}

return which.stdout;
}

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

return version.substring(version.lastIndexOf('-') + 1);
}

/// Fetches the latest available Firefox build version on Mac OS.
Future<String> fetchLatestFirefoxVersionMacOS() async {
final RegExp forFirefoxVersion = RegExp("firefox\/releases\/[0-9.]\+[0-9]");
final io.HttpClientRequest request = await io.HttpClient()
.getUrl(Uri.parse(PlatformBinding.instance.getFirefoxLatestVersionUrl()));
request.followRedirects = false;
// We will parse the HttpHeaders to find the redirect location.
final io.HttpClientResponse response = await request.close();

final String location = response.headers.value('location');
final String version = forFirefoxVersion.stringMatch(location);
return version.substring(version.lastIndexOf('/') + 1);
}

Future<BrowserInstallation> getInstaller({String requestedVersion = 'latest'}) async {
FirefoxInstaller installer;
try {
installer = requestedVersion == 'latest'
? await FirefoxInstaller.latest()
: FirefoxInstaller(version: requestedVersion);

if (installer.isInstalled) {
print('Installation was skipped because Firefox version '
'${installer.version} is already installed.');
} else {
print('Installing Firefox version: ${installer.version}');
await installer.install();
final BrowserInstallation installation = installer.getInstallation();
print(
'Installations complete. To launch it run ${installation.executable}');
}
return installer.getInstallation();
} finally {
installer?.close();
}
}
2 changes: 1 addition & 1 deletion lib/web_ui/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ dependencies:
meta: 1.1.7

dev_dependencies:
http: 0.12.0+2
http: 0.12.0+4
image: 2.1.4
mockito: 4.1.1
path: 1.6.4
Expand Down