Skip to content

[ffigen] ffigen only regenerates if input mtimes > output mtimes (#874) #977

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pkgs/ffigen/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
36 changes: 32 additions & 4 deletions pkgs/ffigen/lib/src/executables/ffigen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,19 @@ void main(List<String> 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);
Expand All @@ -70,6 +79,25 @@ void main(List<String> args) async {
}
}

bool _inputHasBeenUpdated(
File fileToBeGenerated, List<String> 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;
Expand Down
2 changes: 1 addition & 1 deletion pkgs/ffigen/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// AUTO GENERATED FILE, DO NOT EDIT.
//
// Generated by `package:ffigen`.
// ignore_for_file: type=lint
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
143 changes: 143 additions & 0 deletions pkgs/ffigen/test/code_generator_tests/smart_regen_test.dart
Original file line number Diff line number Diff line change
@@ -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<String> 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();
}
}