diff --git a/packages/flutter_tools/lib/src/isolated/native_assets/linux/native_assets.dart b/packages/flutter_tools/lib/src/isolated/native_assets/linux/native_assets.dart index 07a5df00f02..4e1b82dd255 100644 --- a/packages/flutter_tools/lib/src/isolated/native_assets/linux/native_assets.dart +++ b/packages/flutter_tools/lib/src/isolated/native_assets/linux/native_assets.dart @@ -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 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 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 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 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 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; } diff --git a/packages/flutter_tools/lib/src/isolated/native_assets/macos/native_assets_host.dart b/packages/flutter_tools/lib/src/isolated/native_assets/macos/native_assets_host.dart index 8604e57d366..71d4729e464 100644 --- a/packages/flutter_tools/lib/src/isolated/native_assets/macos/native_assets_host.dart +++ b/packages/flutter_tools/lib/src/isolated/native_assets/macos/native_assets_host.dart @@ -168,23 +168,35 @@ Future 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 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 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 _findXcrunBinary(String binaryName) async { +Future _findXcrunBinary(String binaryName, bool throwIfNotFound) async { final ProcessResult xcrunResult = await globals.processManager.run([ '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()); } diff --git a/packages/flutter_tools/lib/src/isolated/native_assets/targets.dart b/packages/flutter_tools/lib/src/isolated/native_assets/targets.dart index b6dd0d42fd9..cc0de9f2452 100644 --- a/packages/flutter_tools/lib/src/isolated/native_assets/targets.dart +++ b/packages/flutter_tools/lib/src/isolated/native_assets/targets.dart @@ -194,7 +194,19 @@ sealed class CodeAssetTarget extends AssetBuildTarget { late final CCompilerConfig? cCompilerConfigSync; - Future 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 setCCompilerConfig({bool mustMatchAppBuild = true}); List get codeAssetExtensions { return [ @@ -223,7 +235,9 @@ class WindowsAssetTarget extends CodeAssetTarget { ]; @override - Future setCCompilerConfig() async => cCompilerConfigSync = await cCompilerConfigWindows(); + Future 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 setCCompilerConfig() async => cCompilerConfigSync = await cCompilerConfigLinux(); + Future setCCompilerConfig({bool mustMatchAppBuild = true}) async => + cCompilerConfigSync = await cCompilerConfigLinux(throwIfNotFound: mustMatchAppBuild); @override List get extensions => [ @@ -252,7 +267,8 @@ final class IOSAssetTarget extends CodeAssetTarget { final FileSystem fileSystem; @override - Future setCCompilerConfig() async => cCompilerConfigSync = await cCompilerConfigMacOS(); + Future setCCompilerConfig({bool mustMatchAppBuild = true}) async => + cCompilerConfigSync = await cCompilerConfigMacOS(throwIfNotFound: mustMatchAppBuild); IOSCodeConfig _getIOSConfig(Map environmentDefines, FileSystem fileSystem) { final String? sdkRoot = environmentDefines[kSdkRoot]; @@ -299,7 +315,8 @@ final class MacOSAssetTarget extends CodeAssetTarget { } @override - Future setCCompilerConfig() async => cCompilerConfigSync = await cCompilerConfigMacOS(); + Future 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 setCCompilerConfig() async => cCompilerConfigSync = await cCompilerConfigAndroid(); + Future setCCompilerConfig({bool mustMatchAppBuild = true}) async => + cCompilerConfigSync = await cCompilerConfigAndroid(); @override List get extensions => [ @@ -362,7 +380,8 @@ final class FlutterTesterAssetTarget extends CodeAssetTarget { CCompilerConfig? get cCompilerConfigSync => subtarget.cCompilerConfigSync; @override - Future setCCompilerConfig() async => subtarget.setCCompilerConfig(); + Future setCCompilerConfig({bool mustMatchAppBuild = true}) => + subtarget.setCCompilerConfig(mustMatchAppBuild: false); } List _androidArchs(TargetPlatform targetPlatform, String? androidArchsEnvironment) { diff --git a/packages/flutter_tools/test/general.shard/isolated/linux/native_assets_test.dart b/packages/flutter_tools/test/general.shard/isolated/linux/native_assets_test.dart index 236b90749cc..23cdffc7762 100644 --- a/packages/flutter_tools/test/general.shard/isolated/linux/native_assets_test.dart +++ b/packages/flutter_tools/test/general.shard/isolated/linux/native_assets_test.dart @@ -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: { ProcessManager: () => FakeProcessManager.list([ const FakeCommand(command: ['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())); + expect(cCompilerConfigLinux(throwIfNotFound: true), throwsA(isA())); + }, + ); + + testUsingContext( + 'cCompilerConfigLinux with missing binaries when not required', + overrides: { + ProcessManager: () => FakeProcessManager.list([ + const FakeCommand(command: ['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); }, ); } diff --git a/packages/flutter_tools/test/general.shard/isolated/macos/native_assets_test.dart b/packages/flutter_tools/test/general.shard/isolated/macos/native_assets_test.dart index 86edf15cbca..20d28437b63 100644 --- a/packages/flutter_tools/test/general.shard/isolated/macos/native_assets_test.dart +++ b/packages/flutter_tools/test/general.shard/isolated/macos/native_assets_test.dart @@ -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: { + ProcessManager: () => FakeProcessManager.list([ + for (final binary in ['clang', 'ar', 'ld']) + FakeCommand( + command: ['xcrun', '--find', binary], + exitCode: 1, + stderr: 'not found', + ), + ]), + }, + () async { + if (!const LocalPlatform().isMacOS) { + return; + } + + await expectLater(cCompilerConfigMacOS(throwIfNotFound: true), throwsA(isA())); + }, + ); + + testUsingContext( + 'missing xcode when not required', + overrides: { + ProcessManager: () => FakeProcessManager.list([ + for (final binary in ['clang', 'ar', 'ld']) + FakeCommand( + command: ['xcrun', '--find', binary], + exitCode: 1, + stderr: 'not found', + ), + ]), + }, + () async { + if (!const LocalPlatform().isMacOS) { + return; + } + + expect(await cCompilerConfigMacOS(throwIfNotFound: false), isNull); + }, + ); } diff --git a/packages/flutter_tools/test/general.shard/isolated/native_assets_test.dart b/packages/flutter_tools/test/general.shard/isolated/native_assets_test.dart index 745573bc80f..01102949cd0 100644 --- a/packages/flutter_tools/test/general.shard/isolated/native_assets_test.dart +++ b/packages/flutter_tools/test/general.shard/isolated/native_assets_test.dart @@ -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: { + ProcessManager: () { + const Platform platform = LocalPlatform(); + return FakeProcessManager.list([ + if (platform.isMacOS) + for (final binary in ['clang', 'ar', 'ld']) + FakeCommand( + command: ['xcrun', '--find', binary], + exitCode: 1, + stderr: 'not found', + ), + if (platform.isLinux) + const FakeCommand( + command: ['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: ['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 setCCompilerConfig(CodeAssetTarget target) async { + await target.setCCompilerConfig(); + didSetCCompilerConfig = true; + } }