Skip to content
Draft
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
32 changes: 30 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,40 @@ jobs:
cp rustpush/certs/legacy-fairplay/fairplay.crt rustpush/certs/fairplay/$name.crt
done

# pub get restores the two vendored CargoKit copies, so patch them after
# dependency resolution and before the Android build.
- name: Patch vendored CargoKit for the Flutter Kotlin plugin
run: |
set -euo pipefail
flutter pub get
bash ./tooling/android/patch_pub_cache_cargokit.sh

# First run is expected to fail until ffmpeg_kit_flutter_new is fixed.
- name: Run Build Script
run: |
flutter build apk --flavor alpha --debug --target-platform android-arm64


- name: Assert required native libraries are in the APK
run: |
set -euo pipefail
apk=build/app/outputs/flutter-apk/app-alpha-debug.apk
# A green Gradle build can still produce an APK that installs and
# then fails during plugin registration if CargoKit skipped a native
# library. Keep this list tied to the app's generated registrant and
# Rust bridge, not to unrelated libraries found in the pub cache.
for entry in \
lib/arm64-v8a/libflutter.so \
lib/arm64-v8a/librust_lib_bluebubbles.so \
lib/arm64-v8a/libirondash_engine_context_native.so \
lib/arm64-v8a/libsuper_native_extensions.so; do
if ! unzip -l "$apk" | grep -q "$entry"; then
echo "APK is missing $entry" >&2
unzip -l "$apk" | grep 'lib/' >&2 || true
exit 1
fi
done

- uses: actions/upload-artifact@v4
with:
name: Alpha Debug APK
path: build/app/outputs/flutter-apk/app-alpha-debug.apk
path: build/app/outputs/flutter-apk/app-alpha-debug.apk
68 changes: 50 additions & 18 deletions rust_builder/cargokit/gradle/plugin.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -82,47 +82,79 @@ class CargoKitPlugin implements Plugin<Project> {

static String file;

private Plugin findFlutterPlugin(Project rootProject) {
_findFlutterPlugin(rootProject.childProjects)
// Returns the Project that Flutter's Gradle plugin is applied to, not the
// plugin instance.
//
// CargoKit used to keep the plugin and reach through `plugin.project`. That
// worked against the Groovy plugin, where the field was reachable. Flutter
// 3.44.8's Kotlin rewrite declares `private var project: Project? = null`
// and dropped `getTargetPlatforms()` entirely, moving it to
// FlutterPluginUtils. Holding the plugin therefore buys nothing and every
// call through it throws once per variant.
private Project findFlutterProject(Project rootProject) {
_findFlutterProject(rootProject.childProjects)
}

private Plugin _findFlutterPlugin(Map projects) {
private Project _findFlutterProject(Map projects) {
for (project in projects) {
for (plugin in project.value.getPlugins()) {
if (plugin.class.name == "FlutterPlugin") {
return plugin;
// Flutter's Groovy Gradle plugin lived in the default package.
// The Kotlin rewrite moved it to com.flutter.gradle, so match
// the class name itself rather than the fully qualified name.
// Getting this wrong silently skips the whole Rust build and
// ships a package with no native library in it.
def pluginClassName = plugin.class.name
if (pluginClassName == "FlutterPlugin" ||
pluginClassName.endsWith(".FlutterPlugin")) {
return project.value;
}
}
def plugin = _findFlutterPlugin(project.value.childProjects);
if (plugin != null) {
return plugin;
def found = _findFlutterProject(project.value.childProjects);
if (found != null) {
return found;
}
}
return null;
}

// Mirrors FlutterPluginUtils.getTargetPlatforms, which is Kotlin-internal
// and not callable from Groovy. Flutter passes -Ptarget-platform on the
// command line; when it is absent this is the same default the Flutter
// plugin itself applies.
private static List<String> flutterTargetPlatforms(Project flutterProject) {
if (flutterProject.hasProperty("target-platform")) {
return flutterProject
.property("target-platform")
.toString()
.split(",")
.collect { it.trim() }
.findAll { !it.isEmpty() }
}
return ["android-arm", "android-arm64", "android-x64"]
}

@Override
void apply(Project project) {
def plugin = findFlutterPlugin(project.rootProject);
def flutterProject = findFlutterProject(project.rootProject);

project.extensions.create("cargokit", CargoKitExtension)

if (plugin == null) {
print("Flutter plugin not found, CargoKit plugin will not be applied.")
if (flutterProject == null) {
println("Flutter plugin not found, CargoKit plugin will not be applied.")
return;
}

def cargoBuildDir = "${project.buildDir}/build"

plugin.project.android.applicationVariants.all { variant ->
flutterProject.android.applicationVariants.all { variant ->

final buildType = variant.buildType.name

def cargoOutputDir = "${project.buildDir}/jniLibs/${buildType}";
def jniLibs = project.android.sourceSets.maybeCreate(buildType).jniLibs;
jniLibs.srcDir(new File(cargoOutputDir))

def platforms = plugin.getTargetPlatforms().collect()
def platforms = flutterTargetPlatforms(flutterProject).collect()

// Same thing addFlutterDependencies does in flutter.gradle
if (buildType == "debug") {
Expand All @@ -139,18 +171,18 @@ class CargoKitPlugin implements Plugin<Project> {
return
}

if (plugin.project.android.ndkVersion == null) {
if (flutterProject.android.ndkVersion == null) {
throw new GradleException("Please set 'android.ndkVersion' in 'app/build.gradle'.")
}

def task = project.tasks.create(taskName, CargoKitBuildTask.class) {
buildMode = variant.buildType.name
buildDir = cargoBuildDir
outputDir = cargoOutputDir
ndkVersion = plugin.project.android.ndkVersion
sdkDirectory = plugin.project.android.sdkDirectory
minSdkVersion = plugin.project.android.defaultConfig.minSdkVersion.apiLevel as int
compileSdkVersion = plugin.project.android.compileSdkVersion.substring(8) as int
ndkVersion = flutterProject.android.ndkVersion
sdkDirectory = flutterProject.android.sdkDirectory
minSdkVersion = flutterProject.android.defaultConfig.minSdkVersion.apiLevel as int
compileSdkVersion = flutterProject.android.compileSdkVersion.substring(8) as int
targetPlatforms = platforms
pluginFile = CargoKitPlugin.file
}
Expand Down
102 changes: 102 additions & 0 deletions tooling/android/patch_pub_cache_cargokit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env bash
#
# Teach the CargoKit copies vendored inside pub packages how to work with
# Flutter 3.44.8's Gradle plugin.
#
# irondash_engine_context and super_native_extensions ship their own CargoKit.
# Those copies live in the pub cache, are refetched by `flutter pub get`, and
# cannot be fixed by a committed edit, so they are patched before Android
# builds.
#
# CargoKit first needs to recognise Flutter's Kotlin Gradle plugin. That alone
# is not sufficient: the Kotlin rewrite made the plugin project private and
# moved getTargetPlatforms() to FlutterPluginUtils. This patch resolves the
# Flutter Project directly and reads -Ptarget-platform instead.
#
# Idempotent. Safe to run repeatedly and on already-patched files.
set -euo pipefail

patched=0
skipped=0
scanned=0

patch_file() {
local file="$1"
scanned=$((scanned + 1))

if grep -q "_findFlutterProject" "$file"; then
skipped=$((skipped + 1))
return 0
fi

if ! grep -q "_findFlutterPlugin" "$file"; then
echo " ?? $file does not look like CargoKit's plugin.gradle; leaving it alone" >&2
return 0
fi

# Rename the search so it yields the Project rather than the Plugin. The
# recursive call is rewritten as its own block first so the remaining bare
# `return plugin;` is unambiguously the one inside the plugin loop.
perl -0777 -pi -e '
s/private Plugin findFlutterPlugin\(/private Project findFlutterProject\(/;
s/private Plugin _findFlutterPlugin\(/private Project _findFlutterProject\(/;
s/_findFlutterPlugin\(rootProject\.childProjects\)/_findFlutterProject(rootProject.childProjects)/;
s/def plugin = _findFlutterPlugin\(project\.value\.childProjects\);\s*\n(\s*)if \(plugin != null\) \{\s*\n\s*return plugin;\s*\n\s*\}/def found = _findFlutterProject(project.value.childProjects);\n$1if (found != null) {\n$1 return found;\n$1}/;
s/if \(plugin\.class\.name == "FlutterPlugin"\) \{\s*\n(\s*)return plugin;/if (plugin.class.name == "FlutterPlugin" ||\n$1 plugin.class.name.endsWith(".FlutterPlugin")) {\n$1 return project.value;/;
s/return plugin;/return project.value;/;
s/def plugin = findFlutterPlugin\(project\.rootProject\);/def flutterProject = findFlutterProject(project.rootProject);/;
s/if \(plugin == null\) \{/if (flutterProject == null) {/;
s/\bprint\("Flutter plugin not found/println("Flutter plugin not found/;
s/plugin\.getTargetPlatforms\(\)/flutterTargetPlatforms(flutterProject)/g;
s/\bplugin\.project\b/flutterProject/g;
' "$file"

# getTargetPlatforms moved into FlutterPluginUtils, which is Kotlin-internal
# and unreachable from Groovy. Flutter passes -Ptarget-platform; the
# fallback list matches the Flutter plugin's own default.
perl -0777 -pi -e '
s/(\n \@Override\n void apply\(Project project\) \{)/\n private static List<String> flutterTargetPlatforms(Project flutterProject) {\n if (flutterProject.hasProperty("target-platform")) {\n return flutterProject\n .property("target-platform")\n .toString()\n .split(",")\n .collect { it.trim() }\n .findAll { !it.isEmpty() }\n }\n return ["android-arm", "android-arm64", "android-x64"]\n }\n$1/;
' "$file"

if ! grep -q "flutterTargetPlatforms" "$file"; then
echo " !! $file: helper was not inserted; the file shape is unexpected" >&2
return 1
fi

echo " patched $file"
patched=$((patched + 1))
}

roots=()
if [ "$#" -gt 0 ]; then
roots=("$@")
else
# Default pub cache locations, Windows first since that is the dev host.
for candidate in \
"${PUB_CACHE:-}" \
"$HOME/AppData/Local/Pub/Cache" \
"$HOME/.pub-cache"
do
[ -n "$candidate" ] && [ -d "$candidate" ] && roots+=("$candidate")
done
fi

if [ "${#roots[@]}" -eq 0 ]; then
echo "no pub cache found; nothing to patch" >&2
exit 0
fi

for root in "${roots[@]}"; do
while IFS= read -r file; do
patch_file "$file"
done < <(find "$root" -path "*cargokit/gradle/plugin.gradle" -type f 2>/dev/null)
done

echo "cargokit pub-cache patch: ${patched} patched, ${skipped} already current, ${scanned} scanned"

if [ "$scanned" -eq 0 ]; then
echo "warning: no vendored CargoKit copies were found." >&2
echo "If irondash_engine_context or super_native_extensions are still" >&2
echo "dependencies, the Android build will produce an APK that hangs on" >&2
echo "startup. Check that pub get has run." >&2
fi