[stable] [flutter_tools] Fix native assets crash on Linux custom devices (#189088)
This pull request is created by [automatic cherry pick workflow](https://github.com/flutter/flutter/blob/main/docs/releases/Flutter-Cherrypick-Process.md#automatically-creates-a-cherry-pick-request)
Please fill in the form below, and a flutter domain expert will evaluate this cherry pick request.
### Issue Link:
https://github.com/flutter/flutter/issues/187980
### Impact Description:
When native assets are enabled on Linux, building asset bundles (e.g., `flutter build bundle` or running on custom devices without a native CMake build) crashes with a tool exit because `CMakeCache.txt` is missing.
### Changelog Description:
[flutter/187980](https://github.com/flutter/flutter/issues/187980): On Linux, building asset bundles with native assets enabled without a native app build directory crashes due to missing `CMakeCache.txt`.
### Workaround:
None for custom devices or bundle builds where CMake is not executed.
### Risk:
What is the risk level of this cherry-pick?
### Test Coverage:
Are you confident that your fix is well-tested by automated tests?
### Validation Steps:
1. Enable native assets on Linux (`flutter config --enable-native-assets`).
2. Run `flutter build bundle` in a project with a native asset hook.
3. Verify that `flutter build bundle` succeeds without crashing due to missing `CMakeCache.txt`.
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 756f2ed..546889e 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
@@ -17,7 +17,10 @@
///
/// Flutter also builds code assets for widget tests. Since there is no app build in that context,
/// [cmakeDirectory] should be set to null for those builds.
-Future<CCompilerConfig?> cCompilerConfigLinux({Directory? cmakeDirectory}) async {
+Future<CCompilerConfig?> cCompilerConfigLinux({
+ Directory? cmakeDirectory,
+ bool throwIfNotFound = true,
+}) async {
if (cmakeDirectory == null) {
// No CMake reference (e.g. for a widget test). Hooks can resolve to any
// compiler.
@@ -27,9 +30,12 @@
// For app builds, use the same compiler as the native/GTK parts of the app.
final File cmakeCacheTxt = cmakeDirectory.childFile('CMakeCache.txt');
if (!cmakeCacheTxt.existsSync()) {
- throwToolExit(
- 'Could not read compiler configurations for build hooks, expected ${cmakeCacheTxt.path} to exist.',
- );
+ if (throwIfNotFound) {
+ throwToolExit(
+ 'Could not read compiler configurations for build hooks, expected ${cmakeCacheTxt.path} to exist.',
+ );
+ }
+ return null;
}
const archiverVariable = 'CMAKE_AR';
@@ -76,19 +82,31 @@
return file.uri;
}
- // Find clang next to the clang++ we use in CMake
- File clangPpFile = globals.fs.file(requireTool(cxxCompiler, compilerVariable));
- clangPpFile = globals.fs.file(await clangPpFile.resolveSymbolicLinks());
- final File clangFile = clangPpFile.parent.childFile('clang');
- if (!clangFile.existsSync()) {
- throwToolExit('Expected to find clang next to ${clangPpFile.path}');
- }
+ try {
+ // Find clang next to the clang++ we use in CMake
+ File clangPpFile = globals.fs.file(requireTool(cxxCompiler, compilerVariable));
+ clangPpFile = globals.fs.file(await clangPpFile.resolveSymbolicLinks());
+ final File clangFile = clangPpFile.parent.childFile('clang');
+ if (!clangFile.existsSync()) {
+ throwToolExit('Expected to find clang next to ${clangPpFile.path}');
+ }
- return CCompilerConfig(
- compiler: clangFile.uri,
- linker: requireTool(linker, linkerVariable),
- archiver: requireTool(archiver, archiverVariable),
- );
+ return CCompilerConfig(
+ compiler: clangFile.uri,
+ linker: requireTool(linker, linkerVariable),
+ archiver: requireTool(archiver, archiverVariable),
+ );
+ } on ToolExit {
+ if (throwIfNotFound) {
+ rethrow;
+ }
+ return null;
+ } on FileSystemException {
+ if (throwIfNotFound) {
+ rethrow;
+ }
+ return null;
+ }
}
// Format: `VARIABLE_NAME:TYPE=value`;
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 b2f0c47..e4726d0 100644
--- a/packages/flutter_tools/lib/src/isolated/native_assets/targets.dart
+++ b/packages/flutter_tools/lib/src/isolated/native_assets/targets.dart
@@ -276,12 +276,11 @@
@override
Future<void> setCCompilerConfig({bool mustMatchAppBuild = true}) async {
- if (cmakeBuildDirectory == null && mustMatchAppBuild) {
- throw StateError('Missing CMake build directory on LinuxAssetTarget');
- }
+ final bool isNativeAppBuild = cmakeBuildDirectory != null && cmakeBuildDirectory!.existsSync();
cCompilerConfigSync = await cCompilerConfigLinux(
- cmakeDirectory: mustMatchAppBuild ? cmakeBuildDirectory! : null,
+ cmakeDirectory: isNativeAppBuild ? cmakeBuildDirectory : null,
+ throwIfNotFound: mustMatchAppBuild,
);
}
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 ea7028f..23074d1 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
@@ -2,6 +2,8 @@
// 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:code_assets/code_assets.dart';
import 'package:file/file.dart';
import 'package:file/memory.dart';
@@ -179,7 +181,7 @@
# CMAKE_LINKER:FILEPATH=/some/path/to/ld.lld
''');
- expect(cCompilerConfigLinux(cmakeDirectory: environment.buildDir), throwsA(isA<ToolExit>()));
+ expect(cCompilerConfigLinux(cmakeDirectory: environment.outputDir), throwsA(isA<ToolExit>()));
},
);
@@ -200,7 +202,7 @@
CMAKE_LINKER:FILEPATH=/some/path/to/ld.lld
''');
- expect(cCompilerConfigLinux(cmakeDirectory: environment.buildDir), throwsA(isA<ToolExit>()));
+ expect(cCompilerConfigLinux(cmakeDirectory: environment.outputDir), throwsA(isA<ToolExit>()));
},
);
@@ -219,6 +221,176 @@
expect(cCompilerConfigLinux(), completes);
},
);
+
+ testUsingContext(
+ 'cCompilerConfigLinux missing CMakeCache and throwIfNotFound: false',
+ overrides: <Type, Generator>{
+ ProcessManager: () => FakeProcessManager.empty(),
+ FileSystem: () => fileSystem,
+ },
+ () async {
+ if (!const LocalPlatform().isLinux) {
+ return;
+ }
+
+ final CCompilerConfig? result = await cCompilerConfigLinux(
+ cmakeDirectory: environment.buildDir,
+ throwIfNotFound: false,
+ );
+ expect(result, isNull);
+ },
+ );
+
+ testUsingContext(
+ 'cCompilerConfigLinux missing entry and throwIfNotFound: false',
+ overrides: <Type, Generator>{
+ ProcessManager: () => FakeProcessManager.empty(),
+ FileSystem: () => fileSystem,
+ },
+ () async {
+ if (!const LocalPlatform().isLinux) {
+ return;
+ }
+
+ await environment.outputDir.childFile('CMakeCache.txt').writeAsString('''
+//CMAKE_CXX_COMPILER:FILEPATH=/some/path/to/clang++
+//CMAKE_AR:FILEPATH=/some/path/to/llvm-ar
+# CMAKE_LINKER:FILEPATH=/some/path/to/ld.lld
+''');
+
+ final CCompilerConfig? result = await cCompilerConfigLinux(
+ cmakeDirectory: environment.outputDir,
+ throwIfNotFound: false,
+ );
+ expect(result, isNull);
+ },
+ );
+
+ testUsingContext(
+ 'cCompilerConfigLinux invalid paths and throwIfNotFound: false',
+ overrides: <Type, Generator>{
+ ProcessManager: () => FakeProcessManager.empty(),
+ FileSystem: () => fileSystem,
+ },
+ () async {
+ if (!const LocalPlatform().isLinux) {
+ return;
+ }
+
+ await environment.outputDir.childFile('CMakeCache.txt').writeAsString('''
+CMAKE_CXX_COMPILER:FILEPATH=/some/path/to/clang++
+CMAKE_AR:FILEPATH=/some/path/to/llvm-ar
+CMAKE_LINKER:FILEPATH=/some/path/to/ld.lld
+''');
+
+ final CCompilerConfig? result = await cCompilerConfigLinux(
+ cmakeDirectory: environment.outputDir,
+ throwIfNotFound: false,
+ );
+ expect(result, isNull);
+ },
+ );
+
+ testUsingContext(
+ 'cCompilerConfigLinux FileSystemException on resolveSymbolicLinks and throwIfNotFound: false',
+ overrides: <Type, Generator>{
+ ProcessManager: () => FakeProcessManager.empty(),
+ FileSystem: () =>
+ _ThrowingResolveFileSystem(fileSystem, '${environment.outputDir.path}/mock_clang++'),
+ },
+ () async {
+ if (!const LocalPlatform().isLinux) {
+ return;
+ }
+
+ await environment.outputDir.childFile('CMakeCache.txt').writeAsString('''
+CMAKE_CXX_COMPILER:FILEPATH=${environment.outputDir.path}/mock_clang++
+CMAKE_AR:FILEPATH=/some/path/to/llvm-ar
+CMAKE_LINKER:FILEPATH=/some/path/to/ld.lld
+''');
+
+ // Create the file so requireTool passes existsSync()
+ await environment.outputDir.childFile('mock_clang++').create();
+
+ final CCompilerConfig? result = await cCompilerConfigLinux(
+ cmakeDirectory: environment.outputDir,
+ throwIfNotFound: false,
+ );
+ expect(result, isNull);
+ },
+ );
+
+ testUsingContext(
+ 'cCompilerConfigLinux FileSystemException on resolveSymbolicLinks and throwIfNotFound: true',
+ overrides: <Type, Generator>{
+ ProcessManager: () => FakeProcessManager.empty(),
+ FileSystem: () =>
+ _ThrowingResolveFileSystem(fileSystem, '${environment.outputDir.path}/mock_clang++'),
+ },
+ () async {
+ if (!const LocalPlatform().isLinux) {
+ return;
+ }
+
+ await environment.outputDir.childFile('CMakeCache.txt').writeAsString('''
+CMAKE_CXX_COMPILER:FILEPATH=${environment.outputDir.path}/mock_clang++
+CMAKE_AR:FILEPATH=/some/path/to/llvm-ar
+CMAKE_LINKER:FILEPATH=/some/path/to/ld.lld
+''');
+
+ // Create the file so requireTool passes existsSync()
+ await environment.outputDir.childFile('mock_clang++').create();
+
+ expect(
+ cCompilerConfigLinux(cmakeDirectory: environment.outputDir),
+ throwsA(isA<FileSystemException>()),
+ );
+ },
+ );
}
class _BuildRunnerWithoutClang extends FakeFlutterNativeAssetsBuildRunner {}
+
+class _ThrowingResolveFileSystem extends ForwardingFileSystem {
+ _ThrowingResolveFileSystem(super.delegate, this.throwingPath);
+
+ final String throwingPath;
+
+ @override
+ File file(dynamic path) {
+ final File delegateFile = super.file(path);
+ if (delegateFile.path == throwingPath) {
+ return _ThrowingResolveFile(this, delegateFile);
+ }
+ return delegateFile;
+ }
+}
+
+class _ThrowingResolveFile extends ForwardingFileSystemEntity<File, io.File> with ForwardingFile {
+ _ThrowingResolveFile(this.fileSystem, this.delegate);
+
+ @override
+ final io.File delegate;
+
+ @override
+ final FileSystem fileSystem;
+
+ @override
+ File wrapFile(io.File delegate) => _ThrowingResolveFile(fileSystem, delegate);
+
+ @override
+ Directory wrapDirectory(io.Directory delegate) => throw UnimplementedError();
+
+ @override
+ Link wrapLink(io.Link delegate) => throw UnimplementedError();
+
+ @override
+ Future<String> resolveSymbolicLinks() async {
+ throw const FileSystemException('Mock FileSystemException during resolveSymbolicLinks');
+ }
+
+ @override
+ String resolveSymbolicLinksSync() {
+ throw const FileSystemException('Mock FileSystemException during resolveSymbolicLinksSync');
+ }
+}
diff --git a/packages/flutter_tools/test/integration.shard/isolated/native_assets_without_cbuild_assemble_test.dart b/packages/flutter_tools/test/integration.shard/isolated/native_assets_without_cbuild_assemble_test.dart
index 37dad8a..098b3e8 100644
--- a/packages/flutter_tools/test/integration.shard/isolated/native_assets_without_cbuild_assemble_test.dart
+++ b/packages/flutter_tools/test/integration.shard/isolated/native_assets_without_cbuild_assemble_test.dart
@@ -54,6 +54,14 @@
codeSign: buildCommand != 'ios',
);
}
+
+ if (platform.isLinux) {
+ _testBuildBundle(
+ targetPlatform: 'linux-x64',
+ processManager: processManager,
+ hooksVersionConstraint: constraint,
+ );
+ }
}
void _testBuildCommand({
@@ -150,3 +158,69 @@
final version = dependencies['hooks']! as String;
return version;
}
+
+void _testBuildBundle({
+ required String targetPlatform,
+ required String hooksVersionConstraint,
+ required ProcessManager processManager,
+}) {
+ testWithoutContext(
+ 'flutter build bundle --target-platform=$targetPlatform succeeds without libraries',
+ () async {
+ await inTempDir((Directory tempDirectory) async {
+ const packageName = 'uses_package_hooks';
+
+ // Create a new (plain Dart SDK) project.
+ await expectLater(
+ processManager.run(<String>[
+ flutterBin,
+ 'create',
+ '--no-pub',
+ packageName,
+ ], workingDirectory: tempDirectory.path),
+ completion(const ProcessResultMatcher()),
+ );
+
+ final Directory packageDirectory = tempDirectory.childDirectory(packageName);
+
+ // Add hooks and resolve implicitly (pub add does pub get).
+ await expectLater(
+ processManager.run(<String>[
+ flutterBin,
+ 'packages',
+ 'add',
+ 'hooks:$hooksVersionConstraint',
+ ], workingDirectory: packageDirectory.path),
+ completion(const ProcessResultMatcher()),
+ );
+
+ // Add a build hook that does nothing to the package.
+ packageDirectory.childDirectory('hook').childFile('build.dart')
+ ..createSync(recursive: true)
+ ..writeAsStringSync('''
+import 'package:hooks/hooks.dart';
+
+void main(List<String> args) async {
+ await build(args, (config, output) async {});
+}
+''');
+
+ // Try building bundle.
+ final args = <String>[
+ flutterBin,
+ 'build',
+ 'bundle',
+ '--target-platform=$targetPlatform',
+ '--debug',
+ ];
+ io.stderr.writeln('Running $args...');
+ final io.Process process = await processManager.start(
+ args,
+ workingDirectory: packageDirectory.path,
+ mode: ProcessStartMode.inheritStdio,
+ );
+ expect(await process.exitCode, 0);
+ });
+ },
+ );
+}