mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Build hooks: Don't require toolchain for unit tests (#178954)
To ensure build hooks emit code assets that are compatible with the main app, Flutter tools pass a `CCompilerConfig` toolchain configuration object to hooks. This is generally what we want, hooks on macOS should use the same `clang` from XCode as the one used to compile the app and native Flutter plugins for instance. In some cases however, we need to run hooks without necessarily compiling a full Flutter app with native sources. A good example for this is `flutter test`, which runs unit / widget tests in a regular Dart VM without embedding it in a Flutter application. So since `flutter test` wouldn't invoke a native compiler, running build hooks shouldn't fail if the expected toolchain is missing. Currently however, `flutter test` tries to resolve a compiler toolchain for the host platform. Doing that on Windows already allows not passing a `CCompilerConfig` if VSCode wasn't found, but on macOS and Linux, this crashes. This fixes the issue by allowing those methods to return `null` instead of throwing. They still throw by default, but for the test target they are configured to not pass a toolchain to hooks if none could be resolved. This means that hooks not invoking the provided toolchain (say because they're only downloading native artifacts instead) would now work, whereas previously `flutter test` would crash if no toolchian was found. This closes https://github.com/flutter/flutter/issues/178715 (but only the part shared in the original issue description, @dcharkes suggested fixing a similar issue in the same PR but that is _not_ done here).
This commit is contained in:
parent
c70e82554a
commit
43d438f2ac
@ -9,10 +9,16 @@ import '../../../base/file_system.dart';
|
||||
import '../../../base/io.dart';
|
||||
import '../../../globals.dart' as globals;
|
||||
|
||||
/// Flutter expects `clang++` to be on the path on Linux hosts.
|
||||
/// Returns a [CCompilerConfig] matching a toolchain that would be used to compile the main app with
|
||||
/// CMake on Linux.
|
||||
///
|
||||
/// Search for the accompanying `clang`, `ar`, and `ld`.
|
||||
Future<CCompilerConfig> cCompilerConfigLinux() async {
|
||||
/// Flutter expects `clang++` to be on the path on Linux hosts, which this uses to search for the
|
||||
/// accompanying `clang`, `ar`, and `ld`.
|
||||
///
|
||||
/// If [throwIfNotFound] is false, this is allowed to fail (in which case `null`) is returned. This
|
||||
/// is used for `flutter test` setups, where no main app is compiled and we thus don't want a
|
||||
/// `clang` toolchain to be a requirement.
|
||||
Future<CCompilerConfig?> cCompilerConfigLinux({required bool throwIfNotFound}) async {
|
||||
const kClangPlusPlusBinary = 'clang++';
|
||||
// NOTE: these binaries sometimes have different names depending on the installation;
|
||||
// thus, we check for a few possible options (in order of preference).
|
||||
@ -25,30 +31,53 @@ Future<CCompilerConfig> cCompilerConfigLinux() async {
|
||||
kClangPlusPlusBinary,
|
||||
]);
|
||||
if (whichResult.exitCode != 0) {
|
||||
throwToolExit('Failed to find $kClangPlusPlusBinary on PATH.');
|
||||
if (throwIfNotFound) {
|
||||
throwToolExit('Failed to find $kClangPlusPlusBinary on PATH.');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
File clangPpFile = globals.fs.file((whichResult.stdout as String).trim());
|
||||
clangPpFile = globals.fs.file(await clangPpFile.resolveSymbolicLinks());
|
||||
|
||||
final Directory clangDir = clangPpFile.parent;
|
||||
return CCompilerConfig(
|
||||
linker: _findExecutableIfExists(path: clangDir, possibleExecutableNames: kLdBinaryOptions),
|
||||
compiler: _findExecutableIfExists(path: clangDir, possibleExecutableNames: kClangBinaryOptions),
|
||||
archiver: _findExecutableIfExists(path: clangDir, possibleExecutableNames: kArBinaryOptions),
|
||||
Uri? findExecutable({required List<String> possibleExecutableNames, required Directory path}) {
|
||||
final Uri? found = _findExecutableIfExists(
|
||||
possibleExecutableNames: possibleExecutableNames,
|
||||
path: path,
|
||||
);
|
||||
|
||||
if (found == null && throwIfNotFound) {
|
||||
throwToolExit('Failed to find any of $possibleExecutableNames in $path');
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
final Uri? linker = findExecutable(path: clangDir, possibleExecutableNames: kLdBinaryOptions);
|
||||
final Uri? compiler = findExecutable(
|
||||
path: clangDir,
|
||||
possibleExecutableNames: kClangBinaryOptions,
|
||||
);
|
||||
final Uri? archiver = findExecutable(path: clangDir, possibleExecutableNames: kArBinaryOptions);
|
||||
|
||||
if (linker == null || compiler == null || archiver == null) {
|
||||
assert(!throwIfNotFound); // otherwise, findExecutable would have thrown
|
||||
return null;
|
||||
}
|
||||
return CCompilerConfig(linker: linker, compiler: compiler, archiver: archiver);
|
||||
}
|
||||
|
||||
/// Searches for an executable with a name in [possibleExecutableNames]
|
||||
/// at [path] and returns the first one it finds, if one is found.
|
||||
/// Otherwise, throws an error.
|
||||
Uri _findExecutableIfExists({
|
||||
/// Otherwise, returns `null`.
|
||||
Uri? _findExecutableIfExists({
|
||||
required List<String> possibleExecutableNames,
|
||||
required Directory path,
|
||||
}) {
|
||||
return possibleExecutableNames
|
||||
.map((execName) => path.childFile(execName))
|
||||
.where((file) => file.existsSync())
|
||||
.map((file) => file.uri)
|
||||
.firstOrNull ??
|
||||
throwToolExit('Failed to find any of $possibleExecutableNames in $path');
|
||||
.map((execName) => path.childFile(execName))
|
||||
.where((file) => file.existsSync())
|
||||
.map((file) => file.uri)
|
||||
.firstOrNull;
|
||||
}
|
||||
|
||||
@ -168,23 +168,35 @@ Future<void> codesignDylib(
|
||||
/// Flutter expects `xcrun` to be on the path on macOS hosts.
|
||||
///
|
||||
/// Use the `clang`, `ar`, and `ld` that would be used if run with `xcrun`.
|
||||
Future<CCompilerConfig> cCompilerConfigMacOS() async {
|
||||
return CCompilerConfig(
|
||||
compiler: await _findXcrunBinary('clang'),
|
||||
archiver: await _findXcrunBinary('ar'),
|
||||
linker: await _findXcrunBinary('ld'),
|
||||
);
|
||||
///
|
||||
/// If no XCode installation was found, [throwIfNotFound] controls whether this
|
||||
/// throws or returns `null`.
|
||||
Future<CCompilerConfig?> cCompilerConfigMacOS({required bool throwIfNotFound}) async {
|
||||
final Uri? compiler = await _findXcrunBinary('clang', throwIfNotFound);
|
||||
final Uri? archiver = await _findXcrunBinary('ar', throwIfNotFound);
|
||||
final Uri? linker = await _findXcrunBinary('ld', throwIfNotFound);
|
||||
|
||||
if (compiler == null || archiver == null || linker == null) {
|
||||
assert(!throwIfNotFound);
|
||||
return null;
|
||||
}
|
||||
|
||||
return CCompilerConfig(compiler: compiler, archiver: archiver, linker: linker);
|
||||
}
|
||||
|
||||
/// Invokes `xcrun --find` to find the full path to [binaryName].
|
||||
Future<Uri> _findXcrunBinary(String binaryName) async {
|
||||
Future<Uri?> _findXcrunBinary(String binaryName, bool throwIfNotFound) async {
|
||||
final ProcessResult xcrunResult = await globals.processManager.run(<String>[
|
||||
'xcrun',
|
||||
'--find',
|
||||
binaryName,
|
||||
]);
|
||||
if (xcrunResult.exitCode != 0) {
|
||||
throwToolExit('Failed to find $binaryName with xcrun:\n${xcrunResult.stderr}');
|
||||
if (throwIfNotFound) {
|
||||
throwToolExit('Failed to find $binaryName with xcrun:\n${xcrunResult.stderr}');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return Uri.file((xcrunResult.stdout as String).trim());
|
||||
}
|
||||
|
||||
@ -194,7 +194,19 @@ sealed class CodeAssetTarget extends AssetBuildTarget {
|
||||
|
||||
late final CCompilerConfig? cCompilerConfigSync;
|
||||
|
||||
Future<void> setCCompilerConfig();
|
||||
/// On platforms where the Flutter app is compiled with a native toolchain, configures this target
|
||||
/// to contain a [CCompilerConfig] matching that toolchain.
|
||||
///
|
||||
/// While hooks are supposed to be able to find a toolchain on their own, we want them to use the
|
||||
/// same tools used to build the main app to make static linking easier in the future. So if we're
|
||||
/// e.g. on Linux and use `clang` to compile the app, hooks should use the same `clang` as a
|
||||
/// compiler too.
|
||||
///
|
||||
/// If [mustMatchAppBuild] is true (the default), this should throw if the expected toolchain
|
||||
/// could not be found. For `flutter test` setups where no app is compiled, we _prefer_ to use the
|
||||
/// same toolchain but would allow not passing a [CCompilerConfig] if that fails. This allows
|
||||
/// hooks that only download code assets instead of compiling them to still function.
|
||||
Future<void> setCCompilerConfig({bool mustMatchAppBuild = true});
|
||||
|
||||
List<CodeAssetExtension> get codeAssetExtensions {
|
||||
return <CodeAssetExtension>[
|
||||
@ -223,7 +235,9 @@ class WindowsAssetTarget extends CodeAssetTarget {
|
||||
];
|
||||
|
||||
@override
|
||||
Future<void> setCCompilerConfig() async => cCompilerConfigSync = await cCompilerConfigWindows();
|
||||
Future<void> setCCompilerConfig({bool mustMatchAppBuild = true}) async =>
|
||||
// TODO(simolus3): Respect the mustMatchAppBuild option in cCompilerConfigWindows.
|
||||
cCompilerConfigSync = await cCompilerConfigWindows();
|
||||
}
|
||||
|
||||
final class LinuxAssetTarget extends CodeAssetTarget {
|
||||
@ -231,7 +245,8 @@ final class LinuxAssetTarget extends CodeAssetTarget {
|
||||
: super(os: OS.linux);
|
||||
|
||||
@override
|
||||
Future<void> setCCompilerConfig() async => cCompilerConfigSync = await cCompilerConfigLinux();
|
||||
Future<void> setCCompilerConfig({bool mustMatchAppBuild = true}) async =>
|
||||
cCompilerConfigSync = await cCompilerConfigLinux(throwIfNotFound: mustMatchAppBuild);
|
||||
|
||||
@override
|
||||
List<ProtocolExtension> get extensions => <ProtocolExtension>[
|
||||
@ -252,7 +267,8 @@ final class IOSAssetTarget extends CodeAssetTarget {
|
||||
final FileSystem fileSystem;
|
||||
|
||||
@override
|
||||
Future<void> setCCompilerConfig() async => cCompilerConfigSync = await cCompilerConfigMacOS();
|
||||
Future<void> setCCompilerConfig({bool mustMatchAppBuild = true}) async =>
|
||||
cCompilerConfigSync = await cCompilerConfigMacOS(throwIfNotFound: mustMatchAppBuild);
|
||||
|
||||
IOSCodeConfig _getIOSConfig(Map<String, String> environmentDefines, FileSystem fileSystem) {
|
||||
final String? sdkRoot = environmentDefines[kSdkRoot];
|
||||
@ -299,7 +315,8 @@ final class MacOSAssetTarget extends CodeAssetTarget {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setCCompilerConfig() async => cCompilerConfigSync = await cCompilerConfigMacOS();
|
||||
Future<void> setCCompilerConfig({bool mustMatchAppBuild = true}) async =>
|
||||
cCompilerConfigSync = await cCompilerConfigMacOS(throwIfNotFound: mustMatchAppBuild);
|
||||
}
|
||||
|
||||
final class AndroidAssetTarget extends CodeAssetTarget {
|
||||
@ -315,7 +332,8 @@ final class AndroidAssetTarget extends CodeAssetTarget {
|
||||
final AndroidCodeConfig? _androidCodeConfig;
|
||||
|
||||
@override
|
||||
Future<void> setCCompilerConfig() async => cCompilerConfigSync = await cCompilerConfigAndroid();
|
||||
Future<void> setCCompilerConfig({bool mustMatchAppBuild = true}) async =>
|
||||
cCompilerConfigSync = await cCompilerConfigAndroid();
|
||||
|
||||
@override
|
||||
List<ProtocolExtension> get extensions => <ProtocolExtension>[
|
||||
@ -362,7 +380,8 @@ final class FlutterTesterAssetTarget extends CodeAssetTarget {
|
||||
CCompilerConfig? get cCompilerConfigSync => subtarget.cCompilerConfigSync;
|
||||
|
||||
@override
|
||||
Future<void> setCCompilerConfig() async => subtarget.setCCompilerConfig();
|
||||
Future<void> setCCompilerConfig({bool mustMatchAppBuild = true}) =>
|
||||
subtarget.setCCompilerConfig(mustMatchAppBuild: false);
|
||||
}
|
||||
|
||||
List<AndroidArch> _androidArchs(TargetPlatform targetPlatform, String? androidArchsEnvironment) {
|
||||
|
||||
@ -93,7 +93,7 @@ void main() {
|
||||
await fileSystem.file('/some/path/to/llvm-ar').create();
|
||||
await fileSystem.file('/some/path/to/ld.lld').create();
|
||||
|
||||
final CCompilerConfig result = await cCompilerConfigLinux();
|
||||
final CCompilerConfig result = (await cCompilerConfigLinux(throwIfNotFound: true))!;
|
||||
expect(result.compiler, Uri.file('/some/path/to/clang'));
|
||||
},
|
||||
);
|
||||
@ -115,7 +115,7 @@ void main() {
|
||||
await fileSystem.file('/path/to/$execName').create(recursive: true);
|
||||
}
|
||||
|
||||
final CCompilerConfig result = await cCompilerConfigLinux();
|
||||
final CCompilerConfig result = (await cCompilerConfigLinux(throwIfNotFound: true))!;
|
||||
expect(result.linker, Uri.file('/path/to/ld'));
|
||||
expect(result.compiler, Uri.file('/path/to/clang'));
|
||||
expect(result.archiver, Uri.file('/path/to/ar'));
|
||||
@ -123,7 +123,7 @@ void main() {
|
||||
);
|
||||
|
||||
testUsingContext(
|
||||
'cCompilerConfigLinux with missing binaries',
|
||||
'cCompilerConfigLinux with missing binaries when required',
|
||||
overrides: <Type, Generator>{
|
||||
ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
|
||||
const FakeCommand(command: <Pattern>['which', 'clang++'], stdout: '/a/path/to/clang++'),
|
||||
@ -137,7 +137,25 @@ void main() {
|
||||
|
||||
await fileSystem.file('/a/path/to/clang++').create(recursive: true);
|
||||
|
||||
expect(cCompilerConfigLinux(), throwsA(isA<ToolExit>()));
|
||||
expect(cCompilerConfigLinux(throwIfNotFound: true), throwsA(isA<ToolExit>()));
|
||||
},
|
||||
);
|
||||
|
||||
testUsingContext(
|
||||
'cCompilerConfigLinux with missing binaries when not required',
|
||||
overrides: <Type, Generator>{
|
||||
ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
|
||||
const FakeCommand(command: <Pattern>['which', 'clang++'], stdout: '/a/path/to/clang++'),
|
||||
]),
|
||||
FileSystem: () => fileSystem,
|
||||
},
|
||||
() async {
|
||||
if (!const LocalPlatform().isLinux) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fileSystem.file('/a/path/to/clang++').create(recursive: true);
|
||||
expect(cCompilerConfigLinux(throwIfNotFound: false), completes);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import 'package:code_assets/code_assets.dart';
|
||||
import 'package:file/file.dart';
|
||||
import 'package:file/memory.dart';
|
||||
import 'package:flutter_tools/src/artifacts.dart';
|
||||
import 'package:flutter_tools/src/base/common.dart';
|
||||
import 'package:flutter_tools/src/base/file_system.dart';
|
||||
import 'package:flutter_tools/src/base/logger.dart';
|
||||
import 'package:flutter_tools/src/base/platform.dart';
|
||||
@ -391,7 +392,7 @@ void main() {
|
||||
return;
|
||||
}
|
||||
|
||||
final CCompilerConfig result = await cCompilerConfigMacOS();
|
||||
final CCompilerConfig result = (await cCompilerConfigMacOS(throwIfNotFound: true))!;
|
||||
expect(
|
||||
result.compiler,
|
||||
Uri.file(
|
||||
@ -429,10 +430,52 @@ void main() {
|
||||
return;
|
||||
}
|
||||
|
||||
final CCompilerConfig result = await cCompilerConfigMacOS();
|
||||
final CCompilerConfig result = (await cCompilerConfigMacOS(throwIfNotFound: true))!;
|
||||
expect(result.compiler, Uri.file('/nix/store/random-path-to-clang-wrapper/bin/clang'));
|
||||
expect(result.archiver, Uri.file('/nix/store/random-path-to-clang-wrapper/bin/ar'));
|
||||
expect(result.linker, Uri.file('/nix/store/random-path-to-clang-wrapper/bin/ld'));
|
||||
},
|
||||
);
|
||||
|
||||
testUsingContext(
|
||||
'missing xcode when required',
|
||||
overrides: <Type, Generator>{
|
||||
ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
|
||||
for (final binary in <String>['clang', 'ar', 'ld'])
|
||||
FakeCommand(
|
||||
command: <Pattern>['xcrun', '--find', binary],
|
||||
exitCode: 1,
|
||||
stderr: 'not found',
|
||||
),
|
||||
]),
|
||||
},
|
||||
() async {
|
||||
if (!const LocalPlatform().isMacOS) {
|
||||
return;
|
||||
}
|
||||
|
||||
await expectLater(cCompilerConfigMacOS(throwIfNotFound: true), throwsA(isA<ToolExit>()));
|
||||
},
|
||||
);
|
||||
|
||||
testUsingContext(
|
||||
'missing xcode when not required',
|
||||
overrides: <Type, Generator>{
|
||||
ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
|
||||
for (final binary in <String>['clang', 'ar', 'ld'])
|
||||
FakeCommand(
|
||||
command: <Pattern>['xcrun', '--find', binary],
|
||||
exitCode: 1,
|
||||
stderr: 'not found',
|
||||
),
|
||||
]),
|
||||
},
|
||||
() async {
|
||||
if (!const LocalPlatform().isMacOS) {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(await cCompilerConfigMacOS(throwIfNotFound: false), isNull);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -9,12 +9,14 @@ import 'package:file_testing/file_testing.dart';
|
||||
import 'package:flutter_tools/src/artifacts.dart';
|
||||
import 'package:flutter_tools/src/base/file_system.dart';
|
||||
import 'package:flutter_tools/src/base/logger.dart';
|
||||
import 'package:flutter_tools/src/base/platform.dart';
|
||||
import 'package:flutter_tools/src/build_info.dart';
|
||||
import 'package:flutter_tools/src/build_system/build_system.dart';
|
||||
import 'package:flutter_tools/src/build_system/targets/native_assets.dart';
|
||||
import 'package:flutter_tools/src/features.dart';
|
||||
import 'package:flutter_tools/src/isolated/native_assets/dart_hook_result.dart';
|
||||
import 'package:flutter_tools/src/isolated/native_assets/native_assets.dart';
|
||||
import 'package:flutter_tools/src/isolated/native_assets/targets.dart';
|
||||
|
||||
import '../../src/common.dart';
|
||||
import '../../src/context.dart';
|
||||
@ -234,4 +236,63 @@ void main() {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testUsingContext(
|
||||
'unit tests does not require compiler toolchain',
|
||||
overrides: <Type, Generator>{
|
||||
ProcessManager: () {
|
||||
const Platform platform = LocalPlatform();
|
||||
return FakeProcessManager.list([
|
||||
if (platform.isMacOS)
|
||||
for (final binary in <String>['clang', 'ar', 'ld'])
|
||||
FakeCommand(
|
||||
command: <Pattern>['xcrun', '--find', binary],
|
||||
exitCode: 1,
|
||||
stderr: 'not found',
|
||||
),
|
||||
if (platform.isLinux)
|
||||
const FakeCommand(
|
||||
command: <Pattern>['which', 'clang++'],
|
||||
exitCode: 1,
|
||||
stderr: 'not found',
|
||||
),
|
||||
]);
|
||||
},
|
||||
},
|
||||
() async {
|
||||
// This calls setCCompilerConfig() on a test target, which must not throw despite the
|
||||
// toolchain not being available.
|
||||
const Platform platform = LocalPlatform();
|
||||
if (!platform.isLinux && !platform.isMacOS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final target = _SetCCompilerConfigTarget(
|
||||
packagesWithNativeAssetsResult: <String>['bar'],
|
||||
buildResult: FakeFlutterNativeAssetsBuilderResult.fromAssets(),
|
||||
);
|
||||
|
||||
await runFlutterSpecificHooks(
|
||||
environmentDefines: {},
|
||||
targetPlatform: TargetPlatform.tester,
|
||||
projectUri: projectUri,
|
||||
fileSystem: fileSystem,
|
||||
buildRunner: target,
|
||||
);
|
||||
|
||||
expect(target.didSetCCompilerConfig, isTrue);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class _SetCCompilerConfigTarget extends FakeFlutterNativeAssetsBuildRunner {
|
||||
_SetCCompilerConfigTarget({super.buildResult, super.packagesWithNativeAssetsResult});
|
||||
|
||||
var didSetCCompilerConfig = false;
|
||||
|
||||
@override
|
||||
Future<void> setCCompilerConfig(CodeAssetTarget target) async {
|
||||
await target.setCCompilerConfig();
|
||||
didSetCCompilerConfig = true;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user