[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);
+      });
+    },
+  );
+}