[ffigen] Symbol file import export (#468)
diff --git a/pkgs/ffigen/.github/workflows/test-package.yml b/pkgs/ffigen/.github/workflows/test-package.yml index fdc7a01..f6deae3 100644 --- a/pkgs/ffigen/.github/workflows/test-package.yml +++ b/pkgs/ffigen/.github/workflows/test-package.yml
@@ -27,7 +27,7 @@ sdk: ${{ matrix.sdk }} - id: install name: Install dependencies - run: dart pub get + run: dart pub get && dart pub get --directory="example/shared_bindings" - name: Check formatting run: dart format --output=none --set-exit-if-changed . if: always() && steps.install.outcome == 'success'
diff --git a/pkgs/ffigen/CHANGELOG.md b/pkgs/ffigen/CHANGELOG.md index 9980c10..1a577da 100644 --- a/pkgs/ffigen/CHANGELOG.md +++ b/pkgs/ffigen/CHANGELOG.md
@@ -1,3 +1,7 @@ +# 7.2.0 +- Added support for sharing bindings using `symbol-file` config. (See `README.md` +and examples/shared_bindings). + # 7.1.0 - Handle declarations with definition accessible from a different entry-point.
diff --git a/pkgs/ffigen/README.md b/pkgs/ffigen/README.md index c72249b..76e944f 100644 --- a/pkgs/ffigen/README.md +++ b/pkgs/ffigen/README.md
@@ -92,6 +92,12 @@ ```yaml output: 'generated_bindings.dart' ``` +or +```yaml +output: + bindings: 'generated_bindings.dart' + ... +``` </td> </tr> <tr> @@ -521,6 +527,36 @@ ``` </td> </tr> + <tr> + <td>output -> symbol-file</td> + <td>Generates a symbol file yaml containing all types defined in the generated output.</td> + <td> + +```yaml +output: + ... + symbol-file: + # Although file paths are supported here, prefer Package Uri's here + # so that other pacakges can use them. + output: 'package:some_pkg/symbols.yaml' + import-path: 'package:some_pkg/base.dart' +``` +</td> + </tr> + <tr> + <td>import -> symbol-files</td> + <td>Import symbols from a symbol file. Used for sharing type definitions from other pacakges.</td> + <td> + +```yaml +import: + symbol-files: + # Both package Uri and file paths are supported here. + - 'package:some_pkg/symbols.yaml' + - 'path/to/some/symbol_file.yaml' +``` +</td> + </tr> </tbody> </table> @@ -757,3 +793,16 @@ Level options are - `[all, fine, info (default), warning, severe]`. The `all` and `fine` will print a ton of logs are meant for debugging purposes only. + +### How can type definitions be shared? + +Ffigen can share type definitions using symbol files. +- A package can generate a symbol file using the `output -> symbol-file` config. +- And another package can then import this, using `import -> symbol-files` config. +- Doing so will reuse all the types such as Struct/Unions, and will automatically + exclude generating other types (E.g functions, enums, macros). + +Checkout `examples/shared_bindings` for details. + +For manually reusing definitions from another package, the `library-imports` +and `type-map` config can be used.
diff --git a/pkgs/ffigen/example/shared_bindings/.gitignore b/pkgs/ffigen/example/shared_bindings/.gitignore new file mode 100644 index 0000000..1b05164 --- /dev/null +++ b/pkgs/ffigen/example/shared_bindings/.gitignore
@@ -0,0 +1,11 @@ +# Files and directories created by pub. +.dart_tool/ +.packages +# Remove the following pattern if you wish to check in your lock file. +pubspec.lock + +# Conventional directory for build outputs. +build/ + +# Directory created by dartdoc. +doc/api/
diff --git a/pkgs/ffigen/example/shared_bindings/README.md b/pkgs/ffigen/example/shared_bindings/README.md new file mode 100644 index 0000000..6a81fdd --- /dev/null +++ b/pkgs/ffigen/example/shared_bindings/README.md
@@ -0,0 +1,10 @@ +# Shared bindings + +An example to showcase how bindings can share types +with other bindings. + +## Generating bindings +At the root of this example (`example/shared_bindings`), run - +``` +dart run generate.dart +```
diff --git a/pkgs/ffigen/example/shared_bindings/ffigen_configs/a.yaml b/pkgs/ffigen/example/shared_bindings/ffigen_configs/a.yaml new file mode 100644 index 0000000..55a720e --- /dev/null +++ b/pkgs/ffigen/example/shared_bindings/ffigen_configs/a.yaml
@@ -0,0 +1,12 @@ +# Copyright (c) 2022, 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. + +name: NativeLibraryA +description: Bindings to `headers/a.h`. +output: '../lib/generated/a_gen.dart' +headers: + entry-points: + - '../headers/a.h' +preamble: | + // ignore_for_file: non_constant_identifier_names, camel_case_types
diff --git a/pkgs/ffigen/example/shared_bindings/ffigen_configs/a_shared_base.yaml b/pkgs/ffigen/example/shared_bindings/ffigen_configs/a_shared_base.yaml new file mode 100644 index 0000000..0adcc20 --- /dev/null +++ b/pkgs/ffigen/example/shared_bindings/ffigen_configs/a_shared_base.yaml
@@ -0,0 +1,16 @@ +# Copyright (c) 2022, 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. + +name: NativeLibraryASharedB +description: Bindings to `headers/a.h` with shared definitions from `headers/base.h`. +output: '../lib/generated/a_shared_b_gen.dart' +headers: + entry-points: + - '../headers/a.h' +import: + symbol-files: + # Both package Uri and file paths are supported here. + - 'package:shared_bindings/generated/base_symbols.yaml' +preamble: | + // ignore_for_file: non_constant_identifier_names, camel_case_types
diff --git a/pkgs/ffigen/example/shared_bindings/ffigen_configs/base.yaml b/pkgs/ffigen/example/shared_bindings/ffigen_configs/base.yaml new file mode 100644 index 0000000..474d0f9 --- /dev/null +++ b/pkgs/ffigen/example/shared_bindings/ffigen_configs/base.yaml
@@ -0,0 +1,17 @@ +# Copyright (c) 2022, 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. + +name: NativeLibraryBase +description: Bindings to `headers/base.h`. +output: + bindings: '../lib/generated/base_gen.dart' + symbol-file: + # Although file paths are supported here, prefer Package Uri's here. + output: 'package:shared_bindings/generated/base_symbols.yaml' + import-path: 'package:shared_bindings/generated/base_gen.dart' +headers: + entry-points: + - '../headers/base.h' +preamble: | + // ignore_for_file: non_constant_identifier_names, camel_case_types
diff --git a/pkgs/ffigen/example/shared_bindings/generate.dart b/pkgs/ffigen/example/shared_bindings/generate.dart new file mode 100644 index 0000000..8e565cf --- /dev/null +++ b/pkgs/ffigen/example/shared_bindings/generate.dart
@@ -0,0 +1,32 @@ +import 'dart:io'; + +import 'package:cli_util/cli_util.dart'; +import 'package:path/path.dart' as p; + +ProcessResult runFfigenForConfig(String sdkPath, String configPath) { + return Process.runSync( + p.join(sdkPath, 'bin', 'dart'), + [ + 'run', + 'ffigen', + '--config=$configPath', + ], + runInShell: Platform.isWindows, + ); +} + +void main() { + final sdkPath = getSdkPath(); + final configPaths = [ + 'ffigen_configs/base.yaml', + 'ffigen_configs/a.yaml', + 'ffigen_configs/a_shared_base.yaml' + ]; + for (final configPath in configPaths) { + final res = runFfigenForConfig(sdkPath, configPath); + print(res.stdout.toString()); + if (res.exitCode != 0) { + throw Exception("Some error occurred: ${res.stderr.toString()}"); + } + } +}
diff --git a/pkgs/ffigen/example/shared_bindings/headers/a.h b/pkgs/ffigen/example/shared_bindings/headers/a.h new file mode 100644 index 0000000..ef3bbea --- /dev/null +++ b/pkgs/ffigen/example/shared_bindings/headers/a.h
@@ -0,0 +1,21 @@ + +#include "base.h" + +struct A_Struct1{ + int a; +}; + +union A_Union1{ + int a; +}; + +enum A_Enum{ + A_ENUM_1, + A_ENUM_2, +}; + +#define A_MACRO_1 1; + +void a_func1(); + +void a_func2(struct BaseStruct2 s, union BaseUnion2 u, BaseTypedef2 t);
diff --git a/pkgs/ffigen/example/shared_bindings/headers/base.h b/pkgs/ffigen/example/shared_bindings/headers/base.h new file mode 100644 index 0000000..345cac6 --- /dev/null +++ b/pkgs/ffigen/example/shared_bindings/headers/base.h
@@ -0,0 +1,31 @@ +// Copyright (c) 2022, 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. + +struct BaseStruct1{ + int a; +}; + +union BaseUnion1{ + int a; +}; + +struct BaseStruct2{ + int a; +}; + +union BaseUnion2{ + int a; +}; + +typedef struct BaseStruct1 BaseTypedef1; +typedef struct BaseStruct2 BaseTypedef2; + +enum BaseEnum{ + BASE_ENUM_1, + BASE_ENUM_2, +}; + +#define BASE_MACRO_1 1; + +void base_func1(BaseTypedef1 t1, BaseTypedef2 t2);
diff --git a/pkgs/ffigen/example/shared_bindings/lib/generated/a_gen.dart b/pkgs/ffigen/example/shared_bindings/lib/generated/a_gen.dart new file mode 100644 index 0000000..631aff8 --- /dev/null +++ b/pkgs/ffigen/example/shared_bindings/lib/generated/a_gen.dart
@@ -0,0 +1,112 @@ +// ignore_for_file: non_constant_identifier_names, camel_case_types + +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +import 'dart:ffi' as ffi; + +/// Bindings to `headers/a.h`. +class NativeLibraryA { + /// Holds the symbol lookup function. + final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName) + _lookup; + + /// The symbols are looked up in [dynamicLibrary]. + NativeLibraryA(ffi.DynamicLibrary dynamicLibrary) + : _lookup = dynamicLibrary.lookup; + + /// The symbols are looked up with [lookup]. + NativeLibraryA.fromLookup( + ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName) + lookup) + : _lookup = lookup; + + void base_func1( + BaseTypedef1 t1, + BaseTypedef2 t2, + ) { + return _base_func1( + t1, + t2, + ); + } + + late final _base_func1Ptr = _lookup< + ffi.NativeFunction<ffi.Void Function(BaseTypedef1, BaseTypedef2)>>( + 'base_func1'); + late final _base_func1 = + _base_func1Ptr.asFunction<void Function(BaseTypedef1, BaseTypedef2)>(); + + void a_func1() { + return _a_func1(); + } + + late final _a_func1Ptr = + _lookup<ffi.NativeFunction<ffi.Void Function()>>('a_func1'); + late final _a_func1 = _a_func1Ptr.asFunction<void Function()>(); + + void a_func2( + BaseStruct2 s, + BaseUnion2 u, + BaseTypedef2 t, + ) { + return _a_func2( + s, + u, + t, + ); + } + + late final _a_func2Ptr = _lookup< + ffi.NativeFunction< + ffi.Void Function(BaseStruct2, BaseUnion2, BaseTypedef2)>>('a_func2'); + late final _a_func2 = _a_func2Ptr + .asFunction<void Function(BaseStruct2, BaseUnion2, BaseTypedef2)>(); +} + +class BaseStruct1 extends ffi.Struct { + @ffi.Int() + external int a; +} + +class BaseUnion1 extends ffi.Union { + @ffi.Int() + external int a; +} + +class BaseStruct2 extends ffi.Struct { + @ffi.Int() + external int a; +} + +class BaseUnion2 extends ffi.Union { + @ffi.Int() + external int a; +} + +abstract class BaseEnum { + static const int BASE_ENUM_1 = 0; + static const int BASE_ENUM_2 = 1; +} + +typedef BaseTypedef1 = BaseStruct1; +typedef BaseTypedef2 = BaseStruct2; + +class A_Struct1 extends ffi.Struct { + @ffi.Int() + external int a; +} + +class A_Union1 extends ffi.Union { + @ffi.Int() + external int a; +} + +abstract class A_Enum { + static const int A_ENUM_1 = 0; + static const int A_ENUM_2 = 1; +} + +const int BASE_MACRO_1 = 1; + +const int A_MACRO_1 = 1;
diff --git a/pkgs/ffigen/example/shared_bindings/lib/generated/a_shared_b_gen.dart b/pkgs/ffigen/example/shared_bindings/lib/generated/a_shared_b_gen.dart new file mode 100644 index 0000000..fbf9c16 --- /dev/null +++ b/pkgs/ffigen/example/shared_bindings/lib/generated/a_shared_b_gen.dart
@@ -0,0 +1,70 @@ +// ignore_for_file: non_constant_identifier_names, camel_case_types + +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +import 'dart:ffi' as ffi; +import 'package:shared_bindings/generated/base_gen.dart' as _imp1; + +/// Bindings to `headers/a.h` with shared definitions from `headers/base.h`. +class NativeLibraryASharedB { + /// Holds the symbol lookup function. + final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName) + _lookup; + + /// The symbols are looked up in [dynamicLibrary]. + NativeLibraryASharedB(ffi.DynamicLibrary dynamicLibrary) + : _lookup = dynamicLibrary.lookup; + + /// The symbols are looked up with [lookup]. + NativeLibraryASharedB.fromLookup( + ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName) + lookup) + : _lookup = lookup; + + void a_func1() { + return _a_func1(); + } + + late final _a_func1Ptr = + _lookup<ffi.NativeFunction<ffi.Void Function()>>('a_func1'); + late final _a_func1 = _a_func1Ptr.asFunction<void Function()>(); + + void a_func2( + _imp1.BaseStruct2 s, + _imp1.BaseUnion2 u, + _imp1.BaseTypedef2 t, + ) { + return _a_func2( + s, + u, + t, + ); + } + + late final _a_func2Ptr = _lookup< + ffi.NativeFunction< + ffi.Void Function(_imp1.BaseStruct2, _imp1.BaseUnion2, + _imp1.BaseTypedef2)>>('a_func2'); + late final _a_func2 = _a_func2Ptr.asFunction< + void Function(_imp1.BaseStruct2, _imp1.BaseUnion2, _imp1.BaseTypedef2)>(); +} + +class A_Struct1 extends ffi.Struct { + @ffi.Int() + external int a; +} + +class A_Union1 extends ffi.Union { + @ffi.Int() + external int a; +} + +abstract class A_Enum { + static const int A_ENUM_1 = 0; + static const int A_ENUM_2 = 1; +} + +const int BASE_MACRO_1 = 1; + +const int A_MACRO_1 = 1;
diff --git a/pkgs/ffigen/example/shared_bindings/lib/generated/base_gen.dart b/pkgs/ffigen/example/shared_bindings/lib/generated/base_gen.dart new file mode 100644 index 0000000..d4cfdb5 --- /dev/null +++ b/pkgs/ffigen/example/shared_bindings/lib/generated/base_gen.dart
@@ -0,0 +1,69 @@ +// ignore_for_file: non_constant_identifier_names, camel_case_types + +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +import 'dart:ffi' as ffi; + +/// Bindings to `headers/base.h`. +class NativeLibraryBase { + /// Holds the symbol lookup function. + final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName) + _lookup; + + /// The symbols are looked up in [dynamicLibrary]. + NativeLibraryBase(ffi.DynamicLibrary dynamicLibrary) + : _lookup = dynamicLibrary.lookup; + + /// The symbols are looked up with [lookup]. + NativeLibraryBase.fromLookup( + ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName) + lookup) + : _lookup = lookup; + + void base_func1( + BaseTypedef1 t1, + BaseTypedef2 t2, + ) { + return _base_func1( + t1, + t2, + ); + } + + late final _base_func1Ptr = _lookup< + ffi.NativeFunction<ffi.Void Function(BaseTypedef1, BaseTypedef2)>>( + 'base_func1'); + late final _base_func1 = + _base_func1Ptr.asFunction<void Function(BaseTypedef1, BaseTypedef2)>(); +} + +class BaseStruct1 extends ffi.Struct { + @ffi.Int() + external int a; +} + +class BaseUnion1 extends ffi.Union { + @ffi.Int() + external int a; +} + +class BaseStruct2 extends ffi.Struct { + @ffi.Int() + external int a; +} + +class BaseUnion2 extends ffi.Union { + @ffi.Int() + external int a; +} + +abstract class BaseEnum { + static const int BASE_ENUM_1 = 0; + static const int BASE_ENUM_2 = 1; +} + +typedef BaseTypedef1 = BaseStruct1; +typedef BaseTypedef2 = BaseStruct2; + +const int BASE_MACRO_1 = 1;
diff --git a/pkgs/ffigen/example/shared_bindings/lib/generated/base_symbols.yaml b/pkgs/ffigen/example/shared_bindings/lib/generated/base_symbols.yaml new file mode 100644 index 0000000..ad455fa --- /dev/null +++ b/pkgs/ffigen/example/shared_bindings/lib/generated/base_symbols.yaml
@@ -0,0 +1,22 @@ +format_version: 1.0.0 +files: + package:shared_bindings/generated/base_gen.dart: + used-config: + ffi-native: false + symbols: + c:@E@BaseEnum: + name: BaseEnum + c:@F@base_func1: + name: base_func1 + c:@S@BaseStruct1: + name: BaseStruct1 + c:@S@BaseStruct2: + name: BaseStruct2 + c:@U@BaseUnion1: + name: BaseUnion1 + c:@U@BaseUnion2: + name: BaseUnion2 + c:base.h@T@BaseTypedef1: + name: BaseTypedef1 + c:base.h@T@BaseTypedef2: + name: BaseTypedef2
diff --git a/pkgs/ffigen/example/shared_bindings/pubspec.yaml b/pkgs/ffigen/example/shared_bindings/pubspec.yaml new file mode 100644 index 0000000..b89304f --- /dev/null +++ b/pkgs/ffigen/example/shared_bindings/pubspec.yaml
@@ -0,0 +1,15 @@ +# Copyright (c) 2022, 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. + +name: shared_bindings + +environment: + sdk: '>=2.17.0 <3.0.0' + +dependencies: + ffi: ^2.0.1 +dev_dependencies: + ffigen: + path: '../../' + lints: ^1.0.1
diff --git a/pkgs/ffigen/lib/src/code_generator/func.dart b/pkgs/ffigen/lib/src/code_generator/func.dart index 4496e71..590f63c 100644 --- a/pkgs/ffigen/lib/src/code_generator/func.dart +++ b/pkgs/ffigen/lib/src/code_generator/func.dart
@@ -86,11 +86,13 @@ _exposedCFunctionTypealias = Typealias( name: 'Native$upperCaseName', type: functionType, + isInternal: true, ); _exposedDartFunctionTypealias = Typealias( name: 'Dart$upperCaseName', type: functionType, useDartType: true, + isInternal: true, ); } }
diff --git a/pkgs/ffigen/lib/src/code_generator/library.dart b/pkgs/ffigen/lib/src/code_generator/library.dart index 7c7ce47..befdfce 100644 --- a/pkgs/ffigen/lib/src/code_generator/library.dart +++ b/pkgs/ffigen/lib/src/code_generator/library.dart
@@ -9,6 +9,7 @@ import 'package:ffigen/src/config_provider/config_types.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; +import 'package:yaml_edit/yaml_edit.dart'; import '../strings.dart' as strings; import 'utils.dart'; @@ -138,6 +139,19 @@ } } + /// Generates [file] with symbol output yaml. + void generateSymbolOutputFile(File file, String importPath) { + if (!file.existsSync()) file.createSync(recursive: true); + final symbolFileYamlMap = writer.generateSymbolOutputYamlMap(importPath); + final yamlEditor = YamlEditor(""); + yamlEditor.update([], wrapAsYamlNode(symbolFileYamlMap)); + var yamlString = yamlEditor.toString(); + if (!yamlString.endsWith('\n')) { + yamlString += "\n"; + } + file.writeAsStringSync(yamlString); + } + /// Formats a file using the Dart formatter. void _dartFormat(String path) { final sdkPath = getSdkPath();
diff --git a/pkgs/ffigen/lib/src/code_generator/typealias.dart b/pkgs/ffigen/lib/src/code_generator/typealias.dart index f42e863..6a47f61 100644 --- a/pkgs/ffigen/lib/src/code_generator/typealias.dart +++ b/pkgs/ffigen/lib/src/code_generator/typealias.dart
@@ -29,12 +29,14 @@ /// /// E.g if C type is ffi.Void func(ffi.Int32), Dart type is void func(int). bool useDartType = false, + bool isInternal = false, }) : _useDartType = useDartType, super( usr: usr, name: name, dartDoc: dartDoc, originalName: originalName, + isInternal: isInternal, ); @override
diff --git a/pkgs/ffigen/lib/src/code_generator/writer.dart b/pkgs/ffigen/lib/src/code_generator/writer.dart index 1f054fc..748275d 100644 --- a/pkgs/ffigen/lib/src/code_generator/writer.dart +++ b/pkgs/ffigen/lib/src/code_generator/writer.dart
@@ -4,6 +4,11 @@ import 'package:ffigen/src/code_generator.dart'; import 'package:ffigen/src/code_generator/utils.dart'; +import 'package:logging/logging.dart'; + +import '../strings.dart' as strings; + +final _logger = Logger('ffigen.code_generator.writer'); /// To store generated String bindings. class Writer { @@ -75,6 +80,11 @@ /// Guaranteed to be a unique prefix. String get arrayHelperClassPrefix => _arrayHelperClassPrefix; + /// Set true after calling [generate]. Indicates if + /// [generateSymbolOutputYamlMap] can be called. + bool get canGenerateSymbolOutput => _canGenerateSymbolOutput; + bool _canGenerateSymbolOutput = false; + /// [_usedUpNames] should contain names of all the declarations which are /// already used. This is used to avoid name collisions. Writer({ @@ -256,8 +266,55 @@ } result.write(s); + _canGenerateSymbolOutput = true; return result.toString(); } + + Map<String, dynamic> generateSymbolOutputYamlMap(String importFilePath) { + final bindings = <Binding>[ + ...noLookUpBindings, + ...ffiNativeBindings, + ...lookUpBindings + ]; + if (!canGenerateSymbolOutput) { + throw Exception( + "Invalid state: generateSymbolOutputYamlMap() called before generate()"); + } + final result = <String, dynamic>{}; + + result[strings.formatVersion] = strings.symbolFileFormatVersion; + result[strings.files] = <String, dynamic>{}; + result[strings.files][importFilePath] = <String, dynamic>{}; + + final fileConfig = result[strings.files][importFilePath]; + fileConfig[strings.usedConfig] = <String, dynamic>{}; + + // Warn for macros. + final hasMacroBindings = bindings.any( + (element) => element is Constant && element.usr.contains('@macro@')); + if (hasMacroBindings) { + _logger.info( + 'Removing all Macros from symbol file since they cannot be cross referenced reliably.'); + } + // Remove internal bindings and macros. + bindings.removeWhere((element) { + return element.isInternal || + (element is Constant && element.usr.contains('@macro@')); + }); + // Sort bindings alphabetically by USR. + bindings.sort((a, b) => a.usr.compareTo(b.usr)); + fileConfig[strings.usedConfig][strings.ffiNative] = bindings + .whereType<Func>() + .any((element) => element.ffiNativeConfig.enabled); + fileConfig[strings.symbols] = <String, dynamic>{}; + final symbolConfig = fileConfig[strings.symbols]; + for (final binding in bindings) { + symbolConfig[binding.usr] = { + strings.name: binding.name, + }; + } + return result; + } } /// Manages the generated `_SymbolAddress` class.
diff --git a/pkgs/ffigen/lib/src/config_provider/config.dart b/pkgs/ffigen/lib/src/config_provider/config.dart index 3457c91..9c094ed 100644 --- a/pkgs/ffigen/lib/src/config_provider/config.dart +++ b/pkgs/ffigen/lib/src/config_provider/config.dart
@@ -8,6 +8,7 @@ import 'package:ffigen/src/code_generator.dart'; import 'package:logging/logging.dart'; +import 'package:package_config/package_config_types.dart'; import 'package:yaml/yaml.dart'; import '../strings.dart' as strings; @@ -24,6 +25,10 @@ String? get filename => _filename; String? _filename; + /// Package config. + PackageConfig? get packageConfig => _packageConfig; + PackageConfig? _packageConfig; + /// Location for llvm/lib folder. String get libclangDylib => _libclangDylib; late String _libclangDylib; @@ -32,6 +37,10 @@ String get output => _output; late String _output; + /// Symbol file config. + SymbolFile? get symbolFile => _symbolFile; + late SymbolFile? _symbolFile; + /// Language that ffigen is consuming. Language get language => _language; late Language _language; @@ -98,6 +107,10 @@ Map<String, LibraryImport> get libraryImports => _libraryImports; late Map<String, LibraryImport> _libraryImports; + /// Stores all the symbol file maps name to ImportedType mappings specified by user. + Map<String, ImportedType> get usrTypeMappings => _usrTypeMappings; + late Map<String, ImportedType> _usrTypeMappings; + /// Stores typedef name to ImportedType mappings specified by user. Map<String, ImportedType> get typedefTypeMappings => _typedefTypeMappings; late Map<String, ImportedType> _typedefTypeMappings; @@ -159,11 +172,12 @@ FfiNativeConfig get ffiNativeConfig => _ffiNativeConfig; late FfiNativeConfig _ffiNativeConfig; - Config._(this._filename); + Config._(this._filename, this._packageConfig); /// Create config from Yaml map. - factory Config.fromYaml(YamlMap map, [String? filename]) { - final configspecs = Config._(filename); + factory Config.fromYaml(YamlMap map, + {String? filename, PackageConfig? packageConfig}) { + final configspecs = Config._(filename, packageConfig); _logger.finest('Config Map: ' + map.toString()); final specs = configspecs._getSpecs(); @@ -178,11 +192,12 @@ } /// Create config from a file. - factory Config.fromFile(File file) { + factory Config.fromFile(File file, {PackageConfig? packageConfig}) { // Throws a [YamlException] if it's unable to parse the Yaml. final configYaml = loadYaml(file.readAsStringSync()) as YamlMap; - return Config.fromYaml(configYaml, file.path); + return Config.fromYaml(configYaml, + filename: file.path, packageConfig: packageConfig); } /// Add compiler options for clang. If [highPriority] is true these are added @@ -245,11 +260,15 @@ _libclangDylib = result as String; }, ), - [strings.output]: Specification<String>( + [strings.output]: Specification<OutputConfig>( requirement: Requirement.yes, validator: outputValidator, - extractor: (dynamic value) => outputExtractor(value, filename), - extractedResult: (dynamic result) => _output = result as String, + extractor: (dynamic value) => + outputExtractor(value, filename, packageConfig), + extractedResult: (dynamic result) { + _output = (result as OutputConfig).output; + _symbolFile = result.symbolFile; + }, ), [strings.language]: Specification<Language>( requirement: Requirement.no, @@ -378,6 +397,16 @@ _libraryImports = result as Map<String, LibraryImport>; }, ), + [strings.import, strings.symbolFilesImport]: + Specification<Map<String, ImportedType>>( + validator: symbolFileImportValidator, + extractor: (value) => symbolFileImportExtractor( + value, _libraryImports, filename, packageConfig), + defaultValue: () => <String, ImportedType>{}, + extractedResult: (dynamic result) { + _usrTypeMappings = result as Map<String, ImportedType>; + }, + ), [strings.typeMap, strings.typeMapTypedefs]: Specification<Map<String, List<String>>>( validator: typeMapValidator,
diff --git a/pkgs/ffigen/lib/src/config_provider/config_types.dart b/pkgs/ffigen/lib/src/config_provider/config_types.dart index e3965c0..8521bdb 100644 --- a/pkgs/ffigen/lib/src/config_provider/config_types.dart +++ b/pkgs/ffigen/lib/src/config_provider/config_types.dart
@@ -409,3 +409,17 @@ const FfiNativeConfig({required this.enabled, this.asset}); } + +class SymbolFile { + final String importPath; + final String output; + + SymbolFile(this.importPath, this.output); +} + +class OutputConfig { + final String output; + final SymbolFile? symbolFile; + + OutputConfig(this.output, this.symbolFile); +}
diff --git a/pkgs/ffigen/lib/src/config_provider/spec_utils.dart b/pkgs/ffigen/lib/src/config_provider/spec_utils.dart index 53c8ce9..176646e 100644 --- a/pkgs/ffigen/lib/src/config_provider/spec_utils.dart +++ b/pkgs/ffigen/lib/src/config_provider/spec_utils.dart
@@ -5,9 +5,11 @@ import 'dart:io'; import 'package:ffigen/src/code_generator.dart'; +import 'package:ffigen/src/code_generator/utils.dart'; import 'package:file/local.dart'; import 'package:glob/glob.dart'; import 'package:logging/logging.dart'; +import 'package:package_config/package_config.dart'; import 'package:path/path.dart' as p; import 'package:quiver/pattern.dart' as quiver; import 'package:yaml/yaml.dart'; @@ -143,6 +145,99 @@ return true; } +void loadImportedTypes(YamlMap fileConfig, + Map<String, ImportedType> usrTypeMappings, LibraryImport libraryImport) { + final symbols = fileConfig['symbols'] as YamlMap; + for (final key in symbols.keys) { + final usr = key as String; + final value = symbols[usr]! as YamlMap; + usrTypeMappings[usr] = ImportedType( + libraryImport, value['name'] as String, value['name'] as String); + } +} + +YamlMap loadSymbolFile(String symbolFilePath, String? configFileName, + PackageConfig? packageConfig) { + final path = symbolFilePath.startsWith('package:') + ? packageConfig!.resolve(Uri.parse(symbolFilePath))!.toFilePath() + : _normalizePath(symbolFilePath, configFileName); + + return loadYaml(File(path).readAsStringSync()) as YamlMap; +} + +Map<String, ImportedType> symbolFileImportExtractor( + dynamic yamlConfig, + Map<String, LibraryImport> libraryImports, + String? configFileName, + PackageConfig? packageConfig) { + final resultMap = <String, ImportedType>{}; + for (final item in (yamlConfig as YamlList)) { + String symbolFilePath; + if (item is String) { + symbolFilePath = item; + } else { + symbolFilePath = item[strings.symbolFile] as String; + } + final symbolFile = + loadSymbolFile(symbolFilePath, configFileName, packageConfig); + final formatVersion = symbolFile[strings.formatVersion] as String; + if (formatVersion.split('.')[0] != + strings.symbolFileFormatVersion.split('.')[0]) { + _logger.severe( + 'Incompatible format versions for file $symbolFilePath: ${strings.symbolFileFormatVersion}(ours), $formatVersion(theirs).'); + exit(1); + } + final uniqueNamer = UniqueNamer(libraryImports.keys + .followedBy([strings.defaultSymbolFileImportPrefix]).toSet()); + for (final file in (symbolFile[strings.files] as YamlMap).keys) { + final existingImports = + libraryImports.values.where((element) => element.importPath == file); + if (existingImports.isEmpty) { + final name = + uniqueNamer.makeUnique(strings.defaultSymbolFileImportPrefix); + libraryImports[name] = LibraryImport(name, file as String); + } + final libraryImport = libraryImports.values.firstWhere( + (element) => element.importPath == file, + ); + loadImportedTypes( + symbolFile[strings.files][file] as YamlMap, resultMap, libraryImport); + } + } + return resultMap; +} + +bool symbolFileImportValidator(List<String> name, dynamic yamlConfig) { + if (!checkType<YamlList>(name, yamlConfig)) { + return false; + } + var result = true; + (yamlConfig as YamlList).asMap().forEach((idx, value) { + if (value is YamlMap) { + if (!value.keys.contains(strings.symbolFile)) { + result = false; + _logger + .severe('Key $name -> $idx -> ${strings.symbolFile} is required.'); + } + for (final key in value.keys) { + if (key == strings.symbolFile) { + if (!checkType<String>( + [...name, idx.toString(), key as String], value[key])) { + result = false; + } + } else { + result = false; + _logger.severe('Unknown key : $name -> $idx -> $key.'); + } + } + } else if (value is! String) { + result = false; + _logger.severe('Expected $name -> $idx should be a String or Map.'); + } + }); + return result; +} + Map<String, List<String>> typeMapExtractor(dynamic yamlConfig) { // Key - type_name, Value - [lib, cType, dartType]. final resultMap = <String, List<String>>{}; @@ -482,11 +577,91 @@ return true; } -String outputExtractor(dynamic value, String? configFilename) => - _normalizePath(value as String, configFilename); +OutputConfig outputExtractor( + dynamic value, String? configFilename, PackageConfig? packageConfig) { + if (value is String) { + return OutputConfig(_normalizePath(value, configFilename), null); + } + value = value as YamlMap; + return OutputConfig( + _normalizePath((value)[strings.bindings] as String, configFilename), + value.containsKey(strings.symbolFile) + ? symbolFileOutputExtractor( + value[strings.symbolFile], configFilename, packageConfig) + : null, + ); +} -bool outputValidator(List<String> name, dynamic value) => - checkType<String>(name, value); +bool outputValidator(List<String> name, dynamic value) { + if (value is String) { + return true; + } else if (value is YamlMap) { + final keys = value.keys; + var result = true; + for (final key in keys) { + if (key == strings.bindings) { + if (!checkType<String>([...name, key as String], value[key])) { + result = false; + } + } else if (key == strings.symbolFile) { + result = symbolFileOutputValidator( + [...name, strings.symbolFile], value[key]); + } else { + result = false; + _logger.severe("Unknown key '$key' in '$name'."); + } + } + return result; + } else { + _logger.severe( + "Expected value of key '${name.join(' -> ')}' to be a String or Map."); + return false; + } +} + +SymbolFile symbolFileOutputExtractor( + dynamic value, String? configFilename, PackageConfig? packageConfig) { + value = value as YamlMap; + var output = value[strings.output] as String; + if (Uri.parse(output).scheme != "package") { + _logger.warning( + 'Consider using a Package Uri for ${strings.symbolFile} -> ${strings.output}: $output so that external packages can use it.'); + output = _normalizePath(output, configFilename); + } else { + output = packageConfig!.resolve(Uri.parse(output))!.toFilePath(); + } + final importPath = value[strings.importPath] as String; + if (Uri.parse(importPath).scheme != "package") { + _logger.warning( + 'Consider using a Package Uri for ${strings.symbolFile} -> ${strings.importPath}: $importPath so that external packages can use it.'); + } + return SymbolFile(importPath, output); +} + +bool symbolFileOutputValidator(List<String> name, dynamic value) { + if (!checkType<YamlMap>(name, value)) { + return false; + } + if (!(value as YamlMap).containsKey(strings.output)) { + _logger.severe("Required '$name -> ${strings.output}'."); + return false; + } + if (!(value).containsKey(strings.importPath)) { + _logger.severe("Required '$name -> ${strings.importPath}'."); + return false; + } + for (final key in value.keys) { + if (key == strings.output || key == strings.importPath) { + if (!checkType<String>([...name, key as String], value[key])) { + return false; + } + } else { + _logger.severe("Unknown key '$key' in '$name'."); + return false; + } + } + return true; +} Language languageExtractor(dynamic value) { if (value == strings.langC) {
diff --git a/pkgs/ffigen/lib/src/executables/ffigen.dart b/pkgs/ffigen/lib/src/executables/ffigen.dart index b3e7c41..c37cb4f 100644 --- a/pkgs/ffigen/lib/src/executables/ffigen.dart +++ b/pkgs/ffigen/lib/src/executables/ffigen.dart
@@ -9,6 +9,7 @@ import 'package:cli_util/cli_logging.dart' show Ansi; import 'package:ffigen/ffigen.dart'; import 'package:logging/logging.dart'; +import 'package:package_config/package_config.dart'; import 'package:yaml/yaml.dart' as yaml; final _logger = Logger('ffigen.ffigen'); @@ -34,7 +35,7 @@ return '${_ansi.red}$str${_ansi.none}'; } -void main(List<String> args) { +void main(List<String> args) async { // Parses the cmd args. This will print usage and exit if --help was passed. final argResult = getArgResults(args); @@ -44,7 +45,7 @@ // Create a config object. Config config; try { - config = getConfig(argResult); + config = getConfig(argResult, await findPackageConfig(Directory.current)); } on FormatException { _logger.severe('Please fix configuration errors and re-run the tool.'); exit(1); @@ -58,17 +59,25 @@ library.generateFile(gen); _logger .info(successPen('Finished, Bindings generated in ${gen.absolute.path}')); + + if (config.symbolFile != null) { + final symbolFileGen = File(config.symbolFile!.output); + library.generateSymbolOutputFile( + symbolFileGen, config.symbolFile!.importPath); + _logger.info(successPen( + 'Finished, Symbol Output generated in ${symbolFileGen.absolute.path}')); + } } -Config getConfig(ArgResults result) { +Config getConfig(ArgResults result, PackageConfig? packageConfig) { _logger.info('Running in ${Directory.current}'); Config config; // Parse config from yaml. if (result.wasParsed(conf)) { - config = getConfigFromCustomYaml(result[conf] as String); + config = getConfigFromCustomYaml(result[conf] as String, packageConfig); } else { - config = getConfigFromPubspec(); + config = getConfigFromPubspec(packageConfig); } // Add compiler options from command line. @@ -82,7 +91,7 @@ } /// Extracts configuration from pubspec file. -Config getConfigFromPubspec() { +Config getConfigFromPubspec(PackageConfig? packageConfig) { final pubspecFile = File(pubspecName); if (!pubspecFile.existsSync()) { @@ -101,11 +110,12 @@ _logger.severe("Couldn't find an entry for '$configKey' in $pubspecName."); exit(1); } - return Config.fromYaml(bindingsConfigMap, pubspecFile.path); + return Config.fromYaml(bindingsConfigMap, + filename: pubspecFile.path, packageConfig: packageConfig); } /// Extracts configuration from a custom yaml file. -Config getConfigFromCustomYaml(String yamlPath) { +Config getConfigFromCustomYaml(String yamlPath, PackageConfig? packageConfig) { final yamlFile = File(yamlPath); if (!yamlFile.existsSync()) { @@ -113,7 +123,7 @@ exit(1); } - return Config.fromFile(yamlFile); + return Config.fromFile(yamlFile, packageConfig: packageConfig); } /// Parses the cmd line arguments.
diff --git a/pkgs/ffigen/lib/src/header_parser/includer.dart b/pkgs/ffigen/lib/src/header_parser/includer.dart index 415f6d9..5e55aa2 100644 --- a/pkgs/ffigen/lib/src/header_parser/includer.dart +++ b/pkgs/ffigen/lib/src/header_parser/includer.dart
@@ -16,6 +16,8 @@ bool Function(String, bool) configIncludes) { if (isSeenDecl(usr) || name == '') { return false; + } else if (config.usrTypeMappings.containsKey(usr)) { + return false; } else if (configIncludes(name, config.excludeAllByDefault)) { return true; } else {
diff --git a/pkgs/ffigen/lib/src/header_parser/type_extractor/extractor.dart b/pkgs/ffigen/lib/src/header_parser/type_extractor/extractor.dart index 065e789..05691d3 100644 --- a/pkgs/ffigen/lib/src/header_parser/type_extractor/extractor.dart +++ b/pkgs/ffigen/lib/src/header_parser/type_extractor/extractor.dart
@@ -174,11 +174,16 @@ // those two types are ABI compatible, so just return bool regardless. return _CreateTypeFromCursorResult(BooleanType()); } + final usr = cursor.usr(); if (config.typedefTypeMappings.containsKey(spelling)) { _logger.fine(' Type $spelling mapped from type-map'); return _CreateTypeFromCursorResult( config.typedefTypeMappings[spelling]!); } + if (config.usrTypeMappings.containsKey(usr)) { + _logger.fine(' Type $spelling mapped from usr'); + return _CreateTypeFromCursorResult(config.usrTypeMappings[usr]!); + } // Get name from supported typedef name if config allows. if (config.useSupportedTypedefs) { if (suportedTypedefToSuportedNativeType.containsKey(spelling)) { @@ -249,6 +254,7 @@ if (cursorKind == clang_types.CXCursorKind.CXCursor_StructDecl || cursorKind == clang_types.CXCursorKind.CXCursor_UnionDecl) { final declSpelling = cursor.spelling(); + final declUsr = cursor.usr(); // Set includer functions according to compoundType. final CompoundType compoundType; @@ -268,10 +274,12 @@ } // Also add a struct binding, if its unseen. - // TODO(23): Check if we should auto add compound declarations. if (compoundTypeMappings.containsKey(declSpelling)) { _logger.fine(' Type Mapped from type-map'); return compoundTypeMappings[declSpelling]!; + } else if (config.usrTypeMappings.containsKey(declUsr)) { + _logger.fine(' Type Mapped from usr'); + return config.usrTypeMappings[declUsr]!; } else { final struct = parseCompoundDeclaration( cursor,
diff --git a/pkgs/ffigen/lib/src/strings.dart b/pkgs/ffigen/lib/src/strings.dart index 99d10c2..4c99ba4 100644 --- a/pkgs/ffigen/lib/src/strings.dart +++ b/pkgs/ffigen/lib/src/strings.dart
@@ -29,6 +29,10 @@ const output = 'output'; +// Sub-keys of output. +const bindings = "bindings"; +const symbolFile = 'symbol-file'; + const language = 'language'; // String mappings for the Language enum. @@ -131,6 +135,29 @@ // Library imports. const libraryImports = 'library-imports'; +// Sub Keys of symbol file. +const symbols = 'symbols'; + +// Symbol file yaml. +const formatVersion = "format_version"; + +/// Current symbol file format version. +/// +/// This is generated when generating any symbol file. When importing any other +/// symbol file, this version is compared according to `semantic` versioning +/// to determine compatibility. +const symbolFileFormatVersion = "1.0.0"; +const files = "files"; +const usedConfig = "used-config"; + +const import = 'import'; +const defaultSymbolFileImportPrefix = '_imp'; + +// Sub keys of import. +const symbolFilesImport = 'symbol-files'; +// Sub-Sub keys of symbolFilesImport. +const importPath = 'import-path'; + final predefinedLibraryImports = { ffiImport.name: ffiImport, ffiPkgImport.name: ffiPkgImport
diff --git a/pkgs/ffigen/pubspec.yaml b/pkgs/ffigen/pubspec.yaml index c13dfb5..1608ea8 100644 --- a/pkgs/ffigen/pubspec.yaml +++ b/pkgs/ffigen/pubspec.yaml
@@ -3,7 +3,7 @@ # BSD-style license that can be found in the LICENSE file. name: ffigen -version: 7.1.0 +version: 7.2.0 description: Generator for FFI bindings, using LibClang to parse C header files. repository: https://github.com/dart-lang/ffigen @@ -20,6 +20,8 @@ cli_util: ^0.3.0 glob: ^2.0.0 file: ^6.0.0 + package_config: ^2.1.0 + yaml_edit: ^2.0.3 dev_dependencies: lints: ^1.0.1
diff --git a/pkgs/ffigen/test/example_tests/shared_bindings_example_test.dart b/pkgs/ffigen/test/example_tests/shared_bindings_example_test.dart new file mode 100644 index 0000000..290ea40 --- /dev/null +++ b/pkgs/ffigen/test/example_tests/shared_bindings_example_test.dart
@@ -0,0 +1,67 @@ +// Copyright (c) 2022, 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. + +import 'package:ffigen/src/config_provider/config.dart'; +import 'package:ffigen/src/header_parser.dart'; +import 'package:ffigen/src/strings.dart' as strings; +import 'package:logging/logging.dart'; +import 'package:test/test.dart'; +import 'package:yaml/yaml.dart'; + +import '../test_utils.dart'; + +void main() { + group('shared_bindings_example', () { + setUpAll(() { + logWarnings(Level.SEVERE); + }); + + test('a_shared_base bindings', () { + final config = Config.fromYaml(loadYaml(''' +${strings.name}: NativeLibraryASharedB +${strings.description}: Bindings to `headers/a.h` with shared definitions from `headers/base.h`. +${strings.output}: 'lib/generated/a_shared_b_gen.dart' +${strings.headers}: + ${strings.entryPoints}: + - 'example/shared_bindings/headers/a.h' +${strings.import}: + ${strings.symbolFilesImport}: + - 'example/shared_bindings/lib/generated/base_symbols.yaml' +${strings.preamble}: | + // ignore_for_file: non_constant_identifier_names, camel_case_types +''') as YamlMap); + final library = parse(config); + + matchLibraryWithExpected( + library, + 'example_shared_bindings.dart', + ['example', 'shared_bindings', config.output], + ); + }); + + test('base symbol file output', () { + final config = Config.fromYaml(loadYaml(''' +${strings.name}: NativeLibraryBase +${strings.description}: Bindings to `headers/base.h`. +${strings.output}: + ${strings.bindings}: 'lib/generated/base_gen.dart' + ${strings.symbolFile}: + ${strings.output}: 'lib/generated/base_symbols.yaml' + ${strings.importPath}: 'package:shared_bindings/generated/base_gen.dart' +${strings.headers}: + ${strings.entryPoints}: + - 'example/shared_bindings/headers/base.h' +${strings.preamble}: | + // ignore_for_file: non_constant_identifier_names, camel_case_types +''') as YamlMap); + final library = parse(config); + matchLibrarySymbolFileWithExpected( + library, + 'example_shared_bindings.yaml', + ['example', 'shared_bindings', config.symbolFile!.output], + config.symbolFile!.importPath, + ); + }); + }); +}
diff --git a/pkgs/ffigen/test/example_tests/swift_example_test.dart b/pkgs/ffigen/test/example_tests/swift_example_test.dart index 151b895..05c854c 100644 --- a/pkgs/ffigen/test/example_tests/swift_example_test.dart +++ b/pkgs/ffigen/test/example_tests/swift_example_test.dart
@@ -46,8 +46,8 @@ final pubspecFile = File('example/swift/pubspec.yaml'); final pubspecYaml = loadYaml(pubspecFile.readAsStringSync()) as YamlMap; - final config = - Config.fromYaml(pubspecYaml['ffigen'] as YamlMap, pubspecFile.path); + final config = Config.fromYaml(pubspecYaml['ffigen'] as YamlMap, + filename: pubspecFile.path); final output = parse(config).generate(); // Verify that the output contains all the methods and classes that the
diff --git a/pkgs/ffigen/test/test_utils.dart b/pkgs/ffigen/test/test_utils.dart index 8aa8b44..93ea295 100644 --- a/pkgs/ffigen/test/test_utils.dart +++ b/pkgs/ffigen/test/test_utils.dart
@@ -49,17 +49,52 @@ return codeNormalizer(noCR); } -/// Generates actual file using library and tests using [expect] with expected +/// Generates actual file using library and tests using [expect] with expected. /// /// This will not delete the actual debug file incase [expect] throws an error. void matchLibraryWithExpected( Library library, String pathForActual, List<String> pathToExpected, {String Function(String)? codeNormalizer}) { + _matchFileWithExpected( + library: library, + pathForActual: pathForActual, + pathToExpected: pathToExpected, + fileWriter: ({required Library library, required File file}) => + library.generateFile(file), + codeNormalizer: codeNormalizer, + ); +} + +/// Generates actual file using library and tests using [expect] with expected. +/// +/// This will not delete the actual debug file incase [expect] throws an error. +void matchLibrarySymbolFileWithExpected(Library library, String pathForActual, + List<String> pathToExpected, String importPath) { + _matchFileWithExpected( + library: library, + pathForActual: pathForActual, + pathToExpected: pathToExpected, + fileWriter: ({required Library library, required File file}) { + if (!library.writer.canGenerateSymbolOutput) library.generate(); + library.generateSymbolOutputFile(file, importPath); + }); +} + +/// Generates actual file using library and tests using [expect] with expected. +/// +/// This will not delete the actual debug file incase [expect] throws an error. +void _matchFileWithExpected({ + required Library library, + required String pathForActual, + required List<String> pathToExpected, + required void Function({required Library library, required File file}) + fileWriter, + String Function(String)? codeNormalizer, +}) { final file = File( path.join(strings.tmpDir, pathForActual), ); - library.generateFile(file); - + fileWriter(library: library, file: file); try { final actual = _normalizeGeneratedCode(file.readAsStringSync(), codeNormalizer);