| // 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:async'; |
| import 'dart:io' as io; |
| import 'dart:typed_data'; |
| |
| import 'package:archive/archive.dart'; |
| import 'package:archive/archive_io.dart'; |
| import 'package:http/http.dart'; |
| import 'package:path/path.dart' as path; |
| |
| import 'common.dart'; |
| import 'environment.dart'; |
| import 'exceptions.dart'; |
| |
| const String _chromeExecutableVar = 'CHROME_EXECUTABLE'; |
| |
| /// Returns the installation of Chrome, 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 "browser_lock.yaml" file. |
| /// |
| /// If [requestedVersion] is not null, installs that version. The value |
| /// may be "latest" (the latest available build of Chrome), "system" |
| /// (manually installed Chrome on the current operating system), or an |
| /// exact build nuber, such as 695653. Build numbers can be found here: |
| /// |
| /// https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Linux_x64/ |
| Future<BrowserInstallation> getOrInstallChrome( |
| String requestedVersion, { |
| StringSink? infoLog, |
| }) async { |
| infoLog ??= io.stdout; |
| |
| // When running on LUCI, if we specify the "chrome_and_driver" dependency, |
| // then the bot will download Chrome from CIPD and place it in a cache and |
| // set the environment variable CHROME_EXECUTABLE. |
| if (io.Platform.environment.containsKey(_chromeExecutableVar)) { |
| infoLog.writeln('Using Chrome from $_chromeExecutableVar variable: ' |
| '${io.Platform.environment[_chromeExecutableVar]}'); |
| return BrowserInstallation( |
| version: 'cipd', |
| executable: io.Platform.environment[_chromeExecutableVar]!, |
| ); |
| } |
| |
| if (requestedVersion == 'system') { |
| return BrowserInstallation( |
| version: 'system', |
| executable: await _findSystemChromeExecutable(), |
| ); |
| } |
| |
| ChromeInstaller? installer; |
| try { |
| installer = requestedVersion == 'latest' |
| ? await ChromeInstaller.latest() |
| : ChromeInstaller(version: requestedVersion); |
| |
| if (installer.isInstalled) { |
| infoLog.writeln( |
| 'Installation was skipped because Chrome version ${installer.version} is already installed.'); |
| } else { |
| infoLog.writeln('Installing Chrome 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(); |
| } |
| } |
| |
| Future<String> _findSystemChromeExecutable() async { |
| final io.ProcessResult which = |
| await io.Process.run('which', <String>['google-chrome']); |
| |
| if (which.exitCode != 0) { |
| throw BrowserInstallerException( |
| 'Failed to locate system Chrome installation.'); |
| } |
| |
| return which.stdout as String; |
| } |
| |
| /// Manages the installation of a particular [version] of Chrome. |
| class ChromeInstaller { |
| factory ChromeInstaller({ |
| required String version, |
| }) { |
| if (version == 'system') { |
| throw BrowserInstallerException( |
| 'Cannot install system version of Chrome. System Chrome must be installed manually.'); |
| } |
| if (version == 'latest') { |
| throw BrowserInstallerException( |
| 'Expected a concrete Chromer version, but got $version. Maybe use ChromeInstaller.latest()?'); |
| } |
| final io.Directory chromeInstallationDir = io.Directory( |
| path.join(environment.webUiDartToolDir.path, 'chrome'), |
| ); |
| final io.Directory versionDir = io.Directory( |
| path.join(chromeInstallationDir.path, version), |
| ); |
| return ChromeInstaller._( |
| version: version, |
| chromeInstallationDir: chromeInstallationDir, |
| versionDir: versionDir, |
| ); |
| } |
| |
| ChromeInstaller._({ |
| required this.version, |
| required this.chromeInstallationDir, |
| required this.versionDir, |
| }); |
| |
| static Future<ChromeInstaller> latest() async { |
| final String latestVersion = await fetchLatestChromeVersion(); |
| return ChromeInstaller(version: latestVersion); |
| } |
| |
| /// Chrome version managed by this installer. |
| final String version; |
| |
| /// HTTP client used to download Chrome. |
| final Client client = Client(); |
| |
| /// Root directory that contains Chrome versions. |
| final io.Directory chromeInstallationDir; |
| |
| /// Installation directory for Chrome 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.getChromeExecutablePath(versionDir), |
| ); |
| } |
| |
| Future<void> install() async { |
| if (isLuci) { |
| throw StateError( |
| 'Rejecting attempt to install Chromium on LUCI. LUCI is expected to ' |
| 'provide Chromium as a CIPD dependency managed using .ci.yaml.', |
| ); |
| } |
| |
| if (versionDir.existsSync()) { |
| versionDir.deleteSync(recursive: true); |
| } |
| versionDir.createSync(recursive: true); |
| |
| final String url = PlatformBinding.instance.getChromeDownloadUrl(version); |
| print('Downloading Chrome from $url'); |
| |
| final StreamedResponse download = await client.send(Request( |
| 'GET', |
| Uri.parse(url), |
| )); |
| |
| final io.File downloadedFile = |
| io.File(path.join(versionDir.path, 'chrome.zip')); |
| await download.stream.pipe(downloadedFile.openWrite()); |
| |
| /// Windows LUCI bots does not have a `unzip`. Instead we are |
| /// using `archive` pub package. |
| /// |
| /// We didn't use `archive` on Mac/Linux since the new files have |
| /// permission issues. For now we are not able change file permissions |
| /// from dart. |
| /// See: https://github.com/dart-lang/sdk/issues/15078. |
| if (io.Platform.isWindows) { |
| final Stopwatch stopwatch = Stopwatch()..start(); |
| |
| // Read the Zip file from disk. |
| final Uint8List bytes = downloadedFile.readAsBytesSync(); |
| |
| final Archive archive = ZipDecoder().decodeBytes(bytes); |
| |
| // Extract the contents of the Zip archive to disk. |
| for (final ArchiveFile file in archive) { |
| final String filename = file.name; |
| if (file.isFile) { |
| final List<int> data = file.content as List<int>; |
| io.File(path.joinAll(<String>[ |
| versionDir.path, |
| // Remove the "chrome-win/" path prefix, which is the Windows |
| // convention for Chromium directory structure. |
| ...path.split(filename).skip(1), |
| ])) |
| ..createSync(recursive: true) |
| ..writeAsBytesSync(data); |
| } |
| } |
| |
| stopwatch.stop(); |
| print( |
| 'The unzip took ${stopwatch.elapsedMilliseconds ~/ 1000} seconds.' |
| ); |
| } else { |
| // We have to unzip into a temporary directory and then copy the files |
| // out because our tests expect the files to be direct children of the |
| // version directory. However, the zip file contains a top-level directory |
| // named e.g. 'chrome-linux'. We need to copy the files out of that |
| // directory and into the version directory. |
| final io.Directory tmpDir = await io.Directory.systemTemp.createTemp(); |
| final io.Directory unzipDir = tmpDir; |
| final io.ProcessResult unzipResult = |
| await io.Process.run('unzip', <String>[ |
| downloadedFile.path, |
| '-d', |
| unzipDir.path, |
| ]); |
| if (unzipResult.exitCode != 0) { |
| throw BrowserInstallerException( |
| 'Failed to unzip the downloaded Chrome archive ${downloadedFile.path}.\n' |
| 'With the version path ${versionDir.path}\n' |
| 'The unzip process exited with code ${unzipResult.exitCode}.'); |
| } |
| |
| final io.Directory topLevelDir = |
| await tmpDir.list().single as io.Directory; |
| await for (final io.FileSystemEntity entity in topLevelDir.list()) { |
| await entity |
| .rename(path.join(versionDir.path, path.basename(entity.path))); |
| } |
| await tmpDir.delete(recursive: true); |
| } |
| |
| downloadedFile.deleteSync(); |
| } |
| |
| void close() { |
| client.close(); |
| } |
| } |
| |
| /// Fetches the latest available Chrome build version. |
| Future<String> fetchLatestChromeVersion() async { |
| final Client client = Client(); |
| try { |
| final Response response = await client.get(Uri.parse( |
| 'https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2FLAST_CHANGE?alt=media')); |
| if (response.statusCode != 200) { |
| throw BrowserInstallerException( |
| 'Failed to fetch latest Chrome version. Server returned status code ${response.statusCode}'); |
| } |
| return response.body; |
| } finally { |
| client.close(); |
| } |
| } |