flutter/packages/flutter_tools/lib/src/linux/linux_doctor.dart
Robert Ancell 887d5dd9c2
Fix flutter doctor usage of eglinfo in failure cases. (#164334)
Ignore return value from eglinfo - it doesn't indicate failure.
Discovered when using an X11 system where Wayland is not available. It
returns 1 in this case, but the output is still valid. Confirmed by
looking at the eglinfo source - we should parse the output regardless of
what it returns.

Catch exception correctly when eglinfo is not installed.

Fixes for https://github.com/flutter/flutter/pull/163980
2025-02-28 13:07:55 +00:00

371 lines
14 KiB
Dart

// Copyright 2014 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 'package:process/process.dart';
import '../base/io.dart';
import '../base/user_messages.dart';
import '../base/version.dart';
import '../convert.dart';
import '../doctor_validator.dart';
/// A combination of version description and parsed version number.
class _VersionInfo {
/// Constructs a VersionInfo from a version description string.
///
/// This should contain a version number. For example:
/// "clang version 9.0.1-6+build1"
_VersionInfo(this.description) {
final String? versionString = RegExp(
r'[0-9]+\.[0-9]+(?:\.[0-9]+)?',
).firstMatch(description)?.group(0);
number = Version.parse(versionString);
}
// The full info string reported by the binary.
String description;
// The parsed Version.
Version? number;
}
/// Information about graphics drivers.
class _DriverInformation {
_DriverInformation({required ProcessManager processManager}) : _processManager = processManager;
final ProcessManager _processManager;
List<List<String>> _sections = <List<String>>[];
Future<bool> load() async {
ProcessResult? result;
try {
result = await _processManager.run(<String>['eglinfo'], stdoutEncoding: utf8);
} on ArgumentError {
// ignore error.
} on ProcessException {
return false;
}
// result.exitCode is ignored, as this is non-zero if some platforms are not avaiable.
// The output information is still parsable in all cases.
if (result == null) {
return false;
}
// Break into sections separated by an empty line.
final List<String> lines = (result.stdout as String).split('\n');
final List<List<String>> sections = <List<String>>[<String>[]];
for (final String line in lines) {
if (line == '') {
if (sections.last.isNotEmpty) {
sections.add(<String>[]);
}
} else {
sections.last.add(line);
}
}
if (sections.last.isEmpty) {
sections.removeLast();
}
_sections = sections;
return true;
}
List<String>? _getSection(String sectionName) {
for (final List<String> lines in _sections) {
if (lines[0] == '$sectionName:') {
return lines;
}
}
return null;
}
// Extracts a variable from eglinfo output.
String? getVariable(String sectionName, String name) {
final List<String>? lines = _getSection(sectionName);
if (lines == null) {
return null;
}
final String prefix = '$name:';
for (int i = 0; i < lines.length; i++) {
if (lines[i].startsWith(prefix)) {
String value = lines[i].substring(prefix.length).trim();
// Combine multi-line indented values.
if (value == '') {
for (int j = i + 1; j < lines.length && lines[j].startsWith(' '); j++) {
if (value == '') {
value += ' ';
}
value += lines[j].trim();
}
}
return value;
}
}
return null;
}
// Extracts a comma separated list variable.
List<String>? getListVariable(String sectionName, String name) {
return getVariable(sectionName, name)?.split(',').map((String s) => s.trim()).toList();
}
}
/// A validator that checks for Clang and Make build dependencies.
class LinuxDoctorValidator extends DoctorValidator {
LinuxDoctorValidator({required ProcessManager processManager, required UserMessages userMessages})
: _processManager = processManager,
_userMessages = userMessages,
super('Linux toolchain - develop for Linux desktop');
final ProcessManager _processManager;
final UserMessages _userMessages;
static const String kClangBinary = 'clang++';
static const String kCmakeBinary = 'cmake';
static const String kNinjaBinary = 'ninja';
static const String kPkgConfigBinary = 'pkg-config';
final Map<String, Version> _requiredBinaryVersions = <String, Version>{
kClangBinary: Version(3, 4, 0),
kCmakeBinary: Version(3, 10, 0),
kNinjaBinary: Version(1, 8, 0),
kPkgConfigBinary: Version(0, 29, 0),
};
final List<String> _requiredGtkLibraries = <String>['gtk+-3.0', 'glib-2.0', 'gio-2.0'];
@override
Future<ValidationResult> validateImpl() async {
ValidationType validationType = ValidationType.success;
final List<ValidationMessage> messages = <ValidationMessage>[];
final Map<String, _VersionInfo?> installedVersions = <String, _VersionInfo?>{
// Sort the check to make the call order predictable for unit tests.
for (final String binary in _requiredBinaryVersions.keys.toList()..sort())
binary: await _getBinaryVersion(binary),
};
// Determine overall validation level.
if (installedVersions.values.any((_VersionInfo? versionInfo) => versionInfo?.number == null)) {
validationType = ValidationType.missing;
} else if (installedVersions.keys.any(
(String binary) => installedVersions[binary]!.number! < _requiredBinaryVersions[binary]!,
)) {
validationType = ValidationType.partial;
}
// Message for Clang.
{
final _VersionInfo? version = installedVersions[kClangBinary];
if (version == null || version.number == null) {
messages.add(ValidationMessage.error(_userMessages.clangMissing));
} else {
assert(_requiredBinaryVersions.containsKey(kClangBinary));
messages.add(ValidationMessage(version.description));
final Version requiredVersion = _requiredBinaryVersions[kClangBinary]!;
if (version.number! < requiredVersion) {
messages.add(
ValidationMessage.error(_userMessages.clangTooOld(requiredVersion.toString())),
);
}
}
}
// Message for CMake.
{
final _VersionInfo? version = installedVersions[kCmakeBinary];
if (version == null || version.number == null) {
messages.add(ValidationMessage.error(_userMessages.cmakeMissing));
} else {
assert(_requiredBinaryVersions.containsKey(kCmakeBinary));
messages.add(ValidationMessage(version.description));
final Version requiredVersion = _requiredBinaryVersions[kCmakeBinary]!;
if (version.number! < requiredVersion) {
messages.add(
ValidationMessage.error(_userMessages.cmakeTooOld(requiredVersion.toString())),
);
}
}
}
// Message for ninja.
{
final _VersionInfo? version = installedVersions[kNinjaBinary];
if (version == null || version.number == null) {
messages.add(ValidationMessage.error(_userMessages.ninjaMissing));
} else {
assert(_requiredBinaryVersions.containsKey(kNinjaBinary));
// The full version description is just the number, so add context.
messages.add(ValidationMessage(_userMessages.ninjaVersion(version.description)));
final Version requiredVersion = _requiredBinaryVersions[kNinjaBinary]!;
if (version.number! < requiredVersion) {
messages.add(
ValidationMessage.error(_userMessages.ninjaTooOld(requiredVersion.toString())),
);
}
}
}
// Message for pkg-config.
{
final _VersionInfo? version = installedVersions[kPkgConfigBinary];
if (version == null || version.number == null) {
messages.add(ValidationMessage.error(_userMessages.pkgConfigMissing));
// Exit early because we cannot validate libraries without pkg-config.
return ValidationResult(validationType, messages);
} else {
assert(_requiredBinaryVersions.containsKey(kPkgConfigBinary));
// The full version description is just the number, so add context.
messages.add(ValidationMessage(_userMessages.pkgConfigVersion(version.description)));
final Version requiredVersion = _requiredBinaryVersions[kPkgConfigBinary]!;
if (version.number! < requiredVersion) {
messages.add(
ValidationMessage.error(_userMessages.pkgConfigTooOld(requiredVersion.toString())),
);
}
}
}
// Messages for libraries.
{
bool libraryMissing = false;
for (final String library in _requiredGtkLibraries) {
if (!await _libraryIsPresent(library)) {
libraryMissing = true;
break;
}
}
if (libraryMissing) {
validationType = ValidationType.missing;
messages.add(ValidationMessage.error(_userMessages.gtkLibrariesMissing));
}
}
// Messages for drivers.
{
final _DriverInformation driverInfo = _DriverInformation(processManager: _processManager);
if (!await driverInfo.load()) {
messages.add(ValidationMessage.hint(_userMessages.eglinfoMissing));
} else {
const String kWaylandPlatform = 'Wayland platform';
const String kX11Platform = 'X11 platform';
const String kOpenGLCoreProfileRenderer = 'OpenGL core profile renderer';
const String kOpenGLCoreProfileShadingLanguageVersion =
'OpenGL core profile shading language version';
const String kOpenGLCoreProfileVersion = 'OpenGL core profile version';
const String kOpenGLCoreProfileExtensions = 'OpenGL core profile extensions';
const String kOpenGLESProfileRenderer = 'OpenGL ES profile renderer';
const String kOpenGLESProfileVersion = 'OpenGL ES profile version';
const String kOpenGLESProfileShadingLanguageVersion =
'OpenGL ES profile shading language version';
const String kOpenGLESProfileExtensions = 'OpenGL ES profile extensions';
// Check both Wayland and X11 platforms for value.
String? getPlatformVariable(String name) {
final String? waylandValue = driverInfo.getVariable(kWaylandPlatform, name);
final String? x11Value = driverInfo.getVariable(kX11Platform, name);
if (waylandValue == null && x11Value == null) {
return null;
}
if (waylandValue == null) {
return '$x11Value (X11)';
} else if (x11Value == null) {
return '$waylandValue (Wayland)';
} else if (waylandValue == x11Value) {
return waylandValue;
} else {
return '$waylandValue (Wayland) $x11Value (X11)';
}
}
// Check if has specified OpenGL extension.
ValidationMessage extensionStatus(String variableName, String extensionName) {
final List<String> waylandExtensions =
driverInfo.getListVariable(kWaylandPlatform, variableName) ?? <String>[];
final List<String> x11Extensions =
driverInfo.getListVariable(kX11Platform, variableName) ?? <String>[];
final bool hasWayland = waylandExtensions.contains(extensionName);
final bool hasX11 = x11Extensions.contains(extensionName);
String status;
if (!hasWayland && !hasX11) {
status = 'no';
} else if (!hasWayland) {
status = 'yes (X11)';
} else if (!hasX11) {
status = 'yes (Wayland)';
} else {
status = 'yes';
}
return ValidationMessage('$extensionName: $status');
}
final String? renderer = getPlatformVariable(kOpenGLCoreProfileRenderer);
if (renderer != null) {
messages.add(ValidationMessage('OpenGL core renderer: $renderer'));
final String version = getPlatformVariable(kOpenGLCoreProfileVersion) ?? 'unknown';
messages.add(ValidationMessage('OpenGL core version: $version'));
final String shadingLanguageVersion =
getPlatformVariable(kOpenGLCoreProfileShadingLanguageVersion) ?? 'unknown';
messages.add(
ValidationMessage('OpenGL core shading language version: $shadingLanguageVersion'),
);
}
final String? esRenderer = getPlatformVariable(kOpenGLESProfileRenderer);
if (esRenderer != null) {
messages.add(ValidationMessage('OpenGL ES renderer: $esRenderer'));
final String version = getPlatformVariable(kOpenGLESProfileVersion) ?? 'unknown';
messages.add(ValidationMessage('OpenGL ES version: $version'));
final String shadingLanguageVersion =
getPlatformVariable(kOpenGLESProfileShadingLanguageVersion) ?? 'unknown';
messages.add(
ValidationMessage('OpenGL ES shading language version: $shadingLanguageVersion'),
);
}
messages.add(extensionStatus(kOpenGLCoreProfileExtensions, 'GL_EXT_framebuffer_blit'));
messages.add(extensionStatus(kOpenGLESProfileExtensions, 'GL_EXT_texture_format_BGRA8888'));
}
}
return ValidationResult(validationType, messages);
}
/// Returns the installed version of [binary], or null if it's not installed.
///
/// Requires tha [binary] take a '--version' flag, and print a version of the
/// form x.y.z somewhere on the first line of output.
Future<_VersionInfo?> _getBinaryVersion(String binary) async {
ProcessResult? result;
try {
result = await _processManager.run(<String>[binary, '--version']);
} on ArgumentError {
// ignore error.
} on ProcessException {
// ignore error.
}
if (result == null || result.exitCode != 0) {
return null;
}
final String firstLine = (result.stdout as String).split('\n').first.trim();
return _VersionInfo(firstLine);
}
/// Checks that [library] is available via pkg-config.
Future<bool> _libraryIsPresent(String library) async {
ProcessResult? result;
try {
result = await _processManager.run(<String>['pkg-config', '--exists', library]);
} on ArgumentError {
// ignore error.
}
return (result?.exitCode ?? 1) == 0;
}
}