blob: e86433f93d3b100a1ee111488d7f1861b9478770 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:io' as io;
import 'package:http/http.dart';
import 'package:path/path.dart' as path;
import 'common.dart';
import 'environment.dart';
import 'exceptions.dart';
const String _firefoxExecutableVar = 'FIREFOX_EXECUTABLE';
/// Returns the installation of Firefox, installing it if necessary.
///
/// If [requestedVersion] is null, uses the version specified on the
/// command-line. If not specified on the command-line, uses the version
/// specified in the "package_lock.yaml" file.
///
/// If [requestedVersion] is not null, installs that version. The value
/// may be "latest" (the latest stable Firefox version), "system"
/// (manually installed Firefox on the current operating system), or an
/// exact version number such as 69.0.3. Versions of Firefox can be found here:
///
/// https://download-installer.cdn.mozilla.net/pub/firefox/releases/
Future<BrowserInstallation> getOrInstallFirefox(
String requestedVersion, {
StringSink? infoLog,
}) 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 && !io.Platform.isMacOS) {
throw UnimplementedError('Firefox Installer is only supported on Linux '
'and Mac operating systems');
}
infoLog ??= io.stdout;
// When running on LUCI, if we specify the "firefox" dependency, then the
// bot will download Firefox from CIPD and place it in a cache and set the
// environment variable FIREFOX_EXECUTABLE.
if (io.Platform.environment.containsKey(_firefoxExecutableVar)) {
infoLog.writeln('Using Firefox from $_firefoxExecutableVar variable: '
'${io.Platform.environment[_firefoxExecutableVar]}');
return BrowserInstallation(
version: 'cipd',
executable: io.Platform.environment[_firefoxExecutableVar]!,
);
}
if (requestedVersion == 'system') {
return BrowserInstallation(
version: 'system',
executable: await _findSystemFirefoxExecutable(),
);
}
FirefoxInstaller? installer;
try {
installer = requestedVersion == 'latest'
? await FirefoxInstaller.latest()
: FirefoxInstaller(version: requestedVersion);
if (installer.isInstalled) {
infoLog.writeln(
'Installation was skipped because Firefox version ${installer.version} is already installed.');
} else {
infoLog.writeln('Installing Firefox version: ${installer.version}');
await installer.install();
final BrowserInstallation installation = installer.getInstallation()!;
infoLog.writeln(
'Installations complete. To launch it run ${installation.executable}');
}
return installer.getInstallation()!;
} finally {
installer?.close();
}
}
/// Manages the installation of a particular [version] of Firefox.
class FirefoxInstaller {
factory FirefoxInstaller({
required String version,
}) {
if (version == 'system') {
throw BrowserInstallerException(
'Cannot install system version of Firefox. System Firefox must be installed manually.');
}
if (version == 'latest') {
throw BrowserInstallerException(
'Expected a concrete Firefox version, but got $version. Maybe use FirefoxInstaller.latest()?');
}
final io.Directory firefoxInstallationDir = io.Directory(
path.join(environment.webUiDartToolDir.path, 'firefox'),
);
final io.Directory versionDir = io.Directory(
path.join(firefoxInstallationDir.path, version),
);
return FirefoxInstaller._(
version: version,
firefoxInstallationDir: firefoxInstallationDir,
versionDir: versionDir,
);
}
FirefoxInstaller._({
required this.version,
required this.firefoxInstallationDir,
required this.versionDir,
});
static Future<FirefoxInstaller> latest() async {
final String latestVersion = io.Platform.isLinux
? await fetchLatestFirefoxVersionLinux()
: await fetchLatestFirefoxVersionMacOS();
return FirefoxInstaller(version: latestVersion);
}
/// Firefox version managed by this installer.
final String version;
/// HTTP client used to download Firefox.
final Client client = Client();
/// Root directory that contains Firefox versions.
final io.Directory firefoxInstallationDir;
/// Installation directory for Firefox of the requested [version].
final io.Directory versionDir;
bool get isInstalled {
return versionDir.existsSync();
}
BrowserInstallation? getInstallation() {
if (!isInstalled) {
return null;
}
return BrowserInstallation(
version: version,
executable: PlatformBinding.instance.getFirefoxExecutablePath(versionDir),
);
}
/// Install the browser by downloading from the web.
Future<void> install() async {
final io.File downloadedFile = await _download();
if (io.Platform.isLinux) {
await _uncompress(downloadedFile);
} else if (io.Platform.isMacOS) {
await _mountDmgAndCopy(downloadedFile);
}
downloadedFile.deleteSync();
}
/// Downloads the browser version from web into a target file.
/// See [version].
Future<io.File> _download() async {
if (versionDir.existsSync()) {
versionDir.deleteSync(recursive: true);
}
versionDir.createSync(recursive: true);
final String url = PlatformBinding.instance.getFirefoxDownloadUrl(version);
final StreamedResponse download = await client.send(Request(
'GET',
Uri.parse(url),
));
final io.File downloadedFile =
io.File(path.join(versionDir.path, PlatformBinding.instance.getFirefoxDownloadFilename(version)));
final io.IOSink sink = downloadedFile.openWrite();
await download.stream.pipe(sink);
await sink.flush();
await sink.close();
return downloadedFile;
}
/// 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>[
'-x',
'-f',
downloadedFile.path,
'-C',
versionDir.path,
]);
if (unzipResult.exitCode != 0) {
throw BrowserInstallerException(
'Failed to unzip the downloaded Firefox archive ${downloadedFile.path}.\n'
'The unzip process exited with code ${unzipResult.exitCode}.');
}
}
/// 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 {
final String volumeName = await _hdiUtilMount(dmgFile);
final String sourcePath = '$volumeName/Firefox.app';
final String targetPath = path.dirname(dmgFile.path);
try {
final 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 {
final 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}');
}
final List<String> processOutput = (mountResult.stdout as String).split('\n');
final 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 (final String line in lines) {
final int pos = line.indexOf('/Volumes');
if (pos != -1) {
return line.substring(pos);
}
}
return null;
}
Future<void> _hdiUtilUnmount(String volumeName) async {
final 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();
}
}
Future<String> _findSystemFirefoxExecutable() async {
final io.ProcessResult which =
await io.Process.run('which', <String>['firefox']);
final 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<String>.value(fireFoxDefaultInstallPath);
}
throw BrowserInstallerException(
'Failed to locate system Firefox installation.');
}
return which.stdout as String;
}
/// 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()));
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);
}
/// 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();
}
}