test generated code is up to date
diff --git a/pkgs/objective_c/test/generate_code_test.dart b/pkgs/objective_c/test/generate_code_test.dart
index 1f7a64a..0427cf3 100644
--- a/pkgs/objective_c/test/generate_code_test.dart
+++ b/pkgs/objective_c/test/generate_code_test.dart
@@ -9,47 +9,120 @@
 import 'dart:io';
 
 import 'package:native_test_helpers/native_test_helpers.dart';
+import 'package:path/path.dart' as p;
 import 'package:test/test.dart';
 
 import '../tool/ffigen.dart' as ffigen_tool;
 
+void _matchFileWithExpected(Uri expectedUri, Uri actualUri) {
+  final expectedPath = expectedUri.toFilePath();
+  final expectedFile = File(expectedPath);
+  final actualPath = actualUri.toFilePath();
+  final actualFile = File(actualPath);
+
+  final actualFileExists = actualFile.existsSync();
+  final actualContent = actualFileExists
+      ? actualFile.readAsStringSync().replaceAll('\r', '')
+      : null;
+
+  final expectedFileExists = expectedFile.existsSync();
+  final expectedContent = expectedFileExists
+      ? expectedFile.readAsStringSync().replaceAll('\r', '')
+      : null;
+
+  if (expectedContent != actualContent) {
+    final String diff;
+    if (expectedFileExists && actualFileExists) {
+      diff = Process.runSync('git', [
+        'diff',
+        '--no-index',
+        '--color=always',
+        expectedPath,
+        actualPath,
+      ]).stdout.toString();
+    } else if (expectedFileExists) {
+      diff = "Expected file exists, but actual file doesn't";
+    } else {
+      diff = "Expected file doesn't exist, but actual file does";
+    }
+
+    fail('''
+$diff
+
+Expected output does not match actual output:
+  ${p.relative(expectedPath)}
+      vs
+  ${p.relative(actualPath)}
+
+Regenerate bindings with `dart run pkgs/objective_c/tool/ffigen.dart`.
+''');
+  }
+}
+
 void main() {
   group('ffigen.dart', () {
     test('Runs without exception', () async {
       final packageRoot = findPackageRoot('objective_c');
-      // As well as testing that this returns normally, this also generates
-      // coverage info for the parts of FFIgen that are gated by
-      // generate-for-package-objective-c. The github workflow that runs this
-      // test also uses it to verify that there are no git-diffs in the output.
-      await expectLater(
-        ffigen_tool.run(format: true, packageRoot: packageRoot),
-        completes,
-      );
+      final tempDir = Directory.fromUri(
+        packageRoot.resolve('test/'),
+      ).createTempSync();
+      try {
+        // As well as testing that the generated code is up to date, this also
+        // generates coverage info for the parts of FFIgen that are gated by
+        // generate-for-package-objective-c.
+        await expectLater(
+          ffigen_tool.run(
+            format: true,
+            packageRoot: packageRoot,
+            outDir: tempDir.uri,
+          ),
+          completes,
+        );
 
-      // Sanity check the generated code.
-      final rBindings = File.fromUri(
-        packageRoot.resolve('lib/src/runtime_bindings_generated.dart'),
-      ).readAsStringSync();
-      expect(rBindings, contains('sel_registerName'));
-      expect(rBindings, contains('objc_msgSend'));
-      expect(rBindings, contains('_NSConcreteGlobalBlock'));
-      expect(rBindings, contains('ObjCObject'));
+        // Sanity check the generated code.
+        final rBindings = File.fromUri(
+          tempDir.uri.resolve(ffigen_tool.runtimeBindings),
+        ).readAsStringSync();
+        expect(rBindings, contains('sel_registerName'));
+        expect(rBindings, contains('objc_msgSend'));
+        expect(rBindings, contains('_NSConcreteGlobalBlock'));
+        expect(rBindings, contains('ObjCObject'));
 
-      final cBindings = File.fromUri(
-        packageRoot.resolve('lib/src/c_bindings_generated.dart'),
-      ).readAsStringSync();
-      expect(cBindings, contains('createContext'));
-      expect(cBindings, contains('DOBJC_Context'));
-      expect(cBindings, contains('Dart_FinalizableHandle'));
-      expect(cBindings, contains('ILLEGAL_PORT'));
-      expect(cBindings, contains('ObjCBlockImpl'));
+        final cBindings = File.fromUri(
+          tempDir.uri.resolve(ffigen_tool.cBindings),
+        ).readAsStringSync();
+        expect(cBindings, contains('createContext'));
+        expect(cBindings, contains('DOBJC_Context'));
+        expect(cBindings, contains('Dart_FinalizableHandle'));
+        expect(cBindings, contains('ILLEGAL_PORT'));
+        expect(cBindings, contains('ObjCBlockImpl'));
 
-      final objcBindings = File.fromUri(
-        packageRoot.resolve('lib/src/objective_c_bindings_generated.dart'),
-      ).readAsStringSync();
-      expect(objcBindings, contains('class NSObject'));
-      expect(objcBindings, contains('class NSString'));
-      expect(objcBindings, contains('NSString(String str)'));
+        final objcBindings = File.fromUri(
+          tempDir.uri.resolve(ffigen_tool.objcBindings),
+        ).readAsStringSync();
+        expect(objcBindings, contains('class NSObject'));
+        expect(objcBindings, contains('class NSString'));
+        expect(objcBindings, contains('NSString(String str)'));
+
+        for (final file in [
+          ffigen_tool.runtimeBindings,
+          ffigen_tool.cBindings,
+          ffigen_tool.objcBindings,
+          ffigen_tool.objcLangBindings,
+          ffigen_tool.objcExports,
+        ]) {
+          _matchFileWithExpected(
+            packageRoot.resolve(file),
+            tempDir.uri.resolve(file),
+          );
+        }
+        _matchFileWithExpected(
+          packageRoot.resolve('../${ffigen_tool.builtInTypes}'),
+          tempDir.uri.resolve(ffigen_tool.builtInTypes),
+        );
+      } finally {
+        tempDir.deleteSync(recursive: true);
+      }
     });
   });
 }
diff --git a/pkgs/objective_c/tool/ffigen.dart b/pkgs/objective_c/tool/ffigen.dart
index 18a700b..ce1cd7c 100644
--- a/pkgs/objective_c/tool/ffigen.dart
+++ b/pkgs/objective_c/tool/ffigen.dart
@@ -11,11 +11,15 @@
 import 'package:ffigen/ffigen.dart';
 import 'package:logging/logging.dart';
 import 'package:native_test_helpers/native_test_helpers.dart';
+import 'package:path/path.dart' as p;
 
 const assetId = 'package:objective_c/objective_c.dylib';
 const runtimeBindings = 'lib/src/runtime_bindings_generated.dart';
 const cBindings = 'lib/src/c_bindings_generated.dart';
 const objcBindings = 'lib/src/objective_c_bindings_generated.dart';
+const objcLangBindings = 'src/objective_c_bindings_generated.m';
+const objcExports = 'lib/src/objective_c_bindings_exported.dart';
+const builtInTypes = 'ffigen/lib/src/code_generator/objc_built_in_types.dart';
 
 const objcInterfaces = {
   'DOBJCDartInputStreamAdapter': 'DartInputStreamAdapter',
@@ -187,35 +191,59 @@
   await run(format: argResults.flag('format'));
 }
 
-Future<void> run({required bool format, Uri? packageRoot}) async {
+Future<void> run({required bool format, Uri? packageRoot, Uri? outDir}) async {
   packageRoot ??= _defaultPackageRoot();
+  final outRoot = outDir ?? packageRoot;
   final logger = Logger.root;
   logger.level = Level.SEVERE;
 
-  final runtimeBindingsPath = packageRoot.resolve(runtimeBindings).toFilePath();
-  final cBindingsPath = packageRoot.resolve(cBindings).toFilePath();
-  final objcBindingsPath = packageRoot.resolve(objcBindings).toFilePath();
-  final objcExportsPath = packageRoot
-      .resolve('lib/src/objective_c_bindings_exported.dart')
-      .toFilePath();
+  final runtimeBindingsPath = outRoot.resolve(runtimeBindings).toFilePath();
+  final cBindingsPath = outRoot.resolve(cBindings).toFilePath();
+  final objcBindingsPath = outRoot.resolve(objcBindings).toFilePath();
+  final objcLangBindingsPath = outRoot.resolve(objcLangBindings).toFilePath();
+  final objcExportsPath = outRoot.resolve(objcExports).toFilePath();
   final extraMethodsFilePath = packageRoot
       .resolve('tool/data/extra_methods.dart.in')
       .toFilePath();
-  final builtInTypesPath = packageRoot
-      .resolve('../ffigen/lib/src/code_generator/objc_built_in_types.dart')
-      .toFilePath();
+  final builtInTypesPath =
+      (outDir != null
+              ? outDir.resolve(builtInTypes)
+              : packageRoot.resolve('../$builtInTypes'))
+          .toFilePath();
   final interfaceListTestPath = packageRoot
       .resolve('test/interface_lists_test.dart')
       .toFilePath();
 
   print('Generating runtime bindings...');
-  await getRuntimeConfig(packageRoot).generate(logger: logger);
+  await getRuntimeConfig(
+    packageRoot: packageRoot,
+    outDir: outRoot,
+  ).generate(logger: logger);
 
   print('Generating C bindings...');
-  await getCConfig(packageRoot).generate(logger: logger);
+  await getCConfig(
+    packageRoot: packageRoot,
+    outDir: outRoot,
+  ).generate(logger: logger);
 
   print('Generating ObjC bindings...');
-  await getObjCConfig(packageRoot).generate(logger: logger);
+  await getObjCConfig(
+    packageRoot: packageRoot,
+    outDir: outRoot,
+  ).generate(logger: logger);
+  if (outDir != null) {
+    final relPrefix = p.relative(
+      packageRoot.resolve('src').toFilePath(),
+      from: outRoot.resolve('src').toFilePath(),
+    );
+    final mFile = File(objcLangBindingsPath);
+    mFile.writeAsStringSync(
+      mFile.readAsStringSync().replaceAll(
+        '#import "$relPrefix${p.separator}',
+        '#import "',
+      ),
+    );
+  }
   mergeExtraMethods(objcBindingsPath, parseExtraMethods(extraMethodsFilePath));
 
   print('Generating objc_built_in_types.dart...');
@@ -236,11 +264,13 @@
     ], workingDir: packageRoot.toFilePath());
   }
 
-  print('Running tests...');
-  dartCmd([
-    'test',
-    interfaceListTestPath,
-  ], workingDir: packageRoot.toFilePath());
+  if (outDir == null) {
+    print('Running tests...');
+    dartCmd([
+      'test',
+      interfaceListTestPath,
+    ], workingDir: packageRoot.toFilePath());
+  }
 }
 
 List<String> writeBuiltInTypes(String out) {
@@ -284,13 +314,17 @@
   exports.addAll([for (final name in objcProtocols.values) '$name\$Builder']);
   writeSet('objCBuiltInCategories', objcCategories);
 
-  File(out).writeAsStringSync(s.toString());
+  File(out)
+    ..createSync(recursive: true)
+    ..writeAsStringSync(s.toString());
 
   return exports.toList()..sort();
 }
 
 void writeExports(List<String> exports, String out) {
-  File(out).writeAsStringSync('''
+  File(out)
+    ..createSync(recursive: true)
+    ..writeAsStringSync('''
 // Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
@@ -303,12 +337,13 @@
 ''');
 }
 
-FfiGenerator getRuntimeConfig([Uri? packageRoot]) {
+FfiGenerator getRuntimeConfig({Uri? packageRoot, Uri? outDir}) {
   packageRoot ??= _defaultPackageRoot();
+  outDir ??= packageRoot;
 
   return FfiGenerator(
     output: Output(
-      dart: DartOutput(path: packageRoot.resolve(runtimeBindings)),
+      dart: DartOutput(path: outDir.resolve(runtimeBindings)),
       style: const NativeExternalBindings(),
       format: false,
       preamble: '''
@@ -388,12 +423,13 @@
   );
 }
 
-FfiGenerator getCConfig([Uri? packageRoot]) {
+FfiGenerator getCConfig({Uri? packageRoot, Uri? outDir}) {
   packageRoot ??= _defaultPackageRoot();
+  outDir ??= packageRoot;
 
   return FfiGenerator(
     output: Output(
-      dart: DartOutput(path: packageRoot.resolve(cBindings)),
+      dart: DartOutput(path: outDir.resolve(cBindings)),
       style: const NativeExternalBindings(assetId: assetId),
       format: false,
       preamble: '''
@@ -461,14 +497,13 @@
   );
 }
 
-FfiGenerator getObjCConfig([Uri? packageRoot]) {
+FfiGenerator getObjCConfig({Uri? packageRoot, Uri? outDir}) {
   packageRoot ??= _defaultPackageRoot();
+  outDir ??= packageRoot;
   return FfiGenerator(
     output: Output(
-      dart: DartOutput(path: packageRoot.resolve(objcBindings)),
-      objectiveCFile: packageRoot.resolve(
-        'src/objective_c_bindings_generated.m',
-      ),
+      dart: DartOutput(path: outDir.resolve(objcBindings)),
+      objectiveCFile: outDir.resolve(objcLangBindings),
       style: const NativeExternalBindings(assetId: assetId),
       format: false,
       preamble: '''