diff --git a/pkgs/ffigen/CHANGELOG.md b/pkgs/ffigen/CHANGELOG.md index 8abf86235..b58742a4c 100644 --- a/pkgs/ffigen/CHANGELOG.md +++ b/pkgs/ffigen/CHANGELOG.md @@ -1,3 +1,8 @@ +## 12.0.1 + +- ffigen now only regenerates the dart bindings only if input (the `config.yaml` +and header files) mtimes is greater than the output mtimes. + ## 12.0.0-wip - Global variables are now compatible with the `ffi-native` option. diff --git a/pkgs/ffigen/lib/src/executables/ffigen.dart b/pkgs/ffigen/lib/src/executables/ffigen.dart index 1c6008592..ba1688932 100644 --- a/pkgs/ffigen/lib/src/executables/ffigen.dart +++ b/pkgs/ffigen/lib/src/executables/ffigen.dart @@ -56,10 +56,19 @@ void main(List args) async { final library = parse(config); // Generate file for the parsed bindings. - final gen = File(config.output); - library.generateFile(gen); - _logger - .info(successPen('Finished, Bindings generated in ${gen.absolute.path}')); + final fileToBeGenerated = File(config.output); + + // Consider regenif config has been updated + final bool inputHasBeenUpdated = _inputHasBeenUpdated( + fileToBeGenerated, [config.filename!, ...config.headers.entryPoints]); + if (!inputHasBeenUpdated) { + _logger.info('Bindings are up-to-date. No changes to headers detected.'); + return; + } + + library.generateFile(fileToBeGenerated); + _logger.info(successPen( + 'Finished, Bindings generated in ${fileToBeGenerated.absolute.path}')); if (config.symbolFile != null) { final symbolFileGen = File(config.symbolFile!.output); @@ -70,6 +79,25 @@ void main(List args) async { } } +bool _inputHasBeenUpdated( + File fileToBeGenerated, List configAndHeaders) { + // if file does not exist, consider it needs to be generated. + if (!fileToBeGenerated.existsSync()) { + _logger.info('Bindings file does not exist, generating bindings.'); + return true; + } + + for (final configOrHeader in configAndHeaders) { + final headerMTime = File(configOrHeader).lastModifiedSync(); + + if (fileToBeGenerated.existsSync() && + headerMTime.isAfter(fileToBeGenerated.lastModifiedSync())) { + return true; // file needs to be regenerated. + } + } + return false; +} + Config getConfig(ArgResults result, PackageConfig? packageConfig) { _logger.info('Running in ${Directory.current}'); Config config; diff --git a/pkgs/ffigen/pubspec.yaml b/pkgs/ffigen/pubspec.yaml index 006cc8c9f..9b076b428 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: 12.0.0-wip +version: 12.0.1 description: > Generator for FFI bindings, using LibClang to parse C, Objective-C, and Swift files. diff --git a/pkgs/ffigen/test/code_generator_tests/ffigen_smart_regen/config.yaml b/pkgs/ffigen/test/code_generator_tests/ffigen_smart_regen/config.yaml new file mode 100644 index 000000000..91c27bb1a --- /dev/null +++ b/pkgs/ffigen/test/code_generator_tests/ffigen_smart_regen/config.yaml @@ -0,0 +1,11 @@ +# yaml-language-server: $schema=../../ffigen.schema.json + +output: 'generated_bindings.dart' +name: 'OnlyRegenIfUpdated' +description: 'Holds bindings to the test header files.' +headers: + entry-points: + - 'test/code_generator_tests/ffigen_smart_regen/headers/header1.h' + include-directives: + - '**header*.h' +comments: true \ No newline at end of file diff --git a/pkgs/ffigen/test/code_generator_tests/ffigen_smart_regen/generated_bindings.dart b/pkgs/ffigen/test/code_generator_tests/ffigen_smart_regen/generated_bindings.dart new file mode 100644 index 000000000..3adb54e60 --- /dev/null +++ b/pkgs/ffigen/test/code_generator_tests/ffigen_smart_regen/generated_bindings.dart @@ -0,0 +1,4 @@ +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +// ignore_for_file: type=lint diff --git a/pkgs/ffigen/test/code_generator_tests/ffigen_smart_regen/headers/file.c b/pkgs/ffigen/test/code_generator_tests/ffigen_smart_regen/headers/file.c new file mode 100644 index 000000000..74803a138 --- /dev/null +++ b/pkgs/ffigen/test/code_generator_tests/ffigen_smart_regen/headers/file.c @@ -0,0 +1,21 @@ +#include "header1.h" + +int doNothingToStruct1(Color dumInput) { + return 0; +} + +int doNothingToStruct2(Vertex dumInput) { + return 0; +} + +int doNothingToStruct3(Vector2 dumInput) { + return 0; +} + +int doNothingToStruct4(Vector3 dumInput) { + return 0; +} + +int doNothingToStruct5(Vector4 dumInput) { + return 0; +} \ No newline at end of file diff --git a/pkgs/ffigen/test/code_generator_tests/ffigen_smart_regen/headers/header1.h b/pkgs/ffigen/test/code_generator_tests/ffigen_smart_regen/headers/header1.h new file mode 100644 index 000000000..7add41101 --- /dev/null +++ b/pkgs/ffigen/test/code_generator_tests/ffigen_smart_regen/headers/header1.h @@ -0,0 +1,35 @@ +typedef struct { + int m_red; + int m_green; + int m_blue; +} Color; + + +typedef struct { + float x, y, z; + Color color; +} Vertex; + +typedef struct { + float x, y; +} Vector2; + +typedef struct { + float x, y, z; +} Vector3; + +typedef struct { + float x, y, z, w; +} Vector4; + +typedef struct { + float m[4][4]; +} Matrix4x4; + +int doNothing(double dumInput, unsigned int dumInput2) { + return 0; +} + +int doNothingToStruct6(Matrix4x4 dumInput) { + return 0; +} diff --git a/pkgs/ffigen/test/code_generator_tests/smart_regen_test.dart b/pkgs/ffigen/test/code_generator_tests/smart_regen_test.dart new file mode 100644 index 000000000..37f90b831 --- /dev/null +++ b/pkgs/ffigen/test/code_generator_tests/smart_regen_test.dart @@ -0,0 +1,143 @@ +// Copyright (c) 2024, 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 'dart:io'; + +import 'package:ffigen/ffigen.dart'; +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +import '../test_utils.dart'; + +final _testDirectoryPath = + path.join("test", "code_generator_tests", "ffigen_smart_regen"); +final _configYamlPath = path.join(_testDirectoryPath, "config.yaml"); +final _headerFiles = Directory(path.join(_testDirectoryPath, "headers")) + .listSync() + .map((e) => e.path) + .where((e) => e.endsWith('.h') || e.endsWith('.c')); +final _configAndHeaders = [ + _configYamlPath, + ..._headerFiles, +]; + +void main() { + group('_inputHasBeenUpdated', () { + test('should return true if file does not exist', () { + _cleanUp(); + + final fileToBeGenerated = + File(path.join(_testDirectoryPath, "generated_bindings.dart")); + + final result = _inputHasBeenUpdated(fileToBeGenerated, _configAndHeaders); + + expect(result, isTrue); + }); + + test( + 'should return true if the config or any header file is newer than the generated file', + () { + _ensureGeneratedBindings(); + final fileToBeGenerated = + File(path.join(_testDirectoryPath, "generated_bindings.dart")); + + // Set the last modified time of the generated file to be older than the header files + fileToBeGenerated.setLastModifiedSync(DateTime(2022, 1, 1)); + + // Set the last modified time of one of the header files to be newer than the generated file + final headerFile = + File(path.join(_testDirectoryPath, 'headers', 'header1.h')); + headerFile.setLastModifiedSync(DateTime(2023, 1, 1)); + + final result = _inputHasBeenUpdated(fileToBeGenerated, _configAndHeaders); + + expect(result, isTrue); + }); + + test('should return true if the config is newer than the generated file', + () { + _ensureGeneratedBindings(); + final fileToBeGenerated = + File(path.join(_testDirectoryPath, "generated_bindings.dart")); + + // Set the last modified time of the generated file to be older than the header files + fileToBeGenerated.setLastModifiedSync(DateTime(2022, 1, 1)); + + // Set the last modified time of the config file to be newer than the generated file + final configFile = File(_configYamlPath); + configFile.setLastModifiedSync(DateTime(2023, 1, 1)); + + final result = _inputHasBeenUpdated(fileToBeGenerated, _configAndHeaders); + + expect(result, isTrue); + }); + + test( + 'should return false if the config and header files are all older than the generated file', + () { + _ensureGeneratedBindings(); + final fileToBeGenerated = + File(path.join(_testDirectoryPath, "generated_bindings.dart")); + + // Set the last modified time of the generated file to be older than the header files + fileToBeGenerated.setLastModifiedSync(DateTime.now()); + + final configFile = File(_configYamlPath); + configFile.setLastModifiedSync(DateTime(2021, 1, 1)); + + // Set the last modified time of the header files to be newer than the generated file + final headerFile1 = + File(path.join(_testDirectoryPath, 'headers', 'header1.h')); + headerFile1.setLastModifiedSync(DateTime(2023, 1, 1)); + + final headerFile2 = + File(path.join(_testDirectoryPath, 'headers', 'file.c')); + headerFile2.setLastModifiedSync(DateTime(2023, 1, 1)); + + final result = _inputHasBeenUpdated(fileToBeGenerated, _configAndHeaders); + + expect(result, isFalse); + }); + + _cleanUp(); + }); +} + +/// Returns true if the file needs to be regenerated. + +bool _inputHasBeenUpdated( + File fileToBeGenerated, List configAndHeaders) { + // if file does not exist, consider it needs to be generated. + if (!fileToBeGenerated.existsSync()) { + return true; + } + + for (final configOrHeader in configAndHeaders) { + final headerMTime = File(configOrHeader).lastModifiedSync(); + + if (fileToBeGenerated.existsSync() && + headerMTime.isAfter(fileToBeGenerated.lastModifiedSync())) { + return true; // file needs to be regenerated. + } + } + return false; +} + +void _ensureGeneratedBindings() { + final config = testConfigFromPath(path.join( + 'test', 'code_generator_tests', 'ffigen_smart_regen', 'config.yaml')); + final library = parse(config); + // Generate file for the parsed bindings. + final fileToBeGenerated = File(config.output); + + library.generateFile(fileToBeGenerated); +} + +void _cleanUp() { + final genFile = + File(path.join(_testDirectoryPath, "generated_bindings.dart")); + if (genFile.existsSync()) { + genFile.deleteSync(); + } +}