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
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# Changelog

## Unreleased

Fixes found by running the README end to end as a new user would.

- The generated `tool/fetch_ios_sdk.sh` ran `flutter build` while trying to
explain it: the backticks in the advice line were command substitution
inside a double-quoted `echo`.
- A vendor binary name containing a dash or a dot — `Acme-SDK`, which the
validator deliberately accepts — was substituted into the generated scripts'
own shell variable names, producing `Acme-SDKS_DIR=...` and
`${VENDOR_Acme-SDK_URL}`. The iOS script died on its first statement and the
Android one built a garbage URL. The env-file keys are now literally
`VENDOR_AAR_URL` / `VENDOR_AAR_SHA256`, as the script's own help text says.
- The `swift package describe` command the script prints ended in `\\`, so
pasting it broke the line instead of continuing it.
- `--flavor native` no longer tells you to add a pub path dependency to a
package that has no `pubspec.yaml`, and its fetch script no longer points at
Flutter's `Runner.app`.
- The README's install step was missing entirely, and its example commands
(`dart run binary_sdk_bridge`) could not work from a fresh checkout.

## 0.1.0

First release.
Expand Down
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,36 @@ Wrap a **closed-source binary SDK** — Swift Package Manager on iOS, a Gradle
module on Android — with the vendor binary kept out of version control and
*optional at build time*.

## Install

Not on pub.dev yet, so install it from git:

```bash
dart pub global activate --source git \
https://github.com/saimskywalker/binary-sdk-bridge.git
```

That puts `binary-sdk-bridge` in `~/.pub-cache/bin`; add that directory to your
`PATH`, or call it as `dart pub global run binary_sdk_bridge`. From a clone,
`dart run bin/binary_sdk_bridge.dart` works with no install at all.

Requires the Dart SDK (3.5 or newer). Generating needs nothing else — the
vendor binary is fetched later, by the generated `tool/fetch_*.sh`, and is
never required to build.

## Usage

Two flavours, same core:

```bash
# Flutter plugin: native wrappers + Dart API + plugin classes
dart run binary_sdk_bridge \
binary-sdk-bridge \
--name acme_ads --org com.example \
--ios-framework AcmeSDK --android-aar AcmeSDK \
--out packages

# Native only: an SPM package and a Gradle module, no Flutter anywhere
dart run binary_sdk_bridge --flavor native \
binary-sdk-bridge --flavor native \
--name acme_sdk --org com.example \
--ios-framework AcmeSDK --android-aar AcmeSDK \
--out vendor
Expand Down Expand Up @@ -116,7 +135,7 @@ acme_ads/
└── tool/fetch_{ios,android}_sdk.sh
```

Exactly one file has a `TODO`: the bridge. That is the only place the vendor's
Exactly one file per platform has a `TODO`: the bridge. That is the only place the vendor's
own API appears, which is the point — everything around it is already decided.

## Notes for the generated Android module
Expand Down Expand Up @@ -174,6 +193,7 @@ right.
| `--ios-framework` | — | `.xcframework` base name; omit to skip iOS |
| `--android-aar` | — | `.aar` base name; omit to skip Android |
| `--out` | `.` | directory to create the package in |
| `--description` | *generic* | `description:` for the generated pubspec (Flutter flavour only) |
| `--ios-target` | `15.0` | iOS deployment target |
| `--min-sdk` | `24` | Android `minSdk` |
| `--compile-sdk` | `36` | Android `compileSdk` |
Expand Down
11 changes: 4 additions & 7 deletions bin/binary_sdk_bridge.dart
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,13 @@ void main(List<String> arguments) {
stdout
..writeln('Created ${root.path}')
..writeln()
..writeln('Next:')
..writeln(' 1. Add it to your app: '
'${spec.pluginName}: {path: ${root.path}}')
..writeln(' 2. Drop the vendor binary in with tool/fetch_*.sh')
..writeln(' 3. Fill in the TODO in the bridge — that is the only place '
'the vendor API appears');
..writeln('Next:');
generator.nextSteps(root.path).forEach(stdout.writeln);
}

String _usage(ArgParser parser) => '''
binary-sdk-bridge — wrap a closed-source binary SDK as a Flutter plugin.
binary-sdk-bridge — wrap a closed-source binary SDK as a Flutter plugin
or as native-only SPM + Gradle modules.

Usage:
binary-sdk-bridge --name <plugin> --org <com.example> [options]
Expand Down
30 changes: 30 additions & 0 deletions lib/src/generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,36 @@ class BridgeGenerator {
return files;
}

/// What to do with the package that was just written, as printed lines.
///
/// Flavour-specific because the native flavour emits no `pubspec.yaml`: the
/// pub path-dependency line is not something a native consumer can act on,
/// and following it puts an unresolvable dependency in their app.
List<String> nextSteps(String packagePath) {
final lines = <String>[];
var number = 1;
void step(String text) => lines.add(' ${number++}. $text');
void detail(String text) => lines.add(' $text');

if (spec.isFlutter) {
step('Add it to your app: ${spec.pluginName}: {path: $packagePath}');
} else {
step('Add it to your app — this flavour has no pubspec:');
if (spec.hasIos) {
detail('iOS — add $packagePath/ios/${spec.pluginName} as a local '
'Swift package');
}
if (spec.hasAndroid) {
detail('Android — include $packagePath/android as a Gradle module');
}
}
step('Drop the vendor binary in with tool/fetch_*.sh');
step('Fill in the TODO in the bridge — that is the only place the vendor '
'API appears');

return lines;
}

/// Writes [plan] under `[outputDir]/<pluginName>`.
///
/// Refuses to touch an existing directory unless [force] is set: silently
Expand Down
62 changes: 42 additions & 20 deletions lib/src/templates/script_templates.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@ import '../spec.dart';
/// It refuses an unpinned download rather than trusting on first use: the
/// artifact links into a shipping app, and a vendor URL whose contents can
/// change silently is not something to accept sight-unseen.
///
/// Placeholders are wrapped in `@...@` on purpose. A bare `FRAMEWORK` token
/// is a SUBSTRING of the shell variables `FRAMEWORKS_DIR` and `XCFRAMEWORK`,
/// so substituting it rewrote those identifiers too — and a vendor name with a
/// dash or a dot (`Acme-SDK`, which [BridgeSpec] accepts, because vendors ship
/// names like that) turned them into names bash cannot assign, killing the
/// script on its first line.
String fetchIosSh(BridgeSpec spec) => r'''
#!/usr/bin/env bash
#
# Fetch the vendor xcframework into ios/PLUGIN/Frameworks/.
# Fetch the vendor xcframework into ios/@PLUGIN@/Frameworks/.
#
# The binary is NOT committed. This script is the only supported way to put it
# in place, so every machine and CI runner ends up with a byte-identical,
Expand All @@ -21,26 +28,26 @@ String fetchIosSh(BridgeSpec spec) => r'''
set -euo pipefail

PKG_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
FRAMEWORKS_DIR="$PKG_DIR/ios/PLUGIN/Frameworks"
FRAMEWORKS_DIR="$PKG_DIR/ios/@PLUGIN@/Frameworks"
ENV_FILE="$PKG_DIR/tool/sdk_source.env"
DEST="$FRAMEWORKS_DIR/FRAMEWORK.xcframework"
DEST="$FRAMEWORKS_DIR/@FRAMEWORK@.xcframework"

mkdir -p "$FRAMEWORKS_DIR"

install_from_dir() {
rm -rf "$DEST"
cp -R "$1" "$DEST"
echo "==> installed $(basename "$1") as FRAMEWORK.xcframework"
echo "==> installed $(basename "$1") as @FRAMEWORK@.xcframework"
}

# SPM caches manifest evaluation by CONTENT, so a freshly-arrived binary is
# invisible to the Package.swift probe until the cache is dropped.
clear_spm_cache() {
CACHE_COMMENT
@CACHE_COMMENT@
local mobile_dir="$PKG_DIR/../.."
local cache
for cache in \
CACHE_PATHS
@CACHE_PATHS@
do
if [[ -e "$cache" ]]; then
rm -rf "$cache"
Expand All @@ -51,12 +58,12 @@ CACHE_PATHS
echo "In Xcode, also run File > Packages > Reset Package Caches if it is open."
echo
echo "VERIFY the binary actually linked -- a green build is NOT evidence."
VERIFY_NOTE
@VERIFY_NOTE@
echo
echo " swift package --package-path PKG_PATH \\\\"
echo " swift package --package-path @PKG_PATH@ \\"
echo " describe --type json | grep '\"type\" : \"binary\"'"
echo
echo " ls build/ios/iphonesimulator/Runner.app/Frameworks/"
@BUNDLE_CHECK@
}

# --- local directory form --------------------------------------------------
Expand Down Expand Up @@ -125,17 +132,21 @@ install_from_dir "$XCFRAMEWORK"
clear_spm_cache
'''
.replaceAll(
'CACHE_COMMENT', spec.isFlutter ? _flutterComment : _nativeComment)
'@CACHE_COMMENT@', spec.isFlutter ? _flutterComment : _nativeComment)
.replaceAll(
'PKG_PATH',
'@PKG_PATH@',
spec.isFlutter
? 'ios/Flutter/ephemeral/Packages/.packages/${spec.pluginName}'
: 'ios/${spec.pluginName}',
)
.replaceAll('CACHE_PATHS', spec.isFlutter ? _flutterCaches : _nativeCaches)
.replaceAll('VERIFY_NOTE', spec.isFlutter ? _flutterVerify : _nativeVerify)
.replaceAll('PLUGIN', spec.pluginName)
.replaceAll('FRAMEWORK', spec.iosFrameworkName ?? 'Vendor');
.replaceAll(
'@CACHE_PATHS@', spec.isFlutter ? _flutterCaches : _nativeCaches)
.replaceAll(
'@VERIFY_NOTE@', spec.isFlutter ? _flutterVerify : _nativeVerify)
.replaceAll('@BUNDLE_CHECK@',
spec.isFlutter ? _flutterBundleCheck : _nativeBundleCheck)
.replaceAll('@PLUGIN@', spec.pluginName)
.replaceAll('@FRAMEWORK@', spec.iosFrameworkName ?? 'Vendor');

/// Flutter resolves through its own ephemeral graph AND Xcode's cloned
/// SourcePackages, on top of SwiftPM's global manifest cache.
Expand All @@ -149,14 +160,25 @@ const _nativeCaches =
r''' "$HOME/Library/Caches/org.swift.swiftpm/manifests" \
"$PKG_DIR/.build"''';

/// The backticks are escaped because this is a DOUBLE-quoted `echo`: unescaped,
/// bash reads them as command substitution and the script literally runs
/// `flutter build` while printing this advice.
const _flutterVerify =
r''' echo "`flutter build` passes -quiet to xcodebuild, which suppresses the"
r''' echo "\`flutter build\` passes -quiet to xcodebuild, which suppresses the"
echo "#warning the bridge emits. Check one of these instead:"''';

const _nativeVerify =
r''' echo "A build that succeeds without the binary looks identical to one"
echo "that linked it. Check one of these instead:"''';

/// `Runner.app` is Flutter's app bundle, so the native flavour cannot point at
/// it — there is no Runner target in a plain iOS app.
const _flutterBundleCheck =
r''' echo " ls build/ios/iphonesimulator/Runner.app/Frameworks/"''';

const _nativeBundleCheck =
r''' echo " ls YourApp.app/Frameworks/ # inside the built app bundle"''';

String fetchAndroidSh(BridgeSpec spec) => r'''
#!/usr/bin/env bash
#
Expand All @@ -174,7 +196,7 @@ set -euo pipefail
PKG_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LIBS_DIR="$PKG_DIR/android/libs"
ENV_FILE="$PKG_DIR/tool/sdk_source.env"
DEST="$LIBS_DIR/AAR.aar"
DEST="$LIBS_DIR/@AAR@.aar"

mkdir -p "$LIBS_DIR"

Expand All @@ -185,7 +207,7 @@ if [[ $# -ge 1 ]]; then
exit 1
fi
cp "$1" "$DEST"
echo "==> installed $(basename "$1") as AAR.aar"
echo "==> installed $(basename "$1") as @AAR@.aar"
echo " sha256: $(shasum -a 256 "$DEST" | cut -d' ' -f1)"
echo
echo "Record that hash — for a hand-delivered .aar it is the only evidence"
Expand Down Expand Up @@ -234,9 +256,9 @@ if [[ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]]; then
fi

cp "$TMP_DIR/sdk.aar" "$DEST"
echo "==> checksum ok, installed AAR.aar"
echo "==> checksum ok, installed @AAR@.aar"
'''
.replaceAll('AAR', spec.androidAarName ?? 'Vendor');
.replaceAll('@AAR@', spec.androidAarName ?? 'Vendor');

String gitignore(BridgeSpec spec) {
final lines = <String>[
Expand Down
Loading
Loading