Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .github/workflows/build-all-platforms.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ jobs:
- name: Install FRB codegen 2.12.0
run: cargo install flutter_rust_bridge_codegen --version 2.12.0 --locked

- name: Install pinned cargo-expand 1.0.126
run: cargo install cargo-expand --version 1.0.126 --locked

- name: Resolve root dependencies
run: flutter pub get --no-example

Expand Down Expand Up @@ -424,8 +427,16 @@ jobs:
BEGIN { RS = "" }
{
record = tolower($0)
# Paragraph granularity couples unrelated lines: the Flutter
# tool nags about the intentionally old Gradle/AGP/Kotlin
# versions pinned by this leg, and the sdk manager mid-build
# CMake install logs an analytics.settings lock warning. Both
# land in the same paragraph as the cargokit env dump, but
# neither is a cargokit deprecation, so skip such records.
if (record ~ /(warning|warn|deprecated|deprecation)/ &&
record ~ /(cargokit|plugin\.gradle|build_tool)/) {
record ~ /(cargokit|plugin\.gradle|build_tool)/ &&
record !~ /flutter support for your project/ &&
record !~ /unable to lock settings file .*analytics\.settings/) {
print $0
found = 1
}
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/precompile-binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ name: Precompile Binaries

on:
workflow_call:
secrets:
PRECOMPILE_BINARIES_PRIVATE_KEY:
required: true
workflow_dispatch:

permissions:
Expand Down Expand Up @@ -85,7 +88,7 @@ jobs:
--repository=${{ github.repository }}
--source-commit=${{ github.sha }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ github.token }}
PRIVATE_KEY: ${{ secrets.PRECOMPILE_BINARIES_PRIVATE_KEY }}

- name: Verify the published v2 generation
Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/publish-pub.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@ jobs:
- name: Resolve dependencies
run: flutter pub get --no-example

- name: Resolve build tool dependencies
run: dart pub get
working-directory: cargokit/build_tool

- name: Fetch verified SwiftPM binary into the package
run: >-
dart run cargokit/build_tool/bin/build_tool.dart
fetch-precompiled-artifacts
--manifest-dir=libfjs
--output-dir=darwin/fjs/Binaries
--asset=fjs.xcframework.zip

- name: Require the SwiftPM binary in the package
shell: bash
run: test -f darwin/fjs/Binaries/fjs.xcframework.zip

- name: Verify tracked package source
run: git diff HEAD --exit-code --

Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## 3.3.1

* **FIX**: Published pub packages now ship `darwin/fjs/Binaries/fjs.xcframework.zip` again. The tag-triggered publish workflow materializes the signed SwiftPM composite from the verified precompiled generation before `flutter pub publish` and hard-fails when the binary is absent, so Swift Package Manager resolution no longer fails with a missing `FjsBinary` archive.
* **FIX**: Android Cargokit builds pin `CLANG_PATH` to the NDK clang and use the NDK LLVM triple directory (`arm-linux-androideabi`) for armv7 bindgen includes, fixing Windows Android builds where bindgen loaded a host LLVM installation and could not find `stdbool.h`.
* **FIX**: `cargokit/run_build_tool.cmd` now propagates the Dart build tool's non-zero exit code (including after the snapshot-rebuild retry path), so Windows Gradle builds fail instead of silently producing an APK without `libfjs.so`.
* **FIX**: Declared `PRECOMPILE_BINARIES_PRIVATE_KEY` on the reusable precompile workflow so the explicitly scoped callers resolve; CI had rejected every workflow startup since the secret passthrough was introduced.
* **INTERNAL**: Updated `h2` to 0.4.19 (RUSTSEC-2026-0258) and `rustls` to 0.23.45 (RUSTSEC-2026-0285), clearing the RustSec audit gate that had not run since the CI startup regression.
* **INTERNAL**: Upgraded `rquickjs` to 0.14.0 (master `3d5ecfc9`) and the `llrt` crates to main `89941ae1`; `ArrayBuffer`/`TypedArray` byte reads now record the copy-before-Java-runs safety invariant required by the new `as_bytes` contract.
* **FIX**: Patched `dlopen2` to a git fix (upstream PR [OpenByteDev/dlopen2#29](https://github.com/OpenByteDev/dlopen2/pull/29)) because 0.9.0 removed its `once_cell` dependency while `raw/unix.rs` still imports it on every non-linux/non-macos target, so Android builds failed with E0433; the patch swaps in `std::sync::LazyLock` and drops out once upstream releases a fix.
* **INTERNAL**: Pinned `cargo-expand` 1.0.126 for FRB regeneration and capped the root `flutter_rust_bridge` dependency to the codegen's minor line (`~2.12.0`). Release drift in cargo-expand re-prints expanded code differently, and an unconstrained caret let fresh resolutions pick newer FRB runtimes, tripping the generator's auto-upgrade into rewriting `pubspec.yaml` on CI; the codegen toolchain uses floating nightly as requested, and both pins are enforced by the regeneration check and installed by CI.

## 3.3.0

* **BREAKING**: Raised the supported toolchain floor to Flutter 3.24.0 and Dart 3.5.0, aligned Cargokit and precompiled generation with that minimum, and added a minimum-version public API compilation gate while keeping the example on the latest pinned Flutter release.
Expand Down
18 changes: 16 additions & 2 deletions cargokit/build_tool/lib/src/android_environment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,12 @@ class AndroidEnvironment {

final bindgenKey = "BINDGEN_EXTRA_CLANG_ARGS_${target.rust}";
final sysrootForClang = _clangPath(sysroot);
final includeForClang =
_clangPath(path.join(sysroot, 'usr', 'include', target.rust));
final includeForClang = _clangPath(path.join(
sysroot,
'usr',
'include',
_ndkIncludeTriple,
));
final bindgenValue =
"$targetArg --sysroot=$sysrootForClang -I$includeForClang";

Expand All @@ -177,6 +181,10 @@ class AndroidEnvironment {
rustFlagsKey: rustFlagsValue,
linkerKey: selfPath,
bindgenKey: bindgenValue,
// bindgen (clang-sys) resolves libclang from CLANG_PATH, so pin it to
// the NDK clang; a host LLVM installation has mismatched resource
// headers and cannot compile against the NDK sysroot.
'CLANG_PATH': ccValue,
// Recognized by main() so we know when we're acting as a wrapper
'_CARGOKIT_NDK_LINK_TARGET': targetArg,
'_CARGOKIT_NDK_LINK_CLANG': ccValue,
Expand All @@ -191,6 +199,12 @@ class AndroidEnvironment {
return value.replaceAll(r'\', '/');
}

/// The NDK sysroot names per-triple include directories by LLVM triple,
/// which drops the `v7` suffix for 32-bit ARM.
String get _ndkIncludeTriple => target.rust == 'armv7-linux-androideabi'
? 'arm-linux-androideabi'
: target.rust;

// Workaround for libgcc missing in NDK23, inspired by cargo-ndk
String _libGccWorkaround(String buildDir, Version ndkVersion) {
final workaroundDir = path.join(
Expand Down
39 changes: 39 additions & 0 deletions cargokit/build_tool/lib/src/build_tool.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'android_environment.dart';
import 'build_cmake.dart';
import 'build_gradle.dart';
import 'build_pod.dart';
import 'fetch_precompiled_artifacts.dart';
import 'logging.dart';
import 'options.dart';
import 'precompile_binaries.dart';
Expand Down Expand Up @@ -347,6 +348,43 @@ class PublishPrecompiledGenerationCommand extends Command {
}
}

class FetchPrecompiledArtifactsCommand extends Command {
FetchPrecompiledArtifactsCommand() {
argParser
..addOption(
'manifest-dir',
mandatory: true,
help: 'Directory containing Cargo.toml and cargokit.yaml',
)
..addOption(
'output-dir',
mandatory: true,
help: 'Directory that receives the fetched assets',
)
..addMultiOption(
'asset',
help: 'Composite output name to fetch. Repeat for each asset.',
);
}

@override
final name = 'fetch-precompiled-artifacts';

@override
final description =
'Fetches verified composite assets from the published generation.';

@override
Future<void> run() async {
final fetch = FetchPrecompiledArtifacts(
manifestDir: argResults!['manifest-dir'] as String,
outputDir: argResults!['output-dir'] as String,
assetNames: argResults!['asset'] as List<String>,
);
await fetch.run();
}
}

class VerifyBinariesCommand extends Command {
VerifyBinariesCommand() {
argParser.addOption(
Expand Down Expand Up @@ -391,6 +429,7 @@ Future<void> runMain(List<String> args) async {
..addCommand(BuildPrecompiledGenerationCommand())
..addCommand(FinalizeLocalGenerationCommand())
..addCommand(PublishPrecompiledGenerationCommand())
..addCommand(FetchPrecompiledArtifactsCommand())
..addCommand(VerifyBinariesCommand());

await runner.run(args);
Expand Down
93 changes: 93 additions & 0 deletions cargokit/build_tool/lib/src/fetch_precompiled_artifacts.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import 'dart:io';

import 'package:path/path.dart' as path;

import 'cargo.dart';
import 'crate_hash.dart';
import 'options.dart';
import 'precompiled_asset_store.dart';
import 'precompiled_generation.dart';
import 'verify_binaries.dart';

class FetchPrecompiledArtifacts {
FetchPrecompiledArtifacts({
required this.manifestDir,
required this.outputDir,
required this.assetNames,
});

final String manifestDir;
final String outputDir;
final List<String> assetNames;

Future<void> run() async {
if (assetNames.isEmpty) {
throw ArgumentError('At least one --asset is required.');
}
final config = CargokitCrateOptions.load(manifestDir: manifestDir);
final precompiled = config.precompiledBinaries;
if (precompiled == null) {
throw PrecompiledGenerationException(
'Crate does not support precompiled binaries.');
}
final recipe = precompiled.buildRecipe;
if (recipe == null) {
throw PrecompiledGenerationException(
'Configured precompiled binaries need a complete build recipe.');
}
final compositeOutputs = <String>{};
for (final group in precompiled.compositeGroups) {
compositeOutputs.addAll(group.outputs);
}
final requested = assetNames.toSet();
final unknown = requested.difference(compositeOutputs);
if (unknown.isNotEmpty) {
throw PrecompiledGenerationException(
'Requested assets are not composite outputs: ${unknown.toList()..sort()}');
}

final crateInfo = CrateInfo.load(manifestDir);
final generationHash = CrateHash.compute(manifestDir);
final cacheRoot = Directory.systemTemp.createTempSync('cargokit-fetch-');
final transport = PrecompiledAssetTransport();
try {
final store = PrecompiledAssetStore(
cacheRoot: path.join(cacheRoot.path, 'cache'),
uriPrefix: Uri.parse(precompiled.uriPrefix),
publicKey: precompiled.publicKey,
recipe: recipe,
expectedAssetNames: expectedGenerationAssetNames(
recipe: recipe,
precompiled: precompiled,
libraryName: crateInfo.packageName,
),
expectedCompositeChecksums: expectedCompositeChecksumPairs(precompiled),
transport: transport,
);
final snapshot = await store.snapshot(
generationHash: generationHash,
requestedAssetNames: requested,
);
if (snapshot == null) {
throw PrecompiledGenerationException(
'Completed v2 precompiled generation is unavailable.');
}
final destinationRoot = Directory(outputDir)..createSync(recursive: true);
for (final name in requested.toList()..sort()) {
final destination =
File(path.join(destinationRoot.path, path.basename(name)));
final staging = File('${destination.path}.staging.$pid');
staging.writeAsBytesSync(
File(snapshot.pathFor(name)).readAsBytesSync(),
flush: true,
);
staging.renameSync(destination.path);
stdout.writeln('Fetched $name');
}
stdout.writeln('Fetched generation $generationHash');
} finally {
transport.close();
if (cacheRoot.existsSync()) cacheRoot.deleteSync(recursive: true);
}
}
}
88 changes: 57 additions & 31 deletions cargokit/build_tool/lib/src/verify_binaries.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,56 @@ import 'precompiled_asset_store.dart';
import 'precompiled_generation.dart';
import 'target.dart';

Set<String> expectedGenerationAssetNames({
required PrecompiledBuildRecipe recipe,
required PrecompiledBinaries precompiled,
required String libraryName,
}) {
final result = <String>{};
for (final triple in recipe.rustTargets) {
final target = Target.forRustTriple(triple);
if (target == null) {
throw PrecompiledGenerationException(
'Build recipe contains an unsupported Rust target.');
}
for (final type in AritifactType.values) {
for (final artifact in getArtifactNames(
target: target,
libraryName: libraryName,
remote: true,
aritifactType: type,
)) {
result.add(PrecompileBinaries.fileName(target, artifact));
}
}
}
for (final group in precompiled.compositeGroups) {
for (final checksum
in group.outputs.where((output) => output.endsWith('.checksum'))) {
final archive = checksum.substring(0, checksum.length - '.checksum'.length);
if (group.outputs.contains(archive)) {
result.add(archive);
result.add(checksum);
}
}
}
Comment on lines +40 to +49
return result;
}

Set<String> expectedCompositeChecksumPairs(PrecompiledBinaries precompiled) {
final result = <String>{};
for (final group in precompiled.compositeGroups) {
for (final checksum
in group.outputs.where((output) => output.endsWith('.checksum'))) {
final archive = checksum.substring(0, checksum.length - '.checksum'.length);
if (group.outputs.contains(archive)) {
result.add('$archive\u0000$checksum');
}
}
}
return result;
}

class VerifyBinaries {
VerifyBinaries({
required this.manifestDir,
Expand All @@ -34,37 +84,13 @@ class VerifyBinaries {
'Configured precompiled binaries need a complete build recipe.');
}
final crateInfo = CrateInfo.load(manifestDir);
final expectedNames = <String>{};
for (final triple in recipe.rustTargets) {
final target = Target.forRustTriple(triple);
if (target == null) {
throw PrecompiledGenerationException(
'Build recipe contains an unsupported Rust target.');
}
for (final type in AritifactType.values) {
for (final artifact in getArtifactNames(
target: target,
libraryName: crateInfo.packageName,
remote: true,
aritifactType: type,
)) {
expectedNames.add(PrecompileBinaries.fileName(target, artifact));
}
}
}
final compositeChecksums = <String>{};
for (final group in precompiledBinaries.compositeGroups) {
for (final checksum
in group.outputs.where((output) => output.endsWith('.checksum'))) {
final archive =
checksum.substring(0, checksum.length - '.checksum'.length);
if (group.outputs.contains(archive)) {
expectedNames.add(archive);
expectedNames.add(checksum);
compositeChecksums.add('$archive\u0000$checksum');
}
}
}
final expectedNames = expectedGenerationAssetNames(
recipe: recipe,
precompiled: precompiledBinaries,
libraryName: crateInfo.packageName,
);
final compositeChecksums =
expectedCompositeChecksumPairs(precompiledBinaries);
final generationHash = CrateHash.compute(manifestDir);
final cacheRoot = Directory.systemTemp.createTempSync('cargokit-verify-');
final transport = PrecompiledAssetTransport();
Expand Down
Loading
Loading