From 4cb3ad768de7e7a0e84cb3b570116c8b3422301b Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 7 Feb 2026 00:13:55 +0100 Subject: [PATCH 01/32] remove devcontainer --- .devcontainer/Dockerfile | 15 --------------- .devcontainer/devcontainer.json | 33 --------------------------------- 2 files changed, 48 deletions(-) delete mode 100644 .devcontainer/Dockerfile delete mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 034392e..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM mcr.microsoft.com/devcontainers/base:debian - -# Install needed packages -RUN apt-get update && apt-get install -y curl git unzip xz-utils zip - -USER 1000:1000 - -ARG FLUTTER_VERSION=3.22.1 -RUN git clone -b $FLUTTER_VERSION https://github.com/flutter/flutter.git /home/vscode/flutter - -ENV PATH /home/vscode/flutter/bin:/home/vscode/.pub-cache/bin:$PATH - -RUN flutter precache - -USER root diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index d2721df..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,33 +0,0 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/debian -{ - "name": "Debian", - - // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "build": { - // Path is relative to the devcontainer.json file. - "dockerfile": "Dockerfile" - }, - - // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - - // Configure tool-specific properties. - // "customizations": {}, - - "customizations": { - "vscode": { - "extensions": [ - "Dart-Code.flutter" - ] - } - }, - - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" - - "postStartCommand": "flutter pub global activate -spath /workspaces/flutterpi_tool" -} From 4d871894c626593a5bdcaf503bc4199970878f1d Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 7 Feb 2026 00:22:28 +0100 Subject: [PATCH 02/32] improve windows testing build all test apps on windows as well. run all tests on macos, windows and linux. --- .github/workflows/build-app.yml | 20 +++++++++------ .github/workflows/flutter.yml | 43 ++++++++++++++++++++++++--------- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build-app.yml b/.github/workflows/build-app.yml index 9603939..d980a51 100644 --- a/.github/workflows/build-app.yml +++ b/.github/workflows/build-app.yml @@ -2,19 +2,25 @@ name: Build Test App on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] permissions: contents: read +env: + FLUTTER_VERSION: 3.38.4 + jobs: build: name: Build Flutter-Pi Bundle (${{ matrix.arch }}, ${{ matrix.cpu}}) - runs-on: ubuntu-latest + runs-on: ${{ matrix.runner }} strategy: matrix: + runner: + - windows-latest + - ubuntu-latest arch: - arm - arm64 @@ -33,13 +39,13 @@ jobs: cpu: pi4 steps: - uses: actions/checkout@v4 - + - uses: subosito/flutter-action@v2 with: cache: true channel: stable - flutter-version: 3.38.4 - + flutter-version: ${{ env.FLUTTER_VERSION }} + - name: Install dependencies & Activate as global executable run: | flutter pub get @@ -60,7 +66,7 @@ jobs: echo '::group::flutterpi_tool build ... --debug' flutterpi_tool build --arch=${{ matrix.arch }} --cpu=${{ matrix.cpu }} --debug --debug-symbols --verbose echo '::endgroup::' - + echo '::group::flutterpi_tool build ... --profile' flutterpi_tool build --arch=${{ matrix.arch }} --cpu=${{ matrix.cpu }} --profile --debug-symbols --verbose echo '::endgroup::' diff --git a/.github/workflows/flutter.yml b/.github/workflows/flutter.yml index cabccc1..aa1a49f 100644 --- a/.github/workflows/flutter.yml +++ b/.github/workflows/flutter.yml @@ -7,39 +7,60 @@ name: Flutter on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] permissions: contents: read +env: + FLUTTER_VERSION: 3.38.4 + jobs: - build: + analyze: runs-on: ubuntu-latest env: GITHUB_TOKEN: ${{ secrets.TOKEN }} steps: - uses: actions/checkout@v4 - + - uses: subosito/flutter-action@v2 with: cache: true channel: stable - flutter-version: 3.38.4 - + flutter-version: ${{ env.FLUTTER_VERSION }} + - name: Install dependencies run: flutter pub get - + - name: Verify formatting run: dart format --output=none --set-exit-if-changed . - # Consider passing '--fatal-infos' for slightly stricter analysis. - name: Analyze project source run: flutter analyze - # Your project will need to have tests in test/ and a dependency on - # package:test for this step to succeed. Note that Flutter projects will - # want to change this to 'flutter test'. + test: + runs-on: ${{ matrix.runner }} + env: + GITHUB_TOKEN: ${{ secrets.token }} + strategy: + matrix: + runner: + - windows-latest + - ubuntu-latest + - macos-latest + steps: + - uses: actions/checkout@v4 + + - uses: subosito/flutter-action@v2 + with: + cache: true + channel: stable + flutter-version: ${{ env.FLUTTER_VERSION }} + + - name: Install dependencies + run: flutter pub get + - name: Run tests run: flutter test From 855ea0a17b2824645eca2719cf629da78a16b2da Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 7 Feb 2026 00:28:44 +0100 Subject: [PATCH 03/32] Update build-app.yml --- .github/workflows/build-app.yml | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-app.yml b/.github/workflows/build-app.yml index d980a51..aa42459 100644 --- a/.github/workflows/build-app.yml +++ b/.github/workflows/build-app.yml @@ -28,14 +28,32 @@ jobs: - riscv64 cpu: - generic + + # TODO: Maybe find a better way to do this. include: - - arch: arm + - runner: windows-latest + arch: arm + cpu: pi3 + - runner: ubuntu-latest + arch: arm cpu: pi3 - - arch: arm + - runner: windows-latest + arch: arm + cpu: pi4 + - runner: ubuntu-latest + arch: arm cpu: pi4 - - arch: arm64 + - runner: windows-latest + arch: arm64 cpu: pi3 - - arch: arm64 + - runner: ubuntu-latest + arch: arm64 + cpu: pi3 + - runner: windows-latest + arch: arm64 + cpu: pi4 + - runner: ubuntu-latest + arch: arm64 cpu: pi4 steps: - uses: actions/checkout@v4 From 7b67fee4b36ac7a96177d456e59b88aa4158526d Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 7 Feb 2026 00:30:10 +0100 Subject: [PATCH 04/32] mention OS in job name --- .github/workflows/build-app.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-app.yml b/.github/workflows/build-app.yml index aa42459..b05c6db 100644 --- a/.github/workflows/build-app.yml +++ b/.github/workflows/build-app.yml @@ -14,7 +14,7 @@ env: jobs: build: - name: Build Flutter-Pi Bundle (${{ matrix.arch }}, ${{ matrix.cpu}}) + name: Build Flutter-Pi Bundle (${{ matrix.runner}}, ${{ matrix.arch }}, ${{ matrix.cpu}}) runs-on: ${{ matrix.runner }} strategy: matrix: From dee2b1ec4f08d68e3ab52fbe86253bf11be41e38 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 7 Feb 2026 00:39:58 +0100 Subject: [PATCH 05/32] app builder test: replace all literal path creations with path.join --- test/app_builder_test.dart | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/test/app_builder_test.dart b/test/app_builder_test.dart index 609194d..aa70af5 100644 --- a/test/app_builder_test.dart +++ b/test/app_builder_test.dart @@ -123,7 +123,7 @@ void main() { }) async { expect( environment.defines[fl.kTargetFile], - equals('lib/main_flutterpi.dart'), + equals(p.join('lib', 'main_flutterpi.dart')), ); buildWasCalled = true; @@ -136,7 +136,7 @@ void main() { target: FlutterpiTargetPlatform.genericRiscv64, buildInfo: fl.BuildInfo.debug, fsLayout: FilesystemLayout.flutterPi, - mainPath: 'lib/main_flutterpi.dart', + mainPath: p.join('lib', 'main_flutterpi.dart'), ), ); @@ -268,7 +268,8 @@ void main() { }) async { expect( environment.outputDir.path, - equals('build/flutter-pi/meta-flutter-riscv64-generic'), + equals( + p.join('build', 'flutter-pi', 'meta-flutter-riscv64-generic')), ); buildWasCalled = true; @@ -375,7 +376,7 @@ void main() { }) async { expect( environment.outputDir.path, - equals('build/flutter-pi/riscv64-generic'), + equals(p.join('build', 'flutter-pi', 'riscv64-generic')), ); buildWasCalled = true; @@ -491,8 +492,8 @@ void main() { expect( bundle.binaries.map( - (file) => - p.relative(file.path, from: 'build/flutter-pi/riscv64-generic'), + (file) => p.relative(file.path, + from: p.join('build', 'flutter-pi', 'riscv64-generic')), ), unorderedEquals([ 'flutter-pi', @@ -525,8 +526,8 @@ void main() { expect( bundle.binaries.map( - (file) => - p.relative(file.path, from: 'build/flutter-pi/riscv64-generic'), + (file) => p.relative(file.path, + from: p.join('build', 'flutter-pi', 'riscv64-generic')), ), unorderedEquals([ 'flutter-pi', @@ -560,11 +561,11 @@ void main() { bundle.binaries.map( (file) => p.relative( file.path, - from: 'build/flutter-pi/meta-flutter-riscv64-generic', + from: p.join('build', 'flutter-pi', 'meta-flutter-riscv64-generic'), ), ), unorderedEquals([ - 'lib/libflutter_engine.so', + p.join('lib', 'libflutter_engine.so'), ]), ); }); @@ -595,12 +596,12 @@ void main() { bundle.binaries.map( (file) => p.relative( file.path, - from: 'build/flutter-pi/meta-flutter-riscv64-generic', + from: p.join('build', 'flutter-pi', 'meta-flutter-riscv64-generic'), ), ), unorderedEquals([ - 'bin/flutter-pi', - 'lib/libflutter_engine.so', + p.join('bin', 'flutter-pi'), + p.join('lib', 'libflutter_engine.so'), ]), ); }); @@ -631,12 +632,12 @@ void main() { bundle.binaries.map( (file) => p.relative( file.path, - from: 'build/flutter-pi/meta-flutter-riscv64-generic', + from: p.join('build', 'flutter-pi', 'meta-flutter-riscv64-generic'), ), ), unorderedEquals([ - 'lib/libflutter_engine.dbgsyms', - 'lib/libflutter_engine.so', + p.join('lib', 'libflutter_engine.dbgsyms'), + p.join('lib', 'libflutter_engine.so'), ]), ); }); From 3c6dd69036f3607cd88acdaf4942254d9a4b6aca Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 7 Feb 2026 00:45:32 +0100 Subject: [PATCH 06/32] use GITHUB_TOKEN from environment to authenticate github requests --- lib/src/context.dart | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/lib/src/context.dart b/lib/src/context.dart index 10560fe..b0eee14 100644 --- a/lib/src/context.dart +++ b/lib/src/context.dart @@ -7,6 +7,7 @@ import 'package:flutterpi_tool/src/build_system/build_app.dart'; import 'package:flutterpi_tool/src/config.dart'; import 'package:flutterpi_tool/src/devices/device_manager.dart'; import 'package:flutterpi_tool/src/devices/flutterpi_ssh/ssh_utils.dart'; +import 'package:github/github.dart' as gh; import 'package:unified_analytics/unified_analytics.dart'; import 'package:http/io_client.dart' as http; @@ -28,20 +29,28 @@ Future runInContext( overrides: { Analytics: () => const NoOpAnalytics(), fl.TemplateRenderer: () => const fl.MustacheTemplateRenderer(), - fl.Cache: () => FlutterpiCache( - hooks: globals.shutdownHooks, - logger: globals.logger, - fileSystem: globals.fs, - platform: globals.platform, - osUtils: globals.os as MoreOperatingSystemUtils, - projectFactory: globals.projectFactory, - processManager: globals.processManager, - github: MyGithub.caching( - httpClient: http.IOClient( - globals.httpClientFactory?.call() ?? io.HttpClient(), - ), + fl.Cache: () { + final auth = switch (globals.platform.environment['GITHUB_TOKEN']) { + final token? => gh.Authentication.bearerToken(token), + _ => null, + }; + + return FlutterpiCache( + hooks: globals.shutdownHooks, + logger: globals.logger, + fileSystem: globals.fs, + platform: globals.platform, + osUtils: globals.os as MoreOperatingSystemUtils, + projectFactory: globals.projectFactory, + processManager: globals.processManager, + github: MyGithub.caching( + httpClient: http.IOClient( + globals.httpClientFactory?.call() ?? io.HttpClient(), ), + auth: auth, ), + ); + }, fl.OperatingSystemUtils: () => MoreOperatingSystemUtils( fileSystem: globals.fs, logger: globals.logger, From b951a6d9e4b7f523846a2fb29bfb3229219e95fb Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 7 Feb 2026 00:48:25 +0100 Subject: [PATCH 07/32] fix missing trailing commas --- test/app_builder_test.dart | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/test/app_builder_test.dart b/test/app_builder_test.dart index aa70af5..f26b202 100644 --- a/test/app_builder_test.dart +++ b/test/app_builder_test.dart @@ -269,7 +269,8 @@ void main() { expect( environment.outputDir.path, equals( - p.join('build', 'flutter-pi', 'meta-flutter-riscv64-generic')), + p.join('build', 'flutter-pi', 'meta-flutter-riscv64-generic'), + ), ); buildWasCalled = true; @@ -492,8 +493,10 @@ void main() { expect( bundle.binaries.map( - (file) => p.relative(file.path, - from: p.join('build', 'flutter-pi', 'riscv64-generic')), + (file) => p.relative( + file.path, + from: p.join('build', 'flutter-pi', 'riscv64-generic'), + ), ), unorderedEquals([ 'flutter-pi', @@ -526,8 +529,10 @@ void main() { expect( bundle.binaries.map( - (file) => p.relative(file.path, - from: p.join('build', 'flutter-pi', 'riscv64-generic')), + (file) => p.relative( + file.path, + from: p.join('build', 'flutter-pi', 'riscv64-generic'), + ), ), unorderedEquals([ 'flutter-pi', From b8eb5fdf07dc79985a4cd2f5f4cb5c294b10eae8 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 7 Feb 2026 01:05:21 +0100 Subject: [PATCH 08/32] remove explicit permissions --- .github/workflows/build-app.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/build-app.yml b/.github/workflows/build-app.yml index b05c6db..d9f128a 100644 --- a/.github/workflows/build-app.yml +++ b/.github/workflows/build-app.yml @@ -6,9 +6,6 @@ on: pull_request: branches: ["main"] -permissions: - contents: read - env: FLUTTER_VERSION: 3.38.4 From aa37e3de5a08083f3822d50611644fcda6c6cf13 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 7 Feb 2026 01:11:24 +0100 Subject: [PATCH 09/32] use public-only PAT to authenticate GH requests --- .github/workflows/build-app.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-app.yml b/.github/workflows/build-app.yml index d9f128a..ab6becb 100644 --- a/.github/workflows/build-app.yml +++ b/.github/workflows/build-app.yml @@ -72,7 +72,7 @@ jobs: - name: Run flutterpi_tool build working-directory: test_app env: - GITHUB_TOKEN: ${{ secrets.TOKEN }} + GITHUB_TOKEN: ${{ secrets.PUBLIC_ONLY_PAT }} run: | echo '::group::flutterpi_tool build ... --debug-unoptimized' flutterpi_tool build --arch=${{ matrix.arch }} --cpu=${{ matrix.cpu }} --debug-unoptimized --debug-symbols --verbose From 0752797255ec04e706fe0448664b697c5698270e Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 7 Feb 2026 14:43:21 +0100 Subject: [PATCH 10/32] split every fs test into a windows and posix test --- test/app_builder_test.dart | 175 ++++++++++++++++++++++++++----------- 1 file changed, 126 insertions(+), 49 deletions(-) diff --git a/test/app_builder_test.dart b/test/app_builder_test.dart index f26b202..c104d93 100644 --- a/test/app_builder_test.dart +++ b/test/app_builder_test.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutterpi_tool/src/build_system/targets.dart'; import 'package:flutterpi_tool/src/cli/flutterpi_command.dart'; @@ -114,33 +115,66 @@ void main() { expect(buildWasCalled, isTrue); }); - test('passes target path correctly', () async { - var buildWasCalled = false; - buildSystem.buildFn = ( - fl.Target target, - fl.Environment environment, { - fl.BuildSystemConfig buildSystemConfig = const fl.BuildSystemConfig(), - }) async { - expect( - environment.defines[fl.kTargetFile], - equals(p.join('lib', 'main_flutterpi.dart')), + group('passes target path correctly', () { + test('posix', () async { + var buildWasCalled = false; + buildSystem.buildFn = ( + fl.Target target, + fl.Environment environment, { + fl.BuildSystemConfig buildSystemConfig = const fl.BuildSystemConfig(), + }) async { + expect( + environment.defines[fl.kTargetFile], + equals(p.posix.join('lib', 'main_flutterpi.dart')), + ); + + buildWasCalled = true; + return fl.BuildResult(success: true); + }; + + await _runInTestContext( + () async => await appBuilder.build( + host: FlutterpiHostPlatform.linuxRV64, + target: FlutterpiTargetPlatform.genericRiscv64, + buildInfo: fl.BuildInfo.debug, + fsLayout: FilesystemLayout.flutterPi, + mainPath: p.posix.join('lib', 'main_flutterpi.dart'), + ), ); - buildWasCalled = true; - return fl.BuildResult(success: true); - }; + expect(buildWasCalled, isTrue); + }); - await _runInTestContext( - () async => await appBuilder.build( - host: FlutterpiHostPlatform.linuxRV64, - target: FlutterpiTargetPlatform.genericRiscv64, - buildInfo: fl.BuildInfo.debug, - fsLayout: FilesystemLayout.flutterPi, - mainPath: p.join('lib', 'main_flutterpi.dart'), - ), - ); + test('windows', () async { + fs = MemoryFileSystem.test(style: FileSystemStyle.windows); - expect(buildWasCalled, isTrue); + var buildWasCalled = false; + buildSystem.buildFn = ( + fl.Target target, + fl.Environment environment, { + fl.BuildSystemConfig buildSystemConfig = const fl.BuildSystemConfig(), + }) async { + expect( + environment.defines[fl.kTargetFile], + equals(p.windows.join('lib', 'main_flutterpi.dart')), + ); + + buildWasCalled = true; + return fl.BuildResult(success: true); + }; + + await _runInTestContext( + () async => await appBuilder.build( + host: FlutterpiHostPlatform.linuxRV64, + target: FlutterpiTargetPlatform.genericRiscv64, + buildInfo: fl.BuildInfo.debug, + fsLayout: FilesystemLayout.flutterPi, + mainPath: p.windows.join('lib', 'main_flutterpi.dart'), + ), + ); + + expect(buildWasCalled, isTrue); + }); }); group('--fs-layout', () { @@ -259,36 +293,79 @@ void main() { expect(bundle.includesFlutterpiBinary, isTrue); }); - test('default output directory is build/-meta-flutter', () async { - var buildWasCalled = false; - buildSystem.buildFn = ( - fl.Target target, - fl.Environment environment, { - fl.BuildSystemConfig buildSystemConfig = const fl.BuildSystemConfig(), - }) async { - expect( - environment.outputDir.path, - equals( - p.join('build', 'flutter-pi', 'meta-flutter-riscv64-generic'), + group('default output directory is build/-meta-flutter', () { + test('posix', () async { + var buildWasCalled = false; + buildSystem.buildFn = ( + fl.Target target, + fl.Environment environment, { + fl.BuildSystemConfig buildSystemConfig = + const fl.BuildSystemConfig(), + }) async { + expect( + environment.outputDir.path, + equals( + p.posix.join( + 'build', + 'flutter-pi', + 'meta-flutter-riscv64-generic', + ), + ), + ); + + buildWasCalled = true; + return fl.BuildResult(success: true); + }; + + await _runInTestContext( + () async => await appBuilder.buildBundle( + id: 'test-id', + host: FlutterpiHostPlatform.linuxRV64, + target: FlutterpiTargetPlatform.genericRiscv64, + buildInfo: fl.BuildInfo.debug, + fsLayout: FilesystemLayout.metaFlutter, + forceBundleFlutterpi: true, ), ); - buildWasCalled = true; - return fl.BuildResult(success: true); - }; - - await _runInTestContext( - () async => await appBuilder.buildBundle( - id: 'test-id', - host: FlutterpiHostPlatform.linuxRV64, - target: FlutterpiTargetPlatform.genericRiscv64, - buildInfo: fl.BuildInfo.debug, - fsLayout: FilesystemLayout.metaFlutter, - forceBundleFlutterpi: true, - ), - ); + expect(buildWasCalled, isTrue); + }); + + test('windows', () async { + fs = MemoryFileSystem.test(style: FileSystemStyle.windows); + + var buildWasCalled = false; + buildSystem.buildFn = ( + fl.Target target, + fl.Environment environment, { + fl.BuildSystemConfig buildSystemConfig = + const fl.BuildSystemConfig(), + }) async { + expect( + environment.outputDir.path, + equals( + p.windows.join( + 'build', 'flutter-pi', 'meta-flutter-riscv64-generic'), + ), + ); + + buildWasCalled = true; + return fl.BuildResult(success: true); + }; + + await _runInTestContext( + () async => await appBuilder.buildBundle( + id: 'test-id', + host: FlutterpiHostPlatform.linuxRV64, + target: FlutterpiTargetPlatform.genericRiscv64, + buildInfo: fl.BuildInfo.debug, + fsLayout: FilesystemLayout.metaFlutter, + forceBundleFlutterpi: true, + ), + ); - expect(buildWasCalled, isTrue); + expect(buildWasCalled, isTrue); + }); }); }); From a8fd6b0b69bdbd3ca88e7dd07b631f29daa94c36 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 7 Feb 2026 14:43:22 +0100 Subject: [PATCH 11/32] split every fs test into a windows and posix test --- test/app_builder_test.dart | 587 ++++++++++++++++++++++++++----------- 1 file changed, 413 insertions(+), 174 deletions(-) diff --git a/test/app_builder_test.dart b/test/app_builder_test.dart index c104d93..6a02ae1 100644 --- a/test/app_builder_test.dart +++ b/test/app_builder_test.dart @@ -445,34 +445,74 @@ void main() { expect(buildWasCalled, isTrue); }); - test('default output directory is build/', () async { - var buildWasCalled = false; - buildSystem.buildFn = ( - fl.Target target, - fl.Environment environment, { - fl.BuildSystemConfig buildSystemConfig = const fl.BuildSystemConfig(), - }) async { - expect( - environment.outputDir.path, - equals(p.join('build', 'flutter-pi', 'riscv64-generic')), + group('default output directory is build/', () { + test('posix', () async { + var buildWasCalled = false; + buildSystem.buildFn = ( + fl.Target target, + fl.Environment environment, { + fl.BuildSystemConfig buildSystemConfig = + const fl.BuildSystemConfig(), + }) async { + expect( + environment.outputDir.path, + equals( + p.posix.join('build', 'flutter-pi', 'riscv64-generic'), + ), + ); + + buildWasCalled = true; + return fl.BuildResult(success: true); + }; + + await _runInTestContext( + () async => await appBuilder.buildBundle( + id: 'test-id', + host: FlutterpiHostPlatform.linuxRV64, + target: FlutterpiTargetPlatform.genericRiscv64, + buildInfo: fl.BuildInfo.debug, + fsLayout: FilesystemLayout.flutterPi, + forceBundleFlutterpi: true, + ), ); - buildWasCalled = true; - return fl.BuildResult(success: true); - }; + expect(buildWasCalled, isTrue); + }); - await _runInTestContext( - () async => await appBuilder.buildBundle( - id: 'test-id', - host: FlutterpiHostPlatform.linuxRV64, - target: FlutterpiTargetPlatform.genericRiscv64, - buildInfo: fl.BuildInfo.debug, - fsLayout: FilesystemLayout.flutterPi, - forceBundleFlutterpi: true, - ), - ); + test('windows', () async { + fs = MemoryFileSystem.test(style: FileSystemStyle.windows); - expect(buildWasCalled, isTrue); + var buildWasCalled = false; + buildSystem.buildFn = ( + fl.Target target, + fl.Environment environment, { + fl.BuildSystemConfig buildSystemConfig = + const fl.BuildSystemConfig(), + }) async { + expect( + environment.outputDir.path, + equals( + p.windows.join('build', 'flutter-pi', 'riscv64-generic'), + ), + ); + + buildWasCalled = true; + return fl.BuildResult(success: true); + }; + + await _runInTestContext( + () async => await appBuilder.buildBundle( + id: 'test-id', + host: FlutterpiHostPlatform.linuxRV64, + target: FlutterpiTargetPlatform.genericRiscv64, + buildInfo: fl.BuildInfo.debug, + fsLayout: FilesystemLayout.flutterPi, + forceBundleFlutterpi: true, + ), + ); + + expect(buildWasCalled, isTrue); + }); }); }); }); @@ -548,180 +588,379 @@ void main() { }); group('bundle binaries', () { - test('binary paths for --fs-layout=flutter-pi', () async { - buildSystem.buildFn = ( - fl.Target target, - fl.Environment environment, { - fl.BuildSystemConfig buildSystemConfig = const fl.BuildSystemConfig(), - }) async { - return fl.BuildResult(success: true); - }; + group('binary paths for --fs-layout=flutter-pi', () { + test('posix', () async { + buildSystem.buildFn = ( + fl.Target target, + fl.Environment environment, { + fl.BuildSystemConfig buildSystemConfig = const fl.BuildSystemConfig(), + }) async { + return fl.BuildResult(success: true); + }; - final bundle = await _runInTestContext( - () async => await appBuilder.buildBundle( - id: 'test-id', - host: FlutterpiHostPlatform.linuxRV64, - target: FlutterpiTargetPlatform.genericRiscv64, - buildInfo: fl.BuildInfo.debug, - fsLayout: FilesystemLayout.flutterPi, - forceBundleFlutterpi: false, - ), - ) as PrebuiltFlutterpiAppBundle; + final bundle = await _runInTestContext( + () async => await appBuilder.buildBundle( + id: 'test-id', + host: FlutterpiHostPlatform.linuxRV64, + target: FlutterpiTargetPlatform.genericRiscv64, + buildInfo: fl.BuildInfo.debug, + fsLayout: FilesystemLayout.flutterPi, + forceBundleFlutterpi: false, + ), + ) as PrebuiltFlutterpiAppBundle; - expect( - bundle.binaries.map( - (file) => p.relative( - file.path, - from: p.join('build', 'flutter-pi', 'riscv64-generic'), + expect( + bundle.binaries.map( + (file) => p.posix.relative( + file.path, + from: p.posix.join('build', 'flutter-pi', 'riscv64-generic'), + ), ), - ), - unorderedEquals([ - 'flutter-pi', - 'libflutter_engine.so', - ]), - ); + unorderedEquals([ + 'flutter-pi', + 'libflutter_engine.so', + ]), + ); + }); + + test('windows', () async { + fs = MemoryFileSystem.test(style: FileSystemStyle.windows); + + buildSystem.buildFn = ( + fl.Target target, + fl.Environment environment, { + fl.BuildSystemConfig buildSystemConfig = const fl.BuildSystemConfig(), + }) async { + return fl.BuildResult(success: true); + }; + + final bundle = await _runInTestContext( + () async => await appBuilder.buildBundle( + id: 'test-id', + host: FlutterpiHostPlatform.linuxRV64, + target: FlutterpiTargetPlatform.genericRiscv64, + buildInfo: fl.BuildInfo.debug, + fsLayout: FilesystemLayout.flutterPi, + forceBundleFlutterpi: false, + ), + ) as PrebuiltFlutterpiAppBundle; + + expect( + bundle.binaries.map( + (file) => p.windows.relative( + file.path, + from: p.windows.join('build', 'flutter-pi', 'riscv64-generic'), + ), + ), + unorderedEquals([ + 'flutter-pi', + 'libflutter_engine.so', + ]), + ); + }); }); - test('binary paths for --fs-layout=flutter-pi and include debug symbols', - () async { - buildSystem.buildFn = ( - fl.Target target, - fl.Environment environment, { - fl.BuildSystemConfig buildSystemConfig = const fl.BuildSystemConfig(), - }) async { - return fl.BuildResult(success: true); - }; + group('binary paths for --fs-layout=flutter-pi and include debug symbols', + () { + test('posix', () async { + buildSystem.buildFn = ( + fl.Target target, + fl.Environment environment, { + fl.BuildSystemConfig buildSystemConfig = const fl.BuildSystemConfig(), + }) async { + return fl.BuildResult(success: true); + }; - final bundle = await _runInTestContext( - () async => await appBuilder.buildBundle( - id: 'test-id', - host: FlutterpiHostPlatform.linuxRV64, - target: FlutterpiTargetPlatform.genericRiscv64, - buildInfo: fl.BuildInfo.debug, - fsLayout: FilesystemLayout.flutterPi, - includeDebugSymbols: true, - forceBundleFlutterpi: false, - ), - ) as PrebuiltFlutterpiAppBundle; + final bundle = await _runInTestContext( + () async => await appBuilder.buildBundle( + id: 'test-id', + host: FlutterpiHostPlatform.linuxRV64, + target: FlutterpiTargetPlatform.genericRiscv64, + buildInfo: fl.BuildInfo.debug, + fsLayout: FilesystemLayout.flutterPi, + includeDebugSymbols: true, + forceBundleFlutterpi: false, + ), + ) as PrebuiltFlutterpiAppBundle; - expect( - bundle.binaries.map( - (file) => p.relative( - file.path, - from: p.join('build', 'flutter-pi', 'riscv64-generic'), + expect( + bundle.binaries.map( + (file) => p.posix.relative( + file.path, + from: p.posix.join('build', 'flutter-pi', 'riscv64-generic'), + ), ), - ), - unorderedEquals([ - 'flutter-pi', - 'libflutter_engine.dbgsyms', - 'libflutter_engine.so', - ]), - ); + unorderedEquals([ + 'flutter-pi', + 'libflutter_engine.dbgsyms', + 'libflutter_engine.so', + ]), + ); + }); + + test('windows', () async { + fs = MemoryFileSystem.test(style: FileSystemStyle.windows); + + buildSystem.buildFn = ( + fl.Target target, + fl.Environment environment, { + fl.BuildSystemConfig buildSystemConfig = const fl.BuildSystemConfig(), + }) async { + return fl.BuildResult(success: true); + }; + + final bundle = await _runInTestContext( + () async => await appBuilder.buildBundle( + id: 'test-id', + host: FlutterpiHostPlatform.linuxRV64, + target: FlutterpiTargetPlatform.genericRiscv64, + buildInfo: fl.BuildInfo.debug, + fsLayout: FilesystemLayout.flutterPi, + includeDebugSymbols: true, + forceBundleFlutterpi: false, + ), + ) as PrebuiltFlutterpiAppBundle; + + expect( + bundle.binaries.map( + (file) => p.windows.relative( + file.path, + from: p.windows.join('build', 'flutter-pi', 'riscv64-generic'), + ), + ), + unorderedEquals([ + 'flutter-pi', + 'libflutter_engine.dbgsyms', + 'libflutter_engine.so', + ]), + ); + }); }); - test('binary paths for --fs-layout=meta-flutter', () async { - buildSystem.buildFn = ( - fl.Target target, - fl.Environment environment, { - fl.BuildSystemConfig buildSystemConfig = const fl.BuildSystemConfig(), - }) async { - return fl.BuildResult(success: true); - }; + group('binary paths for --fs-layout=meta-flutter', () { + test('posix', () async { + buildSystem.buildFn = ( + fl.Target target, + fl.Environment environment, { + fl.BuildSystemConfig buildSystemConfig = const fl.BuildSystemConfig(), + }) async { + return fl.BuildResult(success: true); + }; - final bundle = await _runInTestContext( - () async => await appBuilder.buildBundle( - id: 'test-id', - host: FlutterpiHostPlatform.linuxRV64, - target: FlutterpiTargetPlatform.genericRiscv64, - buildInfo: fl.BuildInfo.debug, - fsLayout: FilesystemLayout.metaFlutter, - forceBundleFlutterpi: false, - ), - ) as PrebuiltFlutterpiAppBundle; + final bundle = await _runInTestContext( + () async => await appBuilder.buildBundle( + id: 'test-id', + host: FlutterpiHostPlatform.linuxRV64, + target: FlutterpiTargetPlatform.genericRiscv64, + buildInfo: fl.BuildInfo.debug, + fsLayout: FilesystemLayout.metaFlutter, + forceBundleFlutterpi: false, + ), + ) as PrebuiltFlutterpiAppBundle; - expect( - bundle.binaries.map( - (file) => p.relative( - file.path, - from: p.join('build', 'flutter-pi', 'meta-flutter-riscv64-generic'), + expect( + bundle.binaries.map( + (file) => p.posix.relative( + file.path, + from: p.posix + .join('build', 'flutter-pi', 'meta-flutter-riscv64-generic'), + ), ), - ), - unorderedEquals([ - p.join('lib', 'libflutter_engine.so'), - ]), - ); + unorderedEquals([ + p.posix.join('lib', 'libflutter_engine.so'), + ]), + ); + }); + + test('windows', () async { + fs = MemoryFileSystem.test(style: FileSystemStyle.windows); + + buildSystem.buildFn = ( + fl.Target target, + fl.Environment environment, { + fl.BuildSystemConfig buildSystemConfig = const fl.BuildSystemConfig(), + }) async { + return fl.BuildResult(success: true); + }; + + final bundle = await _runInTestContext( + () async => await appBuilder.buildBundle( + id: 'test-id', + host: FlutterpiHostPlatform.linuxRV64, + target: FlutterpiTargetPlatform.genericRiscv64, + buildInfo: fl.BuildInfo.debug, + fsLayout: FilesystemLayout.metaFlutter, + forceBundleFlutterpi: false, + ), + ) as PrebuiltFlutterpiAppBundle; + + expect( + bundle.binaries.map( + (file) => p.windows.relative( + file.path, + from: p.windows + .join('build', 'flutter-pi', 'meta-flutter-riscv64-generic'), + ), + ), + unorderedEquals([ + p.windows.join('lib', 'libflutter_engine.so'), + ]), + ); + }); }); - test( + group( 'binary paths for --fs-layout=meta-flutter with force bundle flutterpi', - () async { - buildSystem.buildFn = ( - fl.Target target, - fl.Environment environment, { - fl.BuildSystemConfig buildSystemConfig = const fl.BuildSystemConfig(), - }) async { - return fl.BuildResult(success: true); - }; + () { + test('posix', () async { + buildSystem.buildFn = ( + fl.Target target, + fl.Environment environment, { + fl.BuildSystemConfig buildSystemConfig = const fl.BuildSystemConfig(), + }) async { + return fl.BuildResult(success: true); + }; - final bundle = await _runInTestContext( - () async => await appBuilder.buildBundle( - id: 'test-id', - host: FlutterpiHostPlatform.linuxRV64, - target: FlutterpiTargetPlatform.genericRiscv64, - buildInfo: fl.BuildInfo.debug, - fsLayout: FilesystemLayout.metaFlutter, - forceBundleFlutterpi: true, - ), - ) as PrebuiltFlutterpiAppBundle; + final bundle = await _runInTestContext( + () async => await appBuilder.buildBundle( + id: 'test-id', + host: FlutterpiHostPlatform.linuxRV64, + target: FlutterpiTargetPlatform.genericRiscv64, + buildInfo: fl.BuildInfo.debug, + fsLayout: FilesystemLayout.metaFlutter, + forceBundleFlutterpi: true, + ), + ) as PrebuiltFlutterpiAppBundle; - expect( - bundle.binaries.map( - (file) => p.relative( - file.path, - from: p.join('build', 'flutter-pi', 'meta-flutter-riscv64-generic'), + expect( + bundle.binaries.map( + (file) => p.posix.relative( + file.path, + from: p.posix + .join('build', 'flutter-pi', 'meta-flutter-riscv64-generic'), + ), ), - ), - unorderedEquals([ - p.join('bin', 'flutter-pi'), - p.join('lib', 'libflutter_engine.so'), - ]), - ); + unorderedEquals([ + p.posix.join('bin', 'flutter-pi'), + p.posix.join('lib', 'libflutter_engine.so'), + ]), + ); + }); + + test('windows', () async { + fs = MemoryFileSystem.test(style: FileSystemStyle.windows); + + buildSystem.buildFn = ( + fl.Target target, + fl.Environment environment, { + fl.BuildSystemConfig buildSystemConfig = const fl.BuildSystemConfig(), + }) async { + return fl.BuildResult(success: true); + }; + + final bundle = await _runInTestContext( + () async => await appBuilder.buildBundle( + id: 'test-id', + host: FlutterpiHostPlatform.linuxRV64, + target: FlutterpiTargetPlatform.genericRiscv64, + buildInfo: fl.BuildInfo.debug, + fsLayout: FilesystemLayout.metaFlutter, + forceBundleFlutterpi: true, + ), + ) as PrebuiltFlutterpiAppBundle; + + expect( + bundle.binaries.map( + (file) => p.windows.relative( + file.path, + from: p.windows + .join('build', 'flutter-pi', 'meta-flutter-riscv64-generic'), + ), + ), + unorderedEquals([ + p.windows.join('bin', 'flutter-pi'), + p.windows.join('lib', 'libflutter_engine.so'), + ]), + ); + }); }); - test('binary paths for --fs-layout=meta-flutter with include debug symbols', - () async { - buildSystem.buildFn = ( - fl.Target target, - fl.Environment environment, { - fl.BuildSystemConfig buildSystemConfig = const fl.BuildSystemConfig(), - }) async { - return fl.BuildResult(success: true); - }; + group( + 'binary paths for --fs-layout=meta-flutter with include debug symbols', + () { + test('posix', () async { + buildSystem.buildFn = ( + fl.Target target, + fl.Environment environment, { + fl.BuildSystemConfig buildSystemConfig = const fl.BuildSystemConfig(), + }) async { + return fl.BuildResult(success: true); + }; - final bundle = await _runInTestContext( - () async => await appBuilder.buildBundle( - id: 'test-id', - host: FlutterpiHostPlatform.linuxRV64, - target: FlutterpiTargetPlatform.genericRiscv64, - buildInfo: fl.BuildInfo.debug, - fsLayout: FilesystemLayout.metaFlutter, - includeDebugSymbols: true, - forceBundleFlutterpi: false, - ), - ) as PrebuiltFlutterpiAppBundle; + final bundle = await _runInTestContext( + () async => await appBuilder.buildBundle( + id: 'test-id', + host: FlutterpiHostPlatform.linuxRV64, + target: FlutterpiTargetPlatform.genericRiscv64, + buildInfo: fl.BuildInfo.debug, + fsLayout: FilesystemLayout.metaFlutter, + includeDebugSymbols: true, + forceBundleFlutterpi: false, + ), + ) as PrebuiltFlutterpiAppBundle; - expect( - bundle.binaries.map( - (file) => p.relative( - file.path, - from: p.join('build', 'flutter-pi', 'meta-flutter-riscv64-generic'), + expect( + bundle.binaries.map( + (file) => p.posix.relative( + file.path, + from: p.posix + .join('build', 'flutter-pi', 'meta-flutter-riscv64-generic'), + ), ), - ), - unorderedEquals([ - p.join('lib', 'libflutter_engine.dbgsyms'), - p.join('lib', 'libflutter_engine.so'), - ]), - ); + unorderedEquals([ + p.posix.join('lib', 'libflutter_engine.dbgsyms'), + p.posix.join('lib', 'libflutter_engine.so'), + ]), + ); + }); + + test('windows', () async { + fs = MemoryFileSystem.test(style: FileSystemStyle.windows); + + buildSystem.buildFn = ( + fl.Target target, + fl.Environment environment, { + fl.BuildSystemConfig buildSystemConfig = const fl.BuildSystemConfig(), + }) async { + return fl.BuildResult(success: true); + }; + + final bundle = await _runInTestContext( + () async => await appBuilder.buildBundle( + id: 'test-id', + host: FlutterpiHostPlatform.linuxRV64, + target: FlutterpiTargetPlatform.genericRiscv64, + buildInfo: fl.BuildInfo.debug, + fsLayout: FilesystemLayout.metaFlutter, + includeDebugSymbols: true, + forceBundleFlutterpi: false, + ), + ) as PrebuiltFlutterpiAppBundle; + + expect( + bundle.binaries.map( + (file) => p.windows.relative( + file.path, + from: p.windows + .join('build', 'flutter-pi', 'meta-flutter-riscv64-generic'), + ), + ), + unorderedEquals([ + p.windows.join('lib', 'libflutter_engine.dbgsyms'), + p.windows.join('lib', 'libflutter_engine.so'), + ]), + ); + }); }); }); } From 597810029eeb0001b1759a1f5974b3fe675d2251 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 7 Feb 2026 14:46:03 +0100 Subject: [PATCH 12/32] remove unused import --- test/app_builder_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/test/app_builder_test.dart b/test/app_builder_test.dart index 6a02ae1..39cd157 100644 --- a/test/app_builder_test.dart +++ b/test/app_builder_test.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutterpi_tool/src/build_system/targets.dart'; import 'package:flutterpi_tool/src/cli/flutterpi_command.dart'; From adcceb54fe4ce7204b22710ac3ff5e86f4c2383d Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 7 Feb 2026 14:51:29 +0100 Subject: [PATCH 13/32] fix missing trailing comma --- test/app_builder_test.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/app_builder_test.dart b/test/app_builder_test.dart index 39cd157..83bf118 100644 --- a/test/app_builder_test.dart +++ b/test/app_builder_test.dart @@ -344,7 +344,10 @@ void main() { environment.outputDir.path, equals( p.windows.join( - 'build', 'flutter-pi', 'meta-flutter-riscv64-generic'), + 'build', + 'flutter-pi', + 'meta-flutter-riscv64-generic', + ), ), ); From 800a5d004facff6d7cd6f390ba04697cb1584317 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 7 Feb 2026 15:28:47 +0100 Subject: [PATCH 14/32] add a hooks / native assets test package --- .gitignore | 5 +- integration_test/hooks_test.dart | 3 + integration_test/hooks_test/.gitignore | 29 ++ integration_test/hooks_test/.metadata | 27 + .../hooks_test/example/.gitignore | 45 ++ integration_test/hooks_test/example/README.md | 16 + .../hooks_test/example/lib/main.dart | 74 +++ .../hooks_test/example/pubspec.lock | 300 ++++++++++++ .../hooks_test/example/pubspec.yaml | 97 ++++ integration_test/hooks_test/ffigen.yaml | 20 + integration_test/hooks_test/hook/build.dart | 23 + .../hooks_test/lib/hooks_test.dart | 108 ++++ .../lib/hooks_test_bindings_generated.dart | 30 ++ integration_test/hooks_test/pubspec.lock | 461 ++++++++++++++++++ integration_test/hooks_test/pubspec.yaml | 18 + integration_test/hooks_test/src/hooks_test.c | 29 ++ integration_test/hooks_test/src/hooks_test.h | 30 ++ 17 files changed, 1313 insertions(+), 2 deletions(-) create mode 100644 integration_test/hooks_test.dart create mode 100644 integration_test/hooks_test/.gitignore create mode 100644 integration_test/hooks_test/.metadata create mode 100644 integration_test/hooks_test/example/.gitignore create mode 100644 integration_test/hooks_test/example/README.md create mode 100644 integration_test/hooks_test/example/lib/main.dart create mode 100644 integration_test/hooks_test/example/pubspec.lock create mode 100644 integration_test/hooks_test/example/pubspec.yaml create mode 100644 integration_test/hooks_test/ffigen.yaml create mode 100644 integration_test/hooks_test/hook/build.dart create mode 100644 integration_test/hooks_test/lib/hooks_test.dart create mode 100644 integration_test/hooks_test/lib/hooks_test_bindings_generated.dart create mode 100644 integration_test/hooks_test/pubspec.lock create mode 100644 integration_test/hooks_test/pubspec.yaml create mode 100644 integration_test/hooks_test/src/hooks_test.c create mode 100644 integration_test/hooks_test/src/hooks_test.h diff --git a/.gitignore b/.gitignore index 84683cf..a513d72 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,9 @@ .dart_tool/ .packages build/ + # If you're building an application, you may want to check-in your pubspec.lock -pubspec.lock +/pubspec.lock # Directory created by dartdoc # If you don't generate documentation locally you can remove this line. @@ -52,4 +53,4 @@ Icon .AppleDesktop Network Trash Folder Temporary Items -.apdisk \ No newline at end of file +.apdisk diff --git a/integration_test/hooks_test.dart b/integration_test/hooks_test.dart new file mode 100644 index 0000000..67ef345 --- /dev/null +++ b/integration_test/hooks_test.dart @@ -0,0 +1,3 @@ +void main() { + +} diff --git a/integration_test/hooks_test/.gitignore b/integration_test/hooks_test/.gitignore new file mode 100644 index 0000000..cd5fa12 --- /dev/null +++ b/integration_test/hooks_test/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins-dependencies +/build/ +/coverage/ diff --git a/integration_test/hooks_test/.metadata b/integration_test/hooks_test/.metadata new file mode 100644 index 0000000..c422502 --- /dev/null +++ b/integration_test/hooks_test/.metadata @@ -0,0 +1,27 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "3b62efc2a3da49882f43c372e0bc53daef7295a6" + channel: "stable" + +project_type: package_ffi + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 + base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/integration_test/hooks_test/example/.gitignore b/integration_test/hooks_test/example/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/integration_test/hooks_test/example/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/integration_test/hooks_test/example/README.md b/integration_test/hooks_test/example/README.md new file mode 100644 index 0000000..a7bde44 --- /dev/null +++ b/integration_test/hooks_test/example/README.md @@ -0,0 +1,16 @@ +# hooks_test_example + +Demonstrates how to use the hooks_test package. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/integration_test/hooks_test/example/lib/main.dart b/integration_test/hooks_test/example/lib/main.dart new file mode 100644 index 0000000..9984b7c --- /dev/null +++ b/integration_test/hooks_test/example/lib/main.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; + +import 'package:hooks_test/hooks_test.dart' as hooks_test; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + late int sumResult; + late Future sumAsyncResult; + + @override + void initState() { + super.initState(); + sumResult = hooks_test.sum(1, 2); + sumAsyncResult = hooks_test.sumAsync(3, 4); + } + + @override + Widget build(BuildContext context) { + const textStyle = TextStyle(fontSize: 25); + const spacerSmall = SizedBox(height: 10); + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Native Packages'), + ), + body: SingleChildScrollView( + child: Container( + padding: const .all(10), + child: Column( + children: [ + const Text( + 'This calls a native function through FFI that is shipped as source in the package. ' + 'The native code is built as part of the Flutter Runner build.', + style: textStyle, + textAlign: .center, + ), + spacerSmall, + Text( + 'sum(1, 2) = $sumResult', + style: textStyle, + textAlign: .center, + ), + spacerSmall, + FutureBuilder( + future: sumAsyncResult, + builder: (BuildContext context, AsyncSnapshot value) { + final displayValue = + (value.hasData) ? value.data : 'loading'; + return Text( + 'await sumAsync(3, 4) = $displayValue', + style: textStyle, + textAlign: .center, + ); + }, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/integration_test/hooks_test/example/pubspec.lock b/integration_test/hooks_test/example/pubspec.lock new file mode 100644 index 0000000..150a6a5 --- /dev/null +++ b/integration_test/hooks_test/example/pubspec.lock @@ -0,0 +1,300 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: ae0db647e668cbb295a3527f0938e4039e004c80099dce2f964102373f5ce0b5 + url: "https://pub.dev" + source: hosted + version: "0.19.10" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + hooks: + dependency: transitive + description: + name: hooks + sha256: "5410b9f4f6c9f01e8ff0eb81c9801ea13a3c3d39f8f0b1613cda08e27eab3c18" + url: "https://pub.dev" + source: hosted + version: "0.20.5" + hooks_test: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: f8872ea6c7a50ce08db9ae280ca2b8efdd973157ce462826c82f3c3051d154ce + url: "https://pub.dev" + source: hosted + version: "0.17.2" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.10.7 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/integration_test/hooks_test/example/pubspec.yaml b/integration_test/hooks_test/example/pubspec.yaml new file mode 100644 index 0000000..7084931 --- /dev/null +++ b/integration_test/hooks_test/example/pubspec.yaml @@ -0,0 +1,97 @@ +name: hooks_test_example +description: "Demonstrates how to use the hooks_test package." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.10.7 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + hooks_test: + # When depending on this package from a real application you should use: + # hooks_test: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^6.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/integration_test/hooks_test/ffigen.yaml b/integration_test/hooks_test/ffigen.yaml new file mode 100644 index 0000000..31f33bb --- /dev/null +++ b/integration_test/hooks_test/ffigen.yaml @@ -0,0 +1,20 @@ +# Run with `dart run ffigen --config ffigen.yaml`. +name: HooksTestBindings +description: | + Bindings for `src/hooks_test.h`. + + Regenerate bindings with `dart run ffigen --config ffigen.yaml`. +output: 'lib/hooks_test_bindings_generated.dart' +headers: + entry-points: + - 'src/hooks_test.h' + include-directives: + - 'src/hooks_test.h' +ffi-native: +preamble: | + // ignore_for_file: always_specify_types + // ignore_for_file: camel_case_types + // ignore_for_file: non_constant_identifier_names +comments: + style: any + length: full diff --git a/integration_test/hooks_test/hook/build.dart b/integration_test/hooks_test/hook/build.dart new file mode 100644 index 0000000..d8f0bce --- /dev/null +++ b/integration_test/hooks_test/hook/build.dart @@ -0,0 +1,23 @@ +import 'package:native_toolchain_c/native_toolchain_c.dart'; +import 'package:logging/logging.dart'; +import 'package:hooks/hooks.dart'; + +void main(List args) async { + await build(args, (input, output) async { + final packageName = input.packageName; + final cbuilder = CBuilder.library( + name: packageName, + assetName: '${packageName}_bindings_generated.dart', + sources: [ + 'src/$packageName.c', + ], + ); + await cbuilder.run( + input: input, + output: output, + logger: Logger('') + ..level = .ALL + ..onRecord.listen((record) => print(record.message)), + ); + }); +} diff --git a/integration_test/hooks_test/lib/hooks_test.dart b/integration_test/hooks_test/lib/hooks_test.dart new file mode 100644 index 0000000..326d32c --- /dev/null +++ b/integration_test/hooks_test/lib/hooks_test.dart @@ -0,0 +1,108 @@ +import 'dart:async'; +import 'dart:isolate'; + +import 'hooks_test_bindings_generated.dart' as bindings; + +/// A very short-lived native function. +/// +/// For very short-lived functions, it is fine to call them on the main isolate. +/// They will block the Dart execution while running the native function, so +/// only do this for native functions which are guaranteed to be short-lived. +int sum(int a, int b) => bindings.sum(a, b); + +/// A longer lived native function, which occupies the thread calling it. +/// +/// Do not call these kind of native functions in the main isolate. They will +/// block Dart execution. This will cause dropped frames in Flutter applications. +/// Instead, call these native functions on a separate isolate. +/// +/// Modify this to suit your own use case. Example use cases: +/// +/// 1. Reuse a single isolate for various different kinds of requests. +/// 2. Use multiple helper isolates for parallel execution. +Future sumAsync(int a, int b) async { + final SendPort helperIsolateSendPort = await _helperIsolateSendPort; + final int requestId = _nextSumRequestId++; + final _SumRequest request = _SumRequest(requestId, a, b); + final Completer completer = Completer(); + _sumRequests[requestId] = completer; + helperIsolateSendPort.send(request); + return completer.future; +} + +/// A request to compute `sum`. +/// +/// Typically sent from one isolate to another. +class _SumRequest { + final int id; + final int a; + final int b; + + const _SumRequest(this.id, this.a, this.b); +} + +/// A response with the result of `sum`. +/// +/// Typically sent from one isolate to another. +class _SumResponse { + final int id; + final int result; + + const _SumResponse(this.id, this.result); +} + +/// Counter to identify [_SumRequest]s and [_SumResponse]s. +int _nextSumRequestId = 0; + +/// Mapping from [_SumRequest] `id`s to the completers corresponding to the correct future of the pending request. +final Map> _sumRequests = >{}; + +/// The SendPort belonging to the helper isolate. +Future _helperIsolateSendPort = () async { + // The helper isolate is going to send us back a SendPort, which we want to + // wait for. + final Completer completer = Completer(); + + // Receive port on the main isolate to receive messages from the helper. + // We receive two types of messages: + // 1. A port to send messages on. + // 2. Responses to requests we sent. + final ReceivePort receivePort = ReceivePort() + ..listen((dynamic data) { + if (data is SendPort) { + // The helper isolate sent us the port on which we can sent it requests. + completer.complete(data); + return; + } + if (data is _SumResponse) { + // The helper isolate sent us a response to a request we sent. + final Completer completer = _sumRequests[data.id]!; + _sumRequests.remove(data.id); + completer.complete(data.result); + return; + } + throw UnsupportedError('Unsupported message type: ${data.runtimeType}'); + }); + + // Start the helper isolate. + await Isolate.spawn((SendPort sendPort) async { + final ReceivePort helperReceivePort = ReceivePort() + ..listen((dynamic data) { + // On the helper isolate listen to requests and respond to them. + if (data is _SumRequest) { + final int result = bindings.sum_long_running(data.a, data.b); + final _SumResponse response = _SumResponse(data.id, result); + sendPort.send(response); + return; + } + throw UnsupportedError('Unsupported message type: ${data.runtimeType}'); + }); + + // Send the port to the main isolate on which we can receive requests. + sendPort.send(helperReceivePort.sendPort); + }, receivePort.sendPort); + + // Wait until the helper isolate has sent us back the SendPort on which we + // can start sending requests. + return completer.future; +}(); diff --git a/integration_test/hooks_test/lib/hooks_test_bindings_generated.dart b/integration_test/hooks_test/lib/hooks_test_bindings_generated.dart new file mode 100644 index 0000000..65642b4 --- /dev/null +++ b/integration_test/hooks_test/lib/hooks_test_bindings_generated.dart @@ -0,0 +1,30 @@ +// ignore_for_file: always_specify_types +// ignore_for_file: camel_case_types +// ignore_for_file: non_constant_identifier_names + +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +import 'dart:ffi' as ffi; + +/// A very short-lived native function. +/// +/// For very short-lived functions, it is fine to call them on the main isolate. +/// They will block the Dart execution while running the native function, so +/// only do this for native functions which are guaranteed to be short-lived. +@ffi.Native() +external int sum( + int a, + int b, +); + +/// A longer lived native function, which occupies the thread calling it. +/// +/// Do not call these kind of native functions in the main isolate. They will +/// block Dart execution. This will cause dropped frames in Flutter applications. +/// Instead, call these native functions on a separate isolate. +@ffi.Native() +external int sum_long_running( + int a, + int b, +); diff --git a/integration_test/hooks_test/pubspec.lock b/integration_test/hooks_test/pubspec.lock new file mode 100644 index 0000000..ecbc47d --- /dev/null +++ b/integration_test/hooks_test/pubspec.lock @@ -0,0 +1,461 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "796d97d925add7ffcdf5595f33a2066a6e3cee97971e6dbef09b76b7880fd760" + url: "https://pub.dev" + source: hosted + version: "94.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "9c8ebb304d72c0a0c8764344627529d9503fc83d7d73e43ed727dc532f822e4b" + url: "https://pub.dev" + source: hosted + version: "10.0.2" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" + code_assets: + dependency: "direct main" + description: + name: code_assets + sha256: ae0db647e668cbb295a3527f0938e4039e004c80099dce2f964102373f5ce0b5 + url: "https://pub.dev" + source: hosted + version: "0.19.10" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + url: "https://pub.dev" + source: hosted + version: "1.15.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + ffi: + dependency: "direct dev" + description: + name: ffi + sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c + url: "https://pub.dev" + source: hosted + version: "2.1.5" + ffigen: + dependency: "direct dev" + description: + name: ffigen + sha256: "10cb41647d73e0204f8d35138a3f20eb52418cce96599ad49167b1111e59a557" + url: "https://pub.dev" + source: hosted + version: "13.0.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + hooks: + dependency: "direct main" + description: + name: hooks + sha256: "5410b9f4f6c9f01e8ff0eb81c9801ea13a3c3d39f8f0b1613cda08e27eab3c18" + url: "https://pub.dev" + source: hosted + version: "0.20.5" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + logging: + dependency: "direct main" + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + url: "https://pub.dev" + source: hosted + version: "0.12.18" + meta: + dependency: transitive + description: + name: meta + sha256: "9f29b9bcc8ee287b1a31e0d01be0eae99a930dbffdaecf04b3f3d82a969f296f" + url: "https://pub.dev" + source: hosted + version: "1.18.1" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + native_toolchain_c: + dependency: "direct main" + description: + name: native_toolchain_c + sha256: f8872ea6c7a50ce08db9ae280ca2b8efdd973157ce462826c82f3c3051d154ce + url: "https://pub.dev" + source: hosted + version: "0.17.2" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test: + dependency: "direct dev" + description: + name: test + sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a" + url: "https://pub.dev" + source: hosted + version: "1.29.0" + test_api: + dependency: transitive + description: + name: test_api + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + url: "https://pub.dev" + source: hosted + version: "0.7.9" + test_core: + dependency: transitive + description: + name: test_core + sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943" + url: "https://pub.dev" + source: hosted + version: "0.6.15" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + sha256: ec709065bb2c911b336853b67f3732dd13e0336bd065cc2f1061d7610ddf45e3 + url: "https://pub.dev" + source: hosted + version: "2.2.3" +sdks: + dart: ">=3.10.7 <4.0.0" diff --git a/integration_test/hooks_test/pubspec.yaml b/integration_test/hooks_test/pubspec.yaml new file mode 100644 index 0000000..bf60eae --- /dev/null +++ b/integration_test/hooks_test/pubspec.yaml @@ -0,0 +1,18 @@ +name: hooks_test +description: "A new Dart FFI package project." +version: 0.0.1 + +environment: + sdk: ^3.10.7 + +dependencies: + code_assets: ^0.19.7 + hooks: ^0.20.1 + logging: ^1.3.0 + native_toolchain_c: ^0.17.1 + +dev_dependencies: + ffi: ^2.1.3 + ffigen: ^13.0.0 + flutter_lints: ^6.0.0 + test: ^1.25.8 diff --git a/integration_test/hooks_test/src/hooks_test.c b/integration_test/hooks_test/src/hooks_test.c new file mode 100644 index 0000000..41b4c2f --- /dev/null +++ b/integration_test/hooks_test/src/hooks_test.c @@ -0,0 +1,29 @@ +#include "hooks_test.h" + +// A very short-lived native function. +// +// For very short-lived functions, it is fine to call them on the main isolate. +// They will block the Dart execution while running the native function, so +// only do this for native functions which are guaranteed to be short-lived. +FFI_PLUGIN_EXPORT intptr_t sum(intptr_t a, intptr_t b) { +#ifdef DEBUG + return a + b + 1000; +#else + return a + b; +#endif +} + +// A longer-lived native function, which occupies the thread calling it. +// +// Do not call these kind of native functions in the main isolate. They will +// block Dart execution. This will cause dropped frames in Flutter applications. +// Instead, call these native functions on a separate isolate. +FFI_PLUGIN_EXPORT intptr_t sum_long_running(intptr_t a, intptr_t b) { + // Simulate work. +#if _WIN32 + Sleep(5000); +#else + usleep(5000 * 1000); +#endif + return a + b; +} diff --git a/integration_test/hooks_test/src/hooks_test.h b/integration_test/hooks_test/src/hooks_test.h new file mode 100644 index 0000000..084c642 --- /dev/null +++ b/integration_test/hooks_test/src/hooks_test.h @@ -0,0 +1,30 @@ +#include +#include +#include + +#if _WIN32 +#include +#else +#include +#include +#endif + +#if _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT +#endif + +// A very short-lived native function. +// +// For very short-lived functions, it is fine to call them on the main isolate. +// They will block the Dart execution while running the native function, so +// only do this for native functions which are guaranteed to be short-lived. +FFI_PLUGIN_EXPORT intptr_t sum(intptr_t a, intptr_t b); + +// A longer lived native function, which occupies the thread calling it. +// +// Do not call these kind of native functions in the main isolate. They will +// block Dart execution. This will cause dropped frames in Flutter applications. +// Instead, call these native functions on a separate isolate. +FFI_PLUGIN_EXPORT intptr_t sum_long_running(intptr_t a, intptr_t b); From e4e9aa24fe4f9ca241f37ace42ddc7d82b584134 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 7 Feb 2026 15:34:49 +0100 Subject: [PATCH 15/32] rename integration_test => hooks_test --- {integration_test => e2e}/hooks_test.dart | 0 {integration_test => e2e}/hooks_test/.gitignore | 0 {integration_test => e2e}/hooks_test/.metadata | 0 {integration_test => e2e}/hooks_test/example/.gitignore | 0 {integration_test => e2e}/hooks_test/example/README.md | 0 {integration_test => e2e}/hooks_test/example/lib/main.dart | 0 {integration_test => e2e}/hooks_test/example/pubspec.lock | 0 {integration_test => e2e}/hooks_test/example/pubspec.yaml | 0 {integration_test => e2e}/hooks_test/ffigen.yaml | 0 {integration_test => e2e}/hooks_test/hook/build.dart | 0 {integration_test => e2e}/hooks_test/lib/hooks_test.dart | 0 .../hooks_test/lib/hooks_test_bindings_generated.dart | 0 {integration_test => e2e}/hooks_test/pubspec.lock | 0 {integration_test => e2e}/hooks_test/pubspec.yaml | 0 {integration_test => e2e}/hooks_test/src/hooks_test.c | 0 {integration_test => e2e}/hooks_test/src/hooks_test.h | 0 16 files changed, 0 insertions(+), 0 deletions(-) rename {integration_test => e2e}/hooks_test.dart (100%) rename {integration_test => e2e}/hooks_test/.gitignore (100%) rename {integration_test => e2e}/hooks_test/.metadata (100%) rename {integration_test => e2e}/hooks_test/example/.gitignore (100%) rename {integration_test => e2e}/hooks_test/example/README.md (100%) rename {integration_test => e2e}/hooks_test/example/lib/main.dart (100%) rename {integration_test => e2e}/hooks_test/example/pubspec.lock (100%) rename {integration_test => e2e}/hooks_test/example/pubspec.yaml (100%) rename {integration_test => e2e}/hooks_test/ffigen.yaml (100%) rename {integration_test => e2e}/hooks_test/hook/build.dart (100%) rename {integration_test => e2e}/hooks_test/lib/hooks_test.dart (100%) rename {integration_test => e2e}/hooks_test/lib/hooks_test_bindings_generated.dart (100%) rename {integration_test => e2e}/hooks_test/pubspec.lock (100%) rename {integration_test => e2e}/hooks_test/pubspec.yaml (100%) rename {integration_test => e2e}/hooks_test/src/hooks_test.c (100%) rename {integration_test => e2e}/hooks_test/src/hooks_test.h (100%) diff --git a/integration_test/hooks_test.dart b/e2e/hooks_test.dart similarity index 100% rename from integration_test/hooks_test.dart rename to e2e/hooks_test.dart diff --git a/integration_test/hooks_test/.gitignore b/e2e/hooks_test/.gitignore similarity index 100% rename from integration_test/hooks_test/.gitignore rename to e2e/hooks_test/.gitignore diff --git a/integration_test/hooks_test/.metadata b/e2e/hooks_test/.metadata similarity index 100% rename from integration_test/hooks_test/.metadata rename to e2e/hooks_test/.metadata diff --git a/integration_test/hooks_test/example/.gitignore b/e2e/hooks_test/example/.gitignore similarity index 100% rename from integration_test/hooks_test/example/.gitignore rename to e2e/hooks_test/example/.gitignore diff --git a/integration_test/hooks_test/example/README.md b/e2e/hooks_test/example/README.md similarity index 100% rename from integration_test/hooks_test/example/README.md rename to e2e/hooks_test/example/README.md diff --git a/integration_test/hooks_test/example/lib/main.dart b/e2e/hooks_test/example/lib/main.dart similarity index 100% rename from integration_test/hooks_test/example/lib/main.dart rename to e2e/hooks_test/example/lib/main.dart diff --git a/integration_test/hooks_test/example/pubspec.lock b/e2e/hooks_test/example/pubspec.lock similarity index 100% rename from integration_test/hooks_test/example/pubspec.lock rename to e2e/hooks_test/example/pubspec.lock diff --git a/integration_test/hooks_test/example/pubspec.yaml b/e2e/hooks_test/example/pubspec.yaml similarity index 100% rename from integration_test/hooks_test/example/pubspec.yaml rename to e2e/hooks_test/example/pubspec.yaml diff --git a/integration_test/hooks_test/ffigen.yaml b/e2e/hooks_test/ffigen.yaml similarity index 100% rename from integration_test/hooks_test/ffigen.yaml rename to e2e/hooks_test/ffigen.yaml diff --git a/integration_test/hooks_test/hook/build.dart b/e2e/hooks_test/hook/build.dart similarity index 100% rename from integration_test/hooks_test/hook/build.dart rename to e2e/hooks_test/hook/build.dart diff --git a/integration_test/hooks_test/lib/hooks_test.dart b/e2e/hooks_test/lib/hooks_test.dart similarity index 100% rename from integration_test/hooks_test/lib/hooks_test.dart rename to e2e/hooks_test/lib/hooks_test.dart diff --git a/integration_test/hooks_test/lib/hooks_test_bindings_generated.dart b/e2e/hooks_test/lib/hooks_test_bindings_generated.dart similarity index 100% rename from integration_test/hooks_test/lib/hooks_test_bindings_generated.dart rename to e2e/hooks_test/lib/hooks_test_bindings_generated.dart diff --git a/integration_test/hooks_test/pubspec.lock b/e2e/hooks_test/pubspec.lock similarity index 100% rename from integration_test/hooks_test/pubspec.lock rename to e2e/hooks_test/pubspec.lock diff --git a/integration_test/hooks_test/pubspec.yaml b/e2e/hooks_test/pubspec.yaml similarity index 100% rename from integration_test/hooks_test/pubspec.yaml rename to e2e/hooks_test/pubspec.yaml diff --git a/integration_test/hooks_test/src/hooks_test.c b/e2e/hooks_test/src/hooks_test.c similarity index 100% rename from integration_test/hooks_test/src/hooks_test.c rename to e2e/hooks_test/src/hooks_test.c diff --git a/integration_test/hooks_test/src/hooks_test.h b/e2e/hooks_test/src/hooks_test.h similarity index 100% rename from integration_test/hooks_test/src/hooks_test.h rename to e2e/hooks_test/src/hooks_test.h From 84c7d4b84104d718b99274e4040e84387bf90524 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 7 Feb 2026 16:00:30 +0100 Subject: [PATCH 16/32] add dart hooks e2e test Builds a hooks test app using flutterpi_tool, checks if it succeeds. Automatically run in CI. Also rename hooks_test => hooks_test_package, so it's not run with `dart test e2e`. --- .github/workflows/flutter.yml | 29 ++ e2e/hooks_test.dart | 61 +++- e2e/hooks_test/example/lib/main.dart | 74 ----- .../.gitignore | 0 .../.metadata | 0 .../example/.gitignore | 0 .../example/README.md | 0 e2e/hooks_test_package/example/lib/main.dart | 13 + .../example/pubspec.lock | 273 +++++++++++++++++- .../example/pubspec.yaml | 8 +- .../ffigen.yaml | 0 .../hook/build.dart | 0 .../lib/hooks_test_bindings_generated.dart | 0 .../lib/hooks_test_package.dart} | 0 .../pubspec.lock | 0 .../pubspec.yaml | 2 +- .../src/hooks_test.c | 0 .../src/hooks_test.h | 0 18 files changed, 380 insertions(+), 80 deletions(-) delete mode 100644 e2e/hooks_test/example/lib/main.dart rename e2e/{hooks_test => hooks_test_package}/.gitignore (100%) rename e2e/{hooks_test => hooks_test_package}/.metadata (100%) rename e2e/{hooks_test => hooks_test_package}/example/.gitignore (100%) rename e2e/{hooks_test => hooks_test_package}/example/README.md (100%) create mode 100644 e2e/hooks_test_package/example/lib/main.dart rename e2e/{hooks_test => hooks_test_package}/example/pubspec.lock (52%) rename e2e/{hooks_test => hooks_test_package}/example/pubspec.yaml (96%) rename e2e/{hooks_test => hooks_test_package}/ffigen.yaml (100%) rename e2e/{hooks_test => hooks_test_package}/hook/build.dart (100%) rename e2e/{hooks_test => hooks_test_package}/lib/hooks_test_bindings_generated.dart (100%) rename e2e/{hooks_test/lib/hooks_test.dart => hooks_test_package/lib/hooks_test_package.dart} (100%) rename e2e/{hooks_test => hooks_test_package}/pubspec.lock (100%) rename e2e/{hooks_test => hooks_test_package}/pubspec.yaml (91%) rename e2e/{hooks_test => hooks_test_package}/src/hooks_test.c (100%) rename e2e/{hooks_test => hooks_test_package}/src/hooks_test.h (100%) diff --git a/.github/workflows/flutter.yml b/.github/workflows/flutter.yml index aa1a49f..0446d64 100644 --- a/.github/workflows/flutter.yml +++ b/.github/workflows/flutter.yml @@ -64,3 +64,32 @@ jobs: - name: Run tests run: flutter test + + e2e-test: + name: End-to-End Tests (${{ matrix.runner }}) + runs-on: ${{ matrix.runner }} + env: + GITHUB_TOKEN: ${{ secrets.PUBLIC_ONLY_PAT }} + strategy: + matrix: + runner: + - windows-latest + - ubuntu-latest + - macos-latest + steps: + - uses: actions/checkout@v4 + + - uses: subosito/flutter-action@v2 + with: + cache: true + channel: stable + flutter-version: ${{ env.FLUTTER_VERSION }} + + - name: Install dependencies + run: flutter pub get + + - name: Activate flutterpi_tool globally + run: flutter pub global activate -spath . + + - name: Run e2e tests + run: dart test e2e diff --git a/e2e/hooks_test.dart b/e2e/hooks_test.dart index 67ef345..ef9f9eb 100644 --- a/e2e/hooks_test.dart +++ b/e2e/hooks_test.dart @@ -1,3 +1,62 @@ +import 'dart:io'; + +import 'package:test/test.dart'; + void main() { - + group('hooks_test native assets e2e', () { + const exampleDir = 'e2e/hooks_test_package/example'; + + setUpAll(() async { + // Ensure dependencies are installed + final pubGetResult = await Process.run( + 'flutter', + ['pub', 'get', '--enforce-lockfile'], + workingDirectory: exampleDir, + ); + expect( + pubGetResult.exitCode, + 0, + reason: 'flutter pub get should succeed', + ); + }); + + test('builds successfully in debug mode', () async { + final result = await Process.run( + 'flutterpi_tool', + ['build', '--debug'], + workingDirectory: exampleDir, + ); + + expect(result.exitCode, 0, + reason: 'Build should succeed with exit code 0'); + expect( + result.stderr.toString(), isNot(contains('Failed to build bundle.'))); + }, timeout: const Timeout(Duration(minutes: 5))); + + test('builds successfully in profile mode', () async { + final result = await Process.run( + 'flutterpi_tool', + ['build', '--profile'], + workingDirectory: exampleDir, + ); + + expect(result.exitCode, 0, + reason: 'Build should succeed with exit code 0'); + expect( + result.stderr.toString(), isNot(contains('Failed to build bundle.'))); + }, timeout: const Timeout(Duration(minutes: 5))); + + test('builds successfully in release mode', () async { + final result = await Process.run( + 'flutterpi_tool', + ['build', '--release'], + workingDirectory: exampleDir, + ); + + expect(result.exitCode, 0, + reason: 'Build should succeed with exit code 0'); + expect( + result.stderr.toString(), isNot(contains('Failed to build bundle.'))); + }, timeout: const Timeout(Duration(minutes: 5))); + }); } diff --git a/e2e/hooks_test/example/lib/main.dart b/e2e/hooks_test/example/lib/main.dart deleted file mode 100644 index 9984b7c..0000000 --- a/e2e/hooks_test/example/lib/main.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:flutter/material.dart'; -import 'dart:async'; - -import 'package:hooks_test/hooks_test.dart' as hooks_test; - -void main() { - runApp(const MyApp()); -} - -class MyApp extends StatefulWidget { - const MyApp({super.key}); - - @override - State createState() => _MyAppState(); -} - -class _MyAppState extends State { - late int sumResult; - late Future sumAsyncResult; - - @override - void initState() { - super.initState(); - sumResult = hooks_test.sum(1, 2); - sumAsyncResult = hooks_test.sumAsync(3, 4); - } - - @override - Widget build(BuildContext context) { - const textStyle = TextStyle(fontSize: 25); - const spacerSmall = SizedBox(height: 10); - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Native Packages'), - ), - body: SingleChildScrollView( - child: Container( - padding: const .all(10), - child: Column( - children: [ - const Text( - 'This calls a native function through FFI that is shipped as source in the package. ' - 'The native code is built as part of the Flutter Runner build.', - style: textStyle, - textAlign: .center, - ), - spacerSmall, - Text( - 'sum(1, 2) = $sumResult', - style: textStyle, - textAlign: .center, - ), - spacerSmall, - FutureBuilder( - future: sumAsyncResult, - builder: (BuildContext context, AsyncSnapshot value) { - final displayValue = - (value.hasData) ? value.data : 'loading'; - return Text( - 'await sumAsync(3, 4) = $displayValue', - style: textStyle, - textAlign: .center, - ); - }, - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/e2e/hooks_test/.gitignore b/e2e/hooks_test_package/.gitignore similarity index 100% rename from e2e/hooks_test/.gitignore rename to e2e/hooks_test_package/.gitignore diff --git a/e2e/hooks_test/.metadata b/e2e/hooks_test_package/.metadata similarity index 100% rename from e2e/hooks_test/.metadata rename to e2e/hooks_test_package/.metadata diff --git a/e2e/hooks_test/example/.gitignore b/e2e/hooks_test_package/example/.gitignore similarity index 100% rename from e2e/hooks_test/example/.gitignore rename to e2e/hooks_test_package/example/.gitignore diff --git a/e2e/hooks_test/example/README.md b/e2e/hooks_test_package/example/README.md similarity index 100% rename from e2e/hooks_test/example/README.md rename to e2e/hooks_test_package/example/README.md diff --git a/e2e/hooks_test_package/example/lib/main.dart b/e2e/hooks_test_package/example/lib/main.dart new file mode 100644 index 0000000..db923ca --- /dev/null +++ b/e2e/hooks_test_package/example/lib/main.dart @@ -0,0 +1,13 @@ +import 'package:integration_test/integration_test.dart'; +import 'package:hooks_test_package/hooks_test_package.dart' as hooks_test; +import 'package:test/test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + test('native hooks sum works', () async { + expect(hooks_test.sum(2, 1), equals(3)); + + await expectLater(hooks_test.sumAsync(2, 1), equals(3)); + }); +} diff --git a/e2e/hooks_test/example/pubspec.lock b/e2e/hooks_test_package/example/pubspec.lock similarity index 52% rename from e2e/hooks_test/example/pubspec.lock rename to e2e/hooks_test_package/example/pubspec.lock index 150a6a5..96d3f25 100644 --- a/e2e/hooks_test/example/pubspec.lock +++ b/e2e/hooks_test_package/example/pubspec.lock @@ -1,6 +1,30 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d + url: "https://pub.dev" + source: hosted + version: "91.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 + url: "https://pub.dev" + source: hosted + version: "8.4.1" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -25,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" clock: dependency: transitive description: @@ -49,6 +81,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + url: "https://pub.dev" + source: hosted + version: "1.15.0" crypto: dependency: transitive description: @@ -86,6 +134,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_lints: dependency: "direct dev" description: @@ -99,6 +152,19 @@ packages: description: flutter source: sdk version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" glob: dependency: transitive description: @@ -115,13 +181,50 @@ packages: url: "https://pub.dev" source: hosted version: "0.20.5" - hooks_test: + hooks_test_package: dependency: "direct main" description: path: ".." relative: true source: path version: "0.0.1" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + integration_test: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" leak_tracker: dependency: transitive description: @@ -186,6 +289,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" native_toolchain_c: dependency: transitive description: @@ -194,6 +305,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.17.2" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" path: dependency: transitive description: @@ -202,6 +329,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + process: + dependency: transitive + description: + name: process + sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 + url: "https://pub.dev" + source: hosted + version: "5.0.5" pub_semver: dependency: transitive description: @@ -210,11 +361,59 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" source_span: dependency: transitive description: @@ -247,6 +446,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" term_glyph: dependency: transitive description: @@ -255,6 +462,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" + test: + dependency: "direct main" + description: + name: test + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + url: "https://pub.dev" + source: hosted + version: "1.26.3" test_api: dependency: transitive description: @@ -263,6 +478,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.7" + test_core: + dependency: transitive + description: + name: test_core + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + url: "https://pub.dev" + source: hosted + version: "0.6.12" typed_data: dependency: transitive description: @@ -287,6 +510,54 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" yaml: dependency: transitive description: diff --git a/e2e/hooks_test/example/pubspec.yaml b/e2e/hooks_test_package/example/pubspec.yaml similarity index 96% rename from e2e/hooks_test/example/pubspec.yaml rename to e2e/hooks_test_package/example/pubspec.yaml index 7084931..566f8f5 100644 --- a/e2e/hooks_test/example/pubspec.yaml +++ b/e2e/hooks_test_package/example/pubspec.yaml @@ -2,7 +2,7 @@ name: hooks_test_example description: "Demonstrates how to use the hooks_test package." # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: "none" # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 @@ -30,8 +30,10 @@ environment: dependencies: flutter: sdk: flutter + integration_test: + sdk: flutter - hooks_test: + hooks_test_package: # When depending on this package from a real application you should use: # hooks_test: ^x.y.z # See https://dart.dev/tools/pub/dependencies#version-constraints @@ -42,6 +44,7 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 + test: ^1.26.3 dev_dependencies: flutter_test: @@ -59,7 +62,6 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: - # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. diff --git a/e2e/hooks_test/ffigen.yaml b/e2e/hooks_test_package/ffigen.yaml similarity index 100% rename from e2e/hooks_test/ffigen.yaml rename to e2e/hooks_test_package/ffigen.yaml diff --git a/e2e/hooks_test/hook/build.dart b/e2e/hooks_test_package/hook/build.dart similarity index 100% rename from e2e/hooks_test/hook/build.dart rename to e2e/hooks_test_package/hook/build.dart diff --git a/e2e/hooks_test/lib/hooks_test_bindings_generated.dart b/e2e/hooks_test_package/lib/hooks_test_bindings_generated.dart similarity index 100% rename from e2e/hooks_test/lib/hooks_test_bindings_generated.dart rename to e2e/hooks_test_package/lib/hooks_test_bindings_generated.dart diff --git a/e2e/hooks_test/lib/hooks_test.dart b/e2e/hooks_test_package/lib/hooks_test_package.dart similarity index 100% rename from e2e/hooks_test/lib/hooks_test.dart rename to e2e/hooks_test_package/lib/hooks_test_package.dart diff --git a/e2e/hooks_test/pubspec.lock b/e2e/hooks_test_package/pubspec.lock similarity index 100% rename from e2e/hooks_test/pubspec.lock rename to e2e/hooks_test_package/pubspec.lock diff --git a/e2e/hooks_test/pubspec.yaml b/e2e/hooks_test_package/pubspec.yaml similarity index 91% rename from e2e/hooks_test/pubspec.yaml rename to e2e/hooks_test_package/pubspec.yaml index bf60eae..1b318ea 100644 --- a/e2e/hooks_test/pubspec.yaml +++ b/e2e/hooks_test_package/pubspec.yaml @@ -1,4 +1,4 @@ -name: hooks_test +name: hooks_test_package description: "A new Dart FFI package project." version: 0.0.1 diff --git a/e2e/hooks_test/src/hooks_test.c b/e2e/hooks_test_package/src/hooks_test.c similarity index 100% rename from e2e/hooks_test/src/hooks_test.c rename to e2e/hooks_test_package/src/hooks_test.c diff --git a/e2e/hooks_test/src/hooks_test.h b/e2e/hooks_test_package/src/hooks_test.h similarity index 100% rename from e2e/hooks_test/src/hooks_test.h rename to e2e/hooks_test_package/src/hooks_test.h From b1f20349da884a1d785e43f4111a363bcdbd0884 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 7 Feb 2026 16:03:22 +0100 Subject: [PATCH 17/32] fix unsupported dot shorthand --- e2e/hooks_test_package/hook/build.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/e2e/hooks_test_package/hook/build.dart b/e2e/hooks_test_package/hook/build.dart index d8f0bce..ced5905 100644 --- a/e2e/hooks_test_package/hook/build.dart +++ b/e2e/hooks_test_package/hook/build.dart @@ -8,15 +8,13 @@ void main(List args) async { final cbuilder = CBuilder.library( name: packageName, assetName: '${packageName}_bindings_generated.dart', - sources: [ - 'src/$packageName.c', - ], + sources: ['src/$packageName.c'], ); await cbuilder.run( input: input, output: output, logger: Logger('') - ..level = .ALL + ..level = Level.ALL ..onRecord.listen((record) => print(record.message)), ); }); From c22ae35c2556e4fe015e7691995a0da0e8dd6d55 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 7 Feb 2026 16:49:42 +0100 Subject: [PATCH 18/32] provide process output on test failure, for context --- e2e/hooks_test.dart | 201 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 162 insertions(+), 39 deletions(-) diff --git a/e2e/hooks_test.dart b/e2e/hooks_test.dart index ef9f9eb..7e9a8ea 100644 --- a/e2e/hooks_test.dart +++ b/e2e/hooks_test.dart @@ -2,6 +2,130 @@ import 'dart:io'; import 'package:test/test.dart'; +/// Custom matcher that checks if a ProcessResult succeeded +/// and provides helpful error messages with stdout/stderr on failure. +class _ProcessExitedSuccessfully extends Matcher { + _ProcessExitedSuccessfully({ + Object? exitCode, + Object? stdout, + Object? stderr, + }) : exitCode = wrapMatcher(exitCode ?? equals(0)), + stdout = wrapMatcher(stdout ?? anything), + stderr = wrapMatcher(stderr ?? anything); + + final Matcher exitCode; + final Matcher stdout; + final Matcher stderr; + + @override + bool matches(dynamic item, Map matchState) { + if (item is! ProcessResult) return false; + + final exitCode = this.exitCode; + if (!exitCode.matches(item.exitCode, matchState)) { + matchState['exitCodeMatcher'] = exitCode; + matchState['field'] = 'exitCode'; + return false; + } + + if (!stdout.matches(item.stdout, matchState)) { + matchState['stdoutMatcher'] = stdout; + matchState['field'] = 'stdout'; + return false; + } + + if (!stderr.matches(item.stderr, matchState)) { + matchState['stderrMatcher'] = stderr; + matchState['field'] = 'stderr'; + return false; + } + + return true; + } + + @override + Description describe(Description description) { + description.add('process to succeed'); + + description.add(' with exit code '); + exitCode.describe(description); + + description.add(' and stdout '); + stdout.describe(description); + + description.add(' and stderr '); + stderr.describe(description); + + return description; + } + + @override + Description describeMismatch( + dynamic item, + Description mismatchDescription, + Map matchState, + bool verbose, + ) { + if (item is! ProcessResult) { + return mismatchDescription.add('is not a ProcessResult'); + } + + final field = matchState['field'] as String?; + + if (field == 'exitCode') { + final matcher = matchState['exitCodeMatcher'] as Matcher; + mismatchDescription.add('exit code '); + matcher.describeMismatch( + item.exitCode, + mismatchDescription, + matchState, + verbose, + ); + } else if (field == 'stdout') { + final matcher = matchState['stdoutMatcher'] as Matcher; + mismatchDescription.add('stdout '); + matcher.describeMismatch( + item.stdout, + mismatchDescription, + matchState, + verbose, + ); + } else if (field == 'stderr') { + final matcher = matchState['stderrMatcher'] as Matcher; + mismatchDescription.add('stderr '); + matcher.describeMismatch( + item.stderr, + mismatchDescription, + matchState, + verbose, + ); + } + + // Always include the full process output for context + return mismatchDescription + .add('\n\nProcess output:') + .add('\nexit code: ${item.exitCode}') + .add('\nstdout: ${item.stdout}') + .add('\nstderr: ${item.stderr}'); + } +} + +Matcher exitedSuccessfully({ + Matcher? exitCode, + Matcher? stdout, + Matcher? stderr, +}) => + _ProcessExitedSuccessfully( + exitCode: exitCode, + stdout: stdout, + stderr: stderr, + ); + +Matcher buildExitedSuccessfully() => _ProcessExitedSuccessfully( + stdout: isNot(contains('Failed to build bundle.')), + stderr: isNot(contains('Failed to build bundle.')), + ); + void main() { group('hooks_test native assets e2e', () { const exampleDir = 'e2e/hooks_test_package/example'; @@ -10,53 +134,52 @@ void main() { // Ensure dependencies are installed final pubGetResult = await Process.run( 'flutter', - ['pub', 'get', '--enforce-lockfile'], + ['pub', 'get'], workingDirectory: exampleDir, ); - expect( - pubGetResult.exitCode, - 0, - reason: 'flutter pub get should succeed', - ); + expect(pubGetResult, exitedSuccessfully()); }); - test('builds successfully in debug mode', () async { - final result = await Process.run( - 'flutterpi_tool', - ['build', '--debug'], - workingDirectory: exampleDir, - ); + test( + 'builds successfully in debug mode', + () async { + final result = await Process.run( + 'flutterpi_tool', + ['build', '--debug'], + workingDirectory: exampleDir, + ); - expect(result.exitCode, 0, - reason: 'Build should succeed with exit code 0'); - expect( - result.stderr.toString(), isNot(contains('Failed to build bundle.'))); - }, timeout: const Timeout(Duration(minutes: 5))); + expect(result, buildExitedSuccessfully()); + }, + timeout: const Timeout(Duration(minutes: 5)), + ); - test('builds successfully in profile mode', () async { - final result = await Process.run( - 'flutterpi_tool', - ['build', '--profile'], - workingDirectory: exampleDir, - ); + test( + 'builds successfully in profile mode', + () async { + final result = await Process.run( + 'flutterpi_tool', + ['build', '--profile'], + workingDirectory: exampleDir, + ); - expect(result.exitCode, 0, - reason: 'Build should succeed with exit code 0'); - expect( - result.stderr.toString(), isNot(contains('Failed to build bundle.'))); - }, timeout: const Timeout(Duration(minutes: 5))); + expect(result, buildExitedSuccessfully()); + }, + timeout: const Timeout(Duration(minutes: 5)), + ); - test('builds successfully in release mode', () async { - final result = await Process.run( - 'flutterpi_tool', - ['build', '--release'], - workingDirectory: exampleDir, - ); + test( + 'builds successfully in release mode', + () async { + final result = await Process.run( + 'flutterpi_tool', + ['build', '--release'], + workingDirectory: exampleDir, + ); - expect(result.exitCode, 0, - reason: 'Build should succeed with exit code 0'); - expect( - result.stderr.toString(), isNot(contains('Failed to build bundle.'))); - }, timeout: const Timeout(Duration(minutes: 5))); + expect(result, buildExitedSuccessfully()); + }, + timeout: const Timeout(Duration(minutes: 5)), + ); }); } From 535eb9a45374f97c0af5c032ff0f95e0dda017da Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 7 Feb 2026 16:52:47 +0100 Subject: [PATCH 19/32] lower e2e test dart constraint to 3.10.3 --- e2e/hooks_test_package/example/pubspec.lock | 2 +- e2e/hooks_test_package/example/pubspec.yaml | 2 +- e2e/hooks_test_package/pubspec.lock | 2 +- e2e/hooks_test_package/pubspec.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/e2e/hooks_test_package/example/pubspec.lock b/e2e/hooks_test_package/example/pubspec.lock index 96d3f25..b6c0de3 100644 --- a/e2e/hooks_test_package/example/pubspec.lock +++ b/e2e/hooks_test_package/example/pubspec.lock @@ -567,5 +567,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.10.7 <4.0.0" + dart: ">=3.10.3 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/e2e/hooks_test_package/example/pubspec.yaml b/e2e/hooks_test_package/example/pubspec.yaml index 566f8f5..7f51b01 100644 --- a/e2e/hooks_test_package/example/pubspec.yaml +++ b/e2e/hooks_test_package/example/pubspec.yaml @@ -19,7 +19,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ^3.10.7 + sdk: ^3.10.3 # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions diff --git a/e2e/hooks_test_package/pubspec.lock b/e2e/hooks_test_package/pubspec.lock index ecbc47d..87753b6 100644 --- a/e2e/hooks_test_package/pubspec.lock +++ b/e2e/hooks_test_package/pubspec.lock @@ -458,4 +458,4 @@ packages: source: hosted version: "2.2.3" sdks: - dart: ">=3.10.7 <4.0.0" + dart: ">=3.10.3 <4.0.0" diff --git a/e2e/hooks_test_package/pubspec.yaml b/e2e/hooks_test_package/pubspec.yaml index 1b318ea..6de7b3f 100644 --- a/e2e/hooks_test_package/pubspec.yaml +++ b/e2e/hooks_test_package/pubspec.yaml @@ -3,7 +3,7 @@ description: "A new Dart FFI package project." version: 0.0.1 environment: - sdk: ^3.10.7 + sdk: ^3.10.3 dependencies: code_assets: ^0.19.7 From 85658336379dc42f22987370fe41de296a2c249d Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 7 Feb 2026 16:56:16 +0100 Subject: [PATCH 20/32] fix e2e hooks build --- e2e/hooks_test_package/hook/build.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/hooks_test_package/hook/build.dart b/e2e/hooks_test_package/hook/build.dart index ced5905..3c82887 100644 --- a/e2e/hooks_test_package/hook/build.dart +++ b/e2e/hooks_test_package/hook/build.dart @@ -7,8 +7,8 @@ void main(List args) async { final packageName = input.packageName; final cbuilder = CBuilder.library( name: packageName, - assetName: '${packageName}_bindings_generated.dart', - sources: ['src/$packageName.c'], + assetName: 'hooks_test_bindings_generated.dart', + sources: ['src/hooks_test.c'], ); await cbuilder.run( input: input, From d1eff8a38bf0c058e02705e06e145a9a59e89c3a Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 7 Feb 2026 23:21:36 +0100 Subject: [PATCH 21/32] use dart analyze, don't fail fast --- .github/workflows/flutter.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/flutter.yml b/.github/workflows/flutter.yml index 0446d64..fb0b9fd 100644 --- a/.github/workflows/flutter.yml +++ b/.github/workflows/flutter.yml @@ -32,13 +32,13 @@ jobs: flutter-version: ${{ env.FLUTTER_VERSION }} - name: Install dependencies - run: flutter pub get + run: dart pub get - name: Verify formatting run: dart format --output=none --set-exit-if-changed . - - name: Analyze project source - run: flutter analyze + - name: Analyze + run: dart analyze test: runs-on: ${{ matrix.runner }} @@ -65,8 +65,8 @@ jobs: - name: Run tests run: flutter test - e2e-test: - name: End-to-End Tests (${{ matrix.runner }}) + e2e: + name: E2E Tests (${{ matrix.runner }}) runs-on: ${{ matrix.runner }} env: GITHUB_TOKEN: ${{ secrets.PUBLIC_ONLY_PAT }} @@ -76,6 +76,7 @@ jobs: - windows-latest - ubuntu-latest - macos-latest + fail-fast: false steps: - uses: actions/checkout@v4 From 258283d97ad926f5328233d947325277bf66c9d6 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 7 Feb 2026 23:52:11 +0100 Subject: [PATCH 22/32] analyze e2e tests separately --- .github/workflows/flutter.yml | 15 +++++++++++++++ analysis_options.yaml | 3 +++ 2 files changed, 18 insertions(+) diff --git a/.github/workflows/flutter.yml b/.github/workflows/flutter.yml index fb0b9fd..79f9543 100644 --- a/.github/workflows/flutter.yml +++ b/.github/workflows/flutter.yml @@ -40,6 +40,21 @@ jobs: - name: Analyze run: dart analyze + - name: Analyze e2e/hooks_test_package + working-directory: e2e/hooks_test_package + run: | + echo '::group::dart pub get' + dart pub get --enforce-lockfile + echo '::endgroup::' + + echo '::group::dart format' + dart format --output=none --set-exit-if-changed . + echo '::endgroup::' + + echo '::group::dart analyze' + dart analyze + echo '::endgroup::' + test: runs-on: ${{ matrix.runner }} env: diff --git a/analysis_options.yaml b/analysis_options.yaml index 0c09e66..7d871df 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -21,6 +21,9 @@ analyzer: exclude: - lib/src/archive + # separate package with additional dependencies not contained + # in the root pubspec + - e2e/hooks_test_package # For more information about the core and recommended set of lints, see # https://dart.dev/go/core-lints From 8d17304e39119d404359b95907573e121caeafe5 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sat, 7 Feb 2026 23:59:02 +0100 Subject: [PATCH 23/32] format hooks_test_package --- .../lib/hooks_test_bindings_generated.dart | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/e2e/hooks_test_package/lib/hooks_test_bindings_generated.dart b/e2e/hooks_test_package/lib/hooks_test_bindings_generated.dart index 65642b4..d61c422 100644 --- a/e2e/hooks_test_package/lib/hooks_test_bindings_generated.dart +++ b/e2e/hooks_test_package/lib/hooks_test_bindings_generated.dart @@ -13,10 +13,7 @@ import 'dart:ffi' as ffi; /// They will block the Dart execution while running the native function, so /// only do this for native functions which are guaranteed to be short-lived. @ffi.Native() -external int sum( - int a, - int b, -); +external int sum(int a, int b); /// A longer lived native function, which occupies the thread calling it. /// @@ -24,7 +21,4 @@ external int sum( /// block Dart execution. This will cause dropped frames in Flutter applications. /// Instead, call these native functions on a separate isolate. @ffi.Native() -external int sum_long_running( - int a, - int b, -); +external int sum_long_running(int a, int b); From f1c56e8947d42ba5823472471ff3bae3a0831888 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sun, 8 Feb 2026 00:06:36 +0100 Subject: [PATCH 24/32] skip hooks tests on windows and macos, for now. We can't really use hooks there. --- e2e/hooks_test.dart | 110 ++++++++++++++++++++++++-------------------- 1 file changed, 59 insertions(+), 51 deletions(-) diff --git a/e2e/hooks_test.dart b/e2e/hooks_test.dart index 7e9a8ea..1ddb083 100644 --- a/e2e/hooks_test.dart +++ b/e2e/hooks_test.dart @@ -127,59 +127,67 @@ Matcher buildExitedSuccessfully() => _ProcessExitedSuccessfully( ); void main() { - group('hooks_test native assets e2e', () { - const exampleDir = 'e2e/hooks_test_package/example'; - - setUpAll(() async { - // Ensure dependencies are installed - final pubGetResult = await Process.run( - 'flutter', - ['pub', 'get'], - workingDirectory: exampleDir, - ); - expect(pubGetResult, exitedSuccessfully()); - }); - - test( - 'builds successfully in debug mode', - () async { - final result = await Process.run( - 'flutterpi_tool', - ['build', '--debug'], + // Only tested on linux right now, see testOn below. + // On windows and macOS it's hard to get a working linux + // cross compiler. Best thing we can do maybe is just + // verify we provide an understandable error message. + group( + 'hooks_test native assets e2e', + () { + const exampleDir = 'e2e/hooks_test_package/example'; + + setUpAll(() async { + // Ensure dependencies are installed + final pubGetResult = await Process.run( + 'flutter', + ['pub', 'get'], workingDirectory: exampleDir, ); + expect(pubGetResult, exitedSuccessfully()); + }); + + test( + 'builds successfully in debug mode', + () async { + final result = await Process.run( + 'flutterpi_tool', + ['build', '--debug'], + workingDirectory: exampleDir, + ); + + expect(result, buildExitedSuccessfully()); + }, + timeout: const Timeout(Duration(minutes: 5)), + ); - expect(result, buildExitedSuccessfully()); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - - test( - 'builds successfully in profile mode', - () async { - final result = await Process.run( - 'flutterpi_tool', - ['build', '--profile'], - workingDirectory: exampleDir, - ); - - expect(result, buildExitedSuccessfully()); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - - test( - 'builds successfully in release mode', - () async { - final result = await Process.run( - 'flutterpi_tool', - ['build', '--release'], - workingDirectory: exampleDir, - ); + test( + 'builds successfully in profile mode', + () async { + final result = await Process.run( + 'flutterpi_tool', + ['build', '--profile'], + workingDirectory: exampleDir, + ); + + expect(result, buildExitedSuccessfully()); + }, + timeout: const Timeout(Duration(minutes: 5)), + ); - expect(result, buildExitedSuccessfully()); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - }); + test( + 'builds successfully in release mode', + () async { + final result = await Process.run( + 'flutterpi_tool', + ['build', '--release'], + workingDirectory: exampleDir, + ); + + expect(result, buildExitedSuccessfully()); + }, + timeout: const Timeout(Duration(minutes: 5)), + ); + }, + testOn: 'linux', + ); } From 3ef389e80e3fd14618fc93d791d5d31a6ff32893 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sun, 8 Feb 2026 06:38:41 +0100 Subject: [PATCH 25/32] Revert "remove devcontainer" This reverts commit 4cb3ad768de7e7a0e84cb3b570116c8b3422301b. --- .devcontainer/Dockerfile | 15 +++++++++++++++ .devcontainer/devcontainer.json | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..034392e --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,15 @@ +FROM mcr.microsoft.com/devcontainers/base:debian + +# Install needed packages +RUN apt-get update && apt-get install -y curl git unzip xz-utils zip + +USER 1000:1000 + +ARG FLUTTER_VERSION=3.22.1 +RUN git clone -b $FLUTTER_VERSION https://github.com/flutter/flutter.git /home/vscode/flutter + +ENV PATH /home/vscode/flutter/bin:/home/vscode/.pub-cache/bin:$PATH + +RUN flutter precache + +USER root diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..d2721df --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,33 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/debian +{ + "name": "Debian", + + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "build": { + // Path is relative to the devcontainer.json file. + "dockerfile": "Dockerfile" + }, + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Configure tool-specific properties. + // "customizations": {}, + + "customizations": { + "vscode": { + "extensions": [ + "Dart-Code.flutter" + ] + } + }, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" + + "postStartCommand": "flutter pub global activate -spath /workspaces/flutterpi_tool" +} From 35a83352107f3e1fa5c8464f8c5ef10ba8108c76 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sun, 8 Feb 2026 06:39:39 +0100 Subject: [PATCH 26/32] update devcontainer to flutter 3.38.3 --- .devcontainer/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 034392e..59e4c99 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,11 +1,11 @@ FROM mcr.microsoft.com/devcontainers/base:debian # Install needed packages -RUN apt-get update && apt-get install -y curl git unzip xz-utils zip +RUN apt-get update && apt-get install -y curl git unzip xz-utils zip nano USER 1000:1000 -ARG FLUTTER_VERSION=3.22.1 +ARG FLUTTER_VERSION=3.38.3 RUN git clone -b $FLUTTER_VERSION https://github.com/flutter/flutter.git /home/vscode/flutter ENV PATH /home/vscode/flutter/bin:/home/vscode/.pub-cache/bin:$PATH From c06bc864142b07c085786e2d697a10a388d9bfe4 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Sun, 8 Feb 2026 09:21:52 +0100 Subject: [PATCH 27/32] add more e2e tests make sure the preconditions are correct: - flutter is available - flutterpi_tool is globally activated from the path being tested verify the code assets are actually present in the build bundle. (which is currently not the case) --- e2e/hooks_test.dart | 186 +++++++++++++++++++++--------- lib/src/build_system/targets.dart | 2 + 2 files changed, 134 insertions(+), 54 deletions(-) diff --git a/e2e/hooks_test.dart b/e2e/hooks_test.dart index 1ddb083..36e2ec0 100644 --- a/e2e/hooks_test.dart +++ b/e2e/hooks_test.dart @@ -1,15 +1,14 @@ import 'dart:io'; +import 'dart:math'; +import 'package:path/path.dart' as pathlib; import 'package:test/test.dart'; /// Custom matcher that checks if a ProcessResult succeeded /// and provides helpful error messages with stdout/stderr on failure. class _ProcessExitedSuccessfully extends Matcher { - _ProcessExitedSuccessfully({ - Object? exitCode, - Object? stdout, - Object? stderr, - }) : exitCode = wrapMatcher(exitCode ?? equals(0)), + _ProcessExitedSuccessfully({Object? exitCode, Object? stdout, Object? stderr}) + : exitCode = wrapMatcher(exitCode ?? equals(0)), stdout = wrapMatcher(stdout ?? anything), stderr = wrapMatcher(stderr ?? anything); @@ -127,67 +126,146 @@ Matcher buildExitedSuccessfully() => _ProcessExitedSuccessfully( ); void main() { + setUpAll(() async { + expect( + await Process.run('flutter', ['--version']), + exitedSuccessfully(), + reason: 'flutter must be available in PATH.', + ); + + expect( + await Process.run('flutterpi_tool', ['--version']), + exitedSuccessfully(), + reason: + 'flutterpi_tool must be globally activated and available in PATH.', + ); + + final result = await Process.run('dart', ['pub', 'global', 'list']); + expect( + result, + exitedSuccessfully(), + reason: 'dart must be available in PATH.', + ); + + final regex = RegExp(r'flutterpi_tool [^ ]+ at path "([^"]+)"'); + expect(result.stdout, matches(regex)); + + final match = regex.firstMatch(result.stdout); + expect( + Directory(match!.group(1)!).resolveSymbolicLinksSync(), + pathlib.canonicalize(Directory.current.path), + reason: + 'flutterpi_tool should be globally activated from the same path that is currently being tested.', + ); + }); + // Only tested on linux right now, see testOn below. // On windows and macOS it's hard to get a working linux // cross compiler. Best thing we can do maybe is just // verify we provide an understandable error message. - group( - 'hooks_test native assets e2e', - () { - const exampleDir = 'e2e/hooks_test_package/example'; - - setUpAll(() async { - // Ensure dependencies are installed - final pubGetResult = await Process.run( + group('hooks_test native assets e2e', () { + const exampleDir = 'e2e/hooks_test_package/example'; + + final testAssetFlutterPi = File(pathlib.join( + exampleDir, + 'build', + 'flutter-pi', + 'armv7-generic', + 'libhooks_test_package.so', + )); + + final testAssetMetaFlutter = File(pathlib.join( + exampleDir, + 'build', + 'flutter-pi', + 'armv7-generic-meta-flutter', + 'lib', + 'libhooks_test_package.so', + )); + + setUp(() async { + // Ensure dependencies are installed + final pubGetResult = await Process.run( 'flutter', - ['pub', 'get'], - workingDirectory: exampleDir, - ); - expect(pubGetResult, exitedSuccessfully()); - }); + [ + 'pub', + 'get', + '--enforce-lockfile', + ], + workingDirectory: exampleDir); + expect(pubGetResult, exitedSuccessfully()); + }); + + tearDown(() async { + final clean = + await Process.run('flutter', ['clean'], workingDirectory: exampleDir); + expect(clean, exitedSuccessfully()); + }); - test( - 'builds successfully in debug mode', - () async { - final result = await Process.run( + test( + 'builds successfully in debug mode', + () async { + final result = await Process.run( 'flutterpi_tool', - ['build', '--debug'], - workingDirectory: exampleDir, - ); + [ + 'build', + '--debug', + ], + workingDirectory: exampleDir); - expect(result, buildExitedSuccessfully()); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); + expect(result, buildExitedSuccessfully()); + expect( + testAssetFlutterPi.existsSync(), + isTrue, + reason: + 'libhooks_test_package.so code asset should be present in bundle', + ); + }, + timeout: const Timeout(Duration(minutes: 5)), + ); - test( - 'builds successfully in profile mode', - () async { - final result = await Process.run( + test( + 'builds successfully in profile mode', + () async { + final result = await Process.run( 'flutterpi_tool', - ['build', '--profile'], - workingDirectory: exampleDir, - ); + [ + 'build', + '--profile', + ], + workingDirectory: exampleDir); - expect(result, buildExitedSuccessfully()); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); + expect(result, buildExitedSuccessfully()); + expect( + testAssetFlutterPi.existsSync(), + isTrue, + reason: + 'libhooks_test_package.so code asset should be present in bundle', + ); + }, + timeout: const Timeout(Duration(minutes: 5)), + ); - test( - 'builds successfully in release mode', - () async { - final result = await Process.run( + test( + 'builds successfully in release mode', + () async { + final result = await Process.run( 'flutterpi_tool', - ['build', '--release'], - workingDirectory: exampleDir, - ); + [ + 'build', + '--release', + ], + workingDirectory: exampleDir); - expect(result, buildExitedSuccessfully()); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - }, - testOn: 'linux', - ); + expect(result, buildExitedSuccessfully()); + expect( + testAssetFlutterPi.existsSync(), + isTrue, + reason: + 'libhooks_test_package.so code asset should be present in bundle', + ); + }, + timeout: const Timeout(Duration(minutes: 5)), + ); + }, testOn: 'linux'); } diff --git a/lib/src/build_system/targets.dart b/lib/src/build_system/targets.dart index e3544a0..40c3524 100644 --- a/lib/src/build_system/targets.dart +++ b/lib/src/build_system/targets.dart @@ -479,6 +479,8 @@ class CopyFlutterAssets extends Target { @override List get dependencies => [ const KernelSnapshot(), + const DartBuildForNative(), + const InstallCodeAssets(), ]; @override From 09db39b09d88443d456e84b2ea9aacc61d80099f Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Mon, 9 Feb 2026 01:09:58 +0100 Subject: [PATCH 28/32] fix bundling native assets and improve e2e testing. --- e2e/hooks_test.dart | 267 +++++++++++++++++++----------- lib/src/build_system/targets.dart | 105 +++++++++++- 2 files changed, 271 insertions(+), 101 deletions(-) diff --git a/e2e/hooks_test.dart b/e2e/hooks_test.dart index 36e2ec0..6a895a9 100644 --- a/e2e/hooks_test.dart +++ b/e2e/hooks_test.dart @@ -1,3 +1,4 @@ +import 'dart:ffi'; import 'dart:io'; import 'dart:math'; @@ -125,6 +126,72 @@ Matcher buildExitedSuccessfully() => _ProcessExitedSuccessfully( stderr: isNot(contains('Failed to build bundle.')), ); +Iterable<(A, B)> cartesianProduct2(Iterable a, Iterable b) sync* { + for (final fst in a) { + for (final snd in b) { + yield (fst, snd); + } + } +} + +Iterable<(A, B, C)> cartesianProduct3( + Iterable a, + Iterable b, + Iterable c, +) sync* { + for (final fst in a) { + for (final snd in b) { + for (final thrd in c) { + yield (fst, snd, thrd); + } + } + } +} + +enum Arch { + arm, + arm64, + ia32, + x64, + riscv32, + riscv64; + + factory Arch.fromAbi(Abi abi) { + return switch (abi) { + Abi.androidArm => Arch.arm, + Abi.androidArm64 => Arch.arm64, + Abi.androidIA32 => Arch.ia32, + Abi.androidX64 => Arch.x64, + Abi.androidRiscv64 => Arch.riscv64, + Abi.fuchsiaArm64 => Arch.arm64, + Abi.fuchsiaX64 => Arch.x64, + Abi.fuchsiaRiscv64 => Arch.riscv64, + Abi.iosArm => Arch.arm, + Abi.iosArm64 => Arch.arm64, + Abi.iosX64 => Arch.x64, + Abi.linuxArm => Arch.arm, + Abi.linuxArm64 => Arch.arm64, + Abi.linuxIA32 => Arch.ia32, + Abi.linuxX64 => Arch.x64, + Abi.linuxRiscv32 => Arch.riscv32, + Abi.linuxRiscv64 => Arch.riscv64, + Abi.macosArm64 => Arch.arm64, + Abi.macosX64 => Arch.x64, + Abi.windowsArm64 => Arch.arm64, + Abi.windowsIA32 => Arch.ia32, + Abi.windowsX64 => Arch.x64, + _ => throw ArgumentError.value(abi, 'abi', 'unsupported abi') + }; + } + + factory Arch.current() { + return Arch.fromAbi(Abi.current()); + } + + @override + String toString() => name; +} + void main() { setUpAll(() async { expect( @@ -140,6 +207,12 @@ void main() { 'flutterpi_tool must be globally activated and available in PATH.', ); + expect( + await Process.run('file', ['--version']), + exitedSuccessfully(), + reason: '"file" utility must be available in PATH.', + ); + final result = await Process.run('dart', ['pub', 'global', 'list']); expect( result, @@ -163,109 +236,107 @@ void main() { // On windows and macOS it's hard to get a working linux // cross compiler. Best thing we can do maybe is just // verify we provide an understandable error message. - group('hooks_test native assets e2e', () { - const exampleDir = 'e2e/hooks_test_package/example'; - - final testAssetFlutterPi = File(pathlib.join( - exampleDir, - 'build', - 'flutter-pi', - 'armv7-generic', - 'libhooks_test_package.so', - )); - - final testAssetMetaFlutter = File(pathlib.join( - exampleDir, - 'build', - 'flutter-pi', - 'armv7-generic-meta-flutter', - 'lib', - 'libhooks_test_package.so', - )); - - setUp(() async { - // Ensure dependencies are installed - final pubGetResult = await Process.run( - 'flutter', - [ - 'pub', - 'get', - '--enforce-lockfile', - ], - workingDirectory: exampleDir); - expect(pubGetResult, exitedSuccessfully()); - }); - - tearDown(() async { - final clean = - await Process.run('flutter', ['clean'], workingDirectory: exampleDir); - expect(clean, exitedSuccessfully()); - }); - - test( - 'builds successfully in debug mode', - () async { - final result = await Process.run( - 'flutterpi_tool', + group( + 'hooks_test native assets e2e', + () { + const exampleDir = 'e2e/hooks_test_package/example'; + + setUp(() async { + // Ensure dependencies are installed + final pubGetResult = await Process.run( + 'flutter', [ - 'build', - '--debug', + 'pub', + 'get', + '--enforce-lockfile', ], workingDirectory: exampleDir); + expect(pubGetResult, exitedSuccessfully()); + }); - expect(result, buildExitedSuccessfully()); - expect( - testAssetFlutterPi.existsSync(), - isTrue, - reason: - 'libhooks_test_package.so code asset should be present in bundle', - ); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - - test( - 'builds successfully in profile mode', - () async { - final result = await Process.run( - 'flutterpi_tool', - [ - 'build', - '--profile', - ], + tearDown(() async { + final clean = await Process.run('flutter', ['clean'], workingDirectory: exampleDir); - - expect(result, buildExitedSuccessfully()); - expect( - testAssetFlutterPi.existsSync(), - isTrue, - reason: - 'libhooks_test_package.so code asset should be present in bundle', - ); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - - test( - 'builds successfully in release mode', - () async { - final result = await Process.run( - 'flutterpi_tool', - [ + expect(clean, exitedSuccessfully()); + }); + + for (final (flavor, arch, layout) in cartesianProduct3( + ['debug', 'profile', 'release'], + [Arch.arm, Arch.arm64, Arch.x64, Arch.riscv64], + ['flutter-pi', 'meta-flutter'], + )) { + test( + 'builds successfully in $flavor, for $arch, $layout layout', + () async { + if (arch == Arch.arm) { + return markTestSkipped( + 'armv7 does not currently support native assets', + ); + } else if (arch != Arch.current()) { + return markTestSkipped( + 'cross-compiling native assets is not currently supported', + ); + } + + final result = await Process.run( + 'flutterpi_tool', + [ + 'build', + '--$flavor', + '--arch=$arch', + '--fs-layout=$layout', + ], + workingDirectory: exampleDir, + ); + + final layoutPart = switch (layout) { + 'flutter-pi' => '', + 'meta-flutter' => 'meta-flutter-', + _ => fail('unexpected layout: $layout') + }; + final archPart = switch (arch) { + Arch.arm => 'armv7-generic', + Arch.arm64 => 'aarch64-generic', + Arch.ia32 => 'i386-generic', + Arch.x64 => 'x86_64-generic', + Arch.riscv32 => 'riscv32-generic', + Arch.riscv64 => 'riscv64-generic', + }; + final outputDir = pathlib.join( + exampleDir, 'build', - '--release', - ], - workingDirectory: exampleDir); - - expect(result, buildExitedSuccessfully()); - expect( - testAssetFlutterPi.existsSync(), - isTrue, - reason: - 'libhooks_test_package.so code asset should be present in bundle', + 'flutter-pi', + '$layoutPart$archPart', + ); + final testAsset = switch (layout) { + 'flutter-pi' => + pathlib.join(outputDir, 'libhooks_test_package.so'), + 'meta-flutter' => + pathlib.join(outputDir, 'lib', 'libhooks_test_package.so'), + _ => fail('unexpected layout: $layout'), + }; + + expect(result, buildExitedSuccessfully()); + expect( + File(testAsset).existsSync(), + isTrue, + reason: + 'libhooks_test_package.so code asset should be present in bundle', + ); + + final fileResult = await Process.run('file', [testAsset]); + expect(fileResult, exitedSuccessfully()); + + final fileArchMatcher = switch (arch) { + Arch.arm64 => contains('ARM aarch64'), + _ => isNot(anything), + }; + expect(fileResult.stdout, fileArchMatcher); + }, + timeout: const Timeout(Duration(minutes: 5)), ); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - }, testOn: 'linux'); + } + }, + testOn: 'linux', + ); } diff --git a/lib/src/build_system/targets.dart b/lib/src/build_system/targets.dart index 40c3524..00b3147 100644 --- a/lib/src/build_system/targets.dart +++ b/lib/src/build_system/targets.dart @@ -7,6 +7,9 @@ import 'package:flutterpi_tool/src/build_system/extended_environment.dart'; import 'package:flutterpi_tool/src/cli/flutterpi_command.dart'; import 'package:flutterpi_tool/src/common.dart'; import 'package:flutterpi_tool/src/fltool/common.dart'; +import 'package:flutterpi_tool/src/fltool/common.dart' as fl; +import 'package:flutter_tools/src/isolated/native_assets/native_assets.dart' + as fl; import 'package:flutterpi_tool/src/fltool/globals.dart'; import 'package:flutterpi_tool/src/more_os_utils.dart'; @@ -20,6 +23,7 @@ class ReleaseBundleFlutterpiAssets extends CompositeTarget { CopyFlutterAssets( layout: layout, buildMode: BuildMode.release, + target: target, ), CopyIcudtl(layout: layout), const DartBuildForNative(), @@ -60,6 +64,7 @@ class ProfileBundleFlutterpiAssets extends CompositeTarget { CopyFlutterAssets( layout: layout, buildMode: BuildMode.profile, + target: target, ), CopyIcudtl(layout: layout), const DartBuildForNative(), @@ -100,6 +105,7 @@ class DebugBundleFlutterpiAssets extends CompositeTarget { CopyFlutterAssets( layout: layout, buildMode: BuildMode.debug, + target: target, ), CopyIcudtl(layout: layout), const DartBuildForNative(), @@ -468,19 +474,23 @@ class CopyFlutterAssets extends Target { const CopyFlutterAssets({ required this.layout, required this.buildMode, + required this.target, }); final FilesystemLayout layout; final BuildMode buildMode; + final FlutterpiTargetPlatform target; @override - String get name => 'copy_flutterpi_assets_${layout}_$buildMode'; + String get name => 'copy_flutterpi_assets_${layout}_${buildMode}_$target'; @override List get dependencies => [ const KernelSnapshot(), - const DartBuildForNative(), - const InstallCodeAssets(), + CopyCodeAssets( + target: target, + layout: layout, + ) ]; @override @@ -561,3 +571,92 @@ class CopyFlutterAssets extends Target { ); } } + +/// Inspired by flutter_tools InstallCodeAssets, +/// but install into output folder directly, instead of +/// into build/native_asets +class CopyCodeAssets extends Target { + const CopyCodeAssets({ + required this.target, + required this.layout, + }); + + final FlutterpiTargetPlatform target; + final FilesystemLayout layout; + + @override + Future build(Environment environment) async { + final projectUri = environment.projectDir.uri; + final fs = environment.fileSystem; + + // And install/copy the code assets to the right place and create a + // native_asset.yaml that can be used by the final AOT compilation. + final nativeAssetsJson = + environment.buildDir.childFile(nativeAssetsFilename); + + final nativeAssetsDir = + fs.directory(fl.nativeAssetsBuildUri(projectUri, 'linux')); + + assert(fs.file(nativeAssetsJson).existsSync()); + + final outputDir = switch (layout) { + FilesystemLayout.flutterPi => environment.outputDir, + FilesystemLayout.metaFlutter => + environment.outputDir.childDirectory('lib'), + }; + + final inputs = []; + final outputs = []; + + final outputNativeAssetsJson = + fs.file(environment.outputDir.childFile(nativeAssetsFilename)); + + await nativeAssetsJson.copy(outputNativeAssetsJson.path); + inputs.add(nativeAssetsJson); + outputs.add(outputNativeAssetsJson); + + // copy all files from build/native_assets/linux to build/flutter-pi/.../ + await Future.wait( + nativeAssetsDir.listSync().map((entity) async { + if (entity is! File) return; + + inputs.add(entity); + outputs.add(entity); + + entity.copy(outputDir.childFile(entity.basename).path); + }), + eagerError: true, + ); + + final outputDepfile = environment.buildDir.childFile(depFilename); + environment.depFileService.writeToFile( + fl.Depfile(inputs, outputs), + outputDepfile, + ); + if (!outputDepfile.existsSync()) { + throwToolExit("${outputDepfile.path} doesn't exist."); + } + } + + @override + List get depfiles => [depFilename]; + + @override + List get dependencies => const [InstallCodeAssets()]; + + @override + List get inputs => const [ + Source.pattern('{BUILD_DIR}/$nativeAssetsFilename'), + ]; + + @override + String get name => 'copy_code_assets'; + + @override + List get outputs => const [ + Source.pattern('{OUTPUT_DIR}/$nativeAssetsFilename'), + ]; + + static const nativeAssetsFilename = 'native_assets.json'; + static const depFilename = 'copy_code_assets.d'; +} From 628428899f1cfa8c3236f20c750efcabe0352a17 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Mon, 9 Feb 2026 01:26:40 +0100 Subject: [PATCH 29/32] add an empty test case to runs where all tests are skipped succeed --- e2e/hooks_test.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/e2e/hooks_test.dart b/e2e/hooks_test.dart index 6a895a9..fbadfa4 100644 --- a/e2e/hooks_test.dart +++ b/e2e/hooks_test.dart @@ -339,4 +339,6 @@ void main() { }, testOn: 'linux', ); + + test('empty test to make all-skipped runs succeed', () {}); } From f07ad44600bb60a7b8657ac692a583dba558736d Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Mon, 9 Feb 2026 01:31:53 +0100 Subject: [PATCH 30/32] fix analysis issues --- e2e/hooks_test.dart | 23 +++++++++++++---------- lib/src/build_system/targets.dart | 2 +- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/e2e/hooks_test.dart b/e2e/hooks_test.dart index fbadfa4..5393c36 100644 --- a/e2e/hooks_test.dart +++ b/e2e/hooks_test.dart @@ -1,6 +1,5 @@ import 'dart:ffi'; import 'dart:io'; -import 'dart:math'; import 'package:path/path.dart' as pathlib; import 'package:test/test.dart'; @@ -244,19 +243,23 @@ void main() { setUp(() async { // Ensure dependencies are installed final pubGetResult = await Process.run( - 'flutter', - [ - 'pub', - 'get', - '--enforce-lockfile', - ], - workingDirectory: exampleDir); + 'flutter', + [ + 'pub', + 'get', + '--enforce-lockfile', + ], + workingDirectory: exampleDir, + ); expect(pubGetResult, exitedSuccessfully()); }); tearDown(() async { - final clean = await Process.run('flutter', ['clean'], - workingDirectory: exampleDir); + final clean = await Process.run( + 'flutter', + ['clean'], + workingDirectory: exampleDir, + ); expect(clean, exitedSuccessfully()); }); diff --git a/lib/src/build_system/targets.dart b/lib/src/build_system/targets.dart index 00b3147..c1a43e9 100644 --- a/lib/src/build_system/targets.dart +++ b/lib/src/build_system/targets.dart @@ -490,7 +490,7 @@ class CopyFlutterAssets extends Target { CopyCodeAssets( target: target, layout: layout, - ) + ), ]; @override From 4393c7d678158d9a9b7183743a4134e87d658679 Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Mon, 9 Feb 2026 01:38:34 +0100 Subject: [PATCH 31/32] also test on ubuntu arm64 --- .github/workflows/flutter.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/flutter.yml b/.github/workflows/flutter.yml index 79f9543..7dbfeac 100644 --- a/.github/workflows/flutter.yml +++ b/.github/workflows/flutter.yml @@ -90,6 +90,7 @@ jobs: runner: - windows-latest - ubuntu-latest + - ubuntu-24.04-arm - macos-latest fail-fast: false steps: From b97d2dd50defc72f88a32c42d2f33fc01d34049d Mon Sep 17 00:00:00 2001 From: Hannes Winkler Date: Mon, 9 Feb 2026 01:46:31 +0100 Subject: [PATCH 32/32] use main channel for linux arm64 --- .github/workflows/flutter.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/flutter.yml b/.github/workflows/flutter.yml index 7dbfeac..45432ed 100644 --- a/.github/workflows/flutter.yml +++ b/.github/workflows/flutter.yml @@ -92,6 +92,12 @@ jobs: - ubuntu-latest - ubuntu-24.04-arm - macos-latest + include: + # we need to use the main channel, + # as flutter does apparently not officially release SDKs for arm64 linux. + # see: https://storage.googleapis.com/flutter_infra_release/releases/releases_linux.json + - runner: ubuntu-24.04-arm + channel: main fail-fast: false steps: - uses: actions/checkout@v4 @@ -99,7 +105,7 @@ jobs: - uses: subosito/flutter-action@v2 with: cache: true - channel: stable + channel: ${{ matrix.channel || 'stable' }} flutter-version: ${{ env.FLUTTER_VERSION }} - name: Install dependencies