Add Android and Linux platform support (local gRPC working, tsnet WIP)
- Android: Dart FFI → Go c-shared (.so) in jniLibs (arm64-v8a + x86_64) - Linux: Dart FFI → Go c-shared (.so) via Docker cross-compilation (amd64) - Dart API: TsnetFlutter uses MethodChannel on iOS/macOS, FFI on Android/Linux - Add ffi package dependency for native function bindings - Build script: ./build_go.sh [ios|macos|android|linux|apple|all] - Android RegisterInterfaceGetter to bypass netlink CAP_NET_ADMIN restriction - Make TailscaleStart() idempotent and add GODEBUG=netdns=go for Android Known: Android tsnet tunnel blocked by Go stdlib net.Interfaces() netlink call — local gRPC works, Tailscale fallback needs libtailscale integration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ae105a2bb0
commit
f070bcdb86
36
android/build.gradle
Normal file
36
android/build.gradle
Normal file
@ -0,0 +1,36 @@
|
||||
group 'io.svrnty.tsnet_flutter'
|
||||
version '1.0'
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.1.0'
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
namespace 'io.svrnty.tsnet_flutter'
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk 21
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
jniLibs.srcDirs = ['src/main/jniLibs']
|
||||
}
|
||||
}
|
||||
}
|
||||
4
android/src/main/AndroidManifest.xml
Normal file
4
android/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="io.svrnty.tsnet_flutter">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
</manifest>
|
||||
BIN
android/src/main/jniLibs/arm64-v8a/libtailscale.so
Normal file
BIN
android/src/main/jniLibs/arm64-v8a/libtailscale.so
Normal file
Binary file not shown.
BIN
android/src/main/jniLibs/x86_64/libtailscale.so
Normal file
BIN
android/src/main/jniLibs/x86_64/libtailscale.so
Normal file
Binary file not shown.
81
build_go.sh
81
build_go.sh
@ -187,12 +187,85 @@ build_macos() {
|
||||
du -sh "$OUTPUT"
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Android (c-shared → .so in jniLibs)
|
||||
# ============================================================
|
||||
build_android() {
|
||||
local NDK_HOME="${ANDROID_NDK_HOME:-$HOME/Library/Android/sdk/ndk/$(ls $HOME/Library/Android/sdk/ndk/ 2>/dev/null | sort -V | tail -1)}"
|
||||
|
||||
if [ ! -d "$NDK_HOME" ]; then
|
||||
echo "Error: Android NDK not found. Set ANDROID_NDK_HOME."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local TOOLCHAIN="$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64"
|
||||
local OUTPUT_DIR="$SCRIPT_DIR/android/src/main/jniLibs"
|
||||
mkdir -p "$OUTPUT_DIR/arm64-v8a" "$OUTPUT_DIR/x86_64"
|
||||
|
||||
# Android needs the "android" build tag for proper platform detection
|
||||
# (e.g., netmon_polling.go instead of netmon_linux.go which requires CAP_NET_ADMIN)
|
||||
local ANDROID_TAGS="android,$OMIT_TAGS"
|
||||
|
||||
# Android arm64
|
||||
echo "==> [Android] Building for arm64-v8a..."
|
||||
CGO_ENABLED=1 GOOS=android GOARCH=arm64 \
|
||||
CC="$TOOLCHAIN/bin/aarch64-linux-android21-clang" \
|
||||
go build -buildmode=c-shared -tags "$ANDROID_TAGS" -ldflags="-s -w" \
|
||||
-o "$OUTPUT_DIR/arm64-v8a/libtailscale.so" .
|
||||
|
||||
# Android x86_64 (emulator)
|
||||
echo "==> [Android] Building for x86_64..."
|
||||
CGO_ENABLED=1 GOOS=android GOARCH=amd64 \
|
||||
CC="$TOOLCHAIN/bin/x86_64-linux-android21-clang" \
|
||||
go build -buildmode=c-shared -tags "$ANDROID_TAGS" -ldflags="-s -w" \
|
||||
-o "$OUTPUT_DIR/x86_64/libtailscale.so" .
|
||||
|
||||
# Clean up generated headers (not needed for .so)
|
||||
rm -f "$OUTPUT_DIR"/*/libtailscale.h
|
||||
|
||||
echo "==> [Android] Done:"
|
||||
ls -lh "$OUTPUT_DIR/arm64-v8a/libtailscale.so"
|
||||
ls -lh "$OUTPUT_DIR/x86_64/libtailscale.so"
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Linux (c-shared → .so)
|
||||
# ============================================================
|
||||
build_linux() {
|
||||
local OUTPUT_DIR="$SCRIPT_DIR/linux"
|
||||
|
||||
# Linux amd64 — requires a Linux cross-compiler or building on Linux.
|
||||
# On macOS, use Docker: docker run --rm -v $PWD:/src -w /src golang:1.23 ./build_go.sh linux
|
||||
echo "==> [Linux] Building for amd64..."
|
||||
if [ "$(uname)" = "Linux" ]; then
|
||||
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \
|
||||
go build -buildmode=c-shared -tags "$OMIT_TAGS" -ldflags="-s -w" \
|
||||
-o "$OUTPUT_DIR/libtailscale.so" .
|
||||
else
|
||||
echo " Cross-compiling Linux from $(uname) via Docker..."
|
||||
docker run --rm --platform linux/amd64 -v "$SCRIPT_DIR:/src" -w /src/ios/Go \
|
||||
golang:latest bash -c "
|
||||
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \
|
||||
go build -buildmode=c-shared -tags '$OMIT_TAGS' -ldflags='-s -w' \
|
||||
-o /src/linux/libtailscale.so .
|
||||
"
|
||||
fi
|
||||
|
||||
rm -f "$OUTPUT_DIR/libtailscale.h"
|
||||
|
||||
echo "==> [Linux] Done:"
|
||||
ls -lh "$OUTPUT_DIR/libtailscale.so"
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Main
|
||||
# ============================================================
|
||||
case "$PLATFORM" in
|
||||
ios) build_ios ;;
|
||||
macos) build_macos ;;
|
||||
all) build_ios; build_macos ;;
|
||||
*) echo "Usage: $0 [ios|macos|all]"; exit 1 ;;
|
||||
ios) build_ios ;;
|
||||
macos) build_macos ;;
|
||||
android) build_android ;;
|
||||
linux) build_linux ;;
|
||||
all) build_ios; build_macos; build_android; build_linux ;;
|
||||
apple) build_ios; build_macos ;;
|
||||
*) echo "Usage: $0 [ios|macos|android|linux|apple|all]"; exit 1 ;;
|
||||
esac
|
||||
|
||||
39
ios/Go/android_interfaces.go
Normal file
39
ios/Go/android_interfaces.go
Normal file
@ -0,0 +1,39 @@
|
||||
//go:build android
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"tailscale.com/net/netmon"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Register an alternate interface getter for Android.
|
||||
// Go's net.Interfaces() uses netlink which requires CAP_NET_ADMIN,
|
||||
// and /sys/class/net is restricted on modern Android.
|
||||
// Provide a minimal loopback interface to satisfy tsnet's startup.
|
||||
// tsnet uses userspace netstack and doesn't need real interface data.
|
||||
netmon.RegisterInterfaceGetter(func() ([]netmon.Interface, error) {
|
||||
return []netmon.Interface{
|
||||
{
|
||||
Interface: &net.Interface{
|
||||
Index: 1,
|
||||
MTU: 65536,
|
||||
Name: "lo",
|
||||
HardwareAddr: nil,
|
||||
Flags: net.FlagUp | net.FlagLoopback | net.FlagRunning,
|
||||
},
|
||||
},
|
||||
{
|
||||
Interface: &net.Interface{
|
||||
Index: 2,
|
||||
MTU: 1500,
|
||||
Name: "wlan0",
|
||||
HardwareAddr: net.HardwareAddr{0x02, 0x00, 0x00, 0x00, 0x00, 0x01},
|
||||
Flags: net.FlagUp | net.FlagRunning | net.FlagMulticast,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
@ -20,6 +20,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
@ -50,6 +52,13 @@ func TailscaleStart(stateDir, authKey, hostname *C.char) *C.char {
|
||||
return nil // already started — idempotent
|
||||
}
|
||||
|
||||
// On Android, Go's net.Interfaces() uses netlink which requires
|
||||
// CAP_NET_ADMIN. Set GODEBUG to use Go's pure DNS resolver and
|
||||
// suppress network interface enumeration errors.
|
||||
if runtime.GOOS == "android" {
|
||||
os.Setenv("GODEBUG", "netdns=go")
|
||||
}
|
||||
|
||||
s := &tsnet.Server{
|
||||
Dir: C.GoString(stateDir),
|
||||
Hostname: C.GoString(hostname),
|
||||
|
||||
125
lib/tsnet_ffi.dart
Normal file
125
lib/tsnet_ffi.dart
Normal file
@ -0,0 +1,125 @@
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
// C function typedefs from bridge.go
|
||||
typedef _StartNative = Pointer<Utf8> Function(Pointer<Utf8>, Pointer<Utf8>, Pointer<Utf8>);
|
||||
typedef _StartProxyNative = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Int32>);
|
||||
typedef _StartProxyDart = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Int32>);
|
||||
typedef _SimpleNative = Pointer<Utf8> Function();
|
||||
typedef _OutParamNative = Pointer<Utf8> Function(Pointer<Pointer<Utf8>>);
|
||||
typedef _FreeNative = Void Function(Pointer<Utf8>);
|
||||
typedef _FreeDart = void Function(Pointer<Utf8>);
|
||||
|
||||
/// FFI-based implementation for Android and Linux.
|
||||
/// Loads the Go shared library and calls C functions directly.
|
||||
class TsnetFfi {
|
||||
late final DynamicLibrary _lib;
|
||||
late final _StartNative _start;
|
||||
late final _StartProxyDart _startProxy;
|
||||
late final _SimpleNative _stopProxy;
|
||||
late final _SimpleNative _stop;
|
||||
late final _OutParamNative _status;
|
||||
late final _OutParamNative _tailscaleIP;
|
||||
late final _FreeDart _free;
|
||||
|
||||
TsnetFfi() {
|
||||
_lib = _loadLibrary();
|
||||
_start = _lib.lookupFunction<_StartNative, _StartNative>('TailscaleStart');
|
||||
_startProxy = _lib.lookupFunction<_StartProxyNative, _StartProxyDart>('TailscaleStartProxy');
|
||||
_stopProxy = _lib.lookupFunction<_SimpleNative, _SimpleNative>('TailscaleStopProxy');
|
||||
_stop = _lib.lookupFunction<_SimpleNative, _SimpleNative>('TailscaleStop');
|
||||
_status = _lib.lookupFunction<_OutParamNative, _OutParamNative>('TailscaleStatus');
|
||||
_tailscaleIP = _lib.lookupFunction<_OutParamNative, _OutParamNative>('TailscaleIP');
|
||||
_free = _lib.lookupFunction<_FreeNative, _FreeDart>('TailscaleFreeString');
|
||||
}
|
||||
|
||||
static DynamicLibrary _loadLibrary() {
|
||||
if (Platform.isAndroid) {
|
||||
return DynamicLibrary.open('libtailscale.so');
|
||||
} else if (Platform.isLinux) {
|
||||
// Look in the app's lib directory (where Flutter bundles native libs)
|
||||
return DynamicLibrary.open('libtailscale.so');
|
||||
}
|
||||
throw UnsupportedError('FFI not supported on ${Platform.operatingSystem}');
|
||||
}
|
||||
|
||||
String? _consumeString(Pointer<Utf8> ptr) {
|
||||
if (ptr == nullptr) return null;
|
||||
final str = ptr.toDartString();
|
||||
_free(ptr);
|
||||
return str;
|
||||
}
|
||||
|
||||
void _checkError(Pointer<Utf8> errPtr, String code) {
|
||||
final err = _consumeString(errPtr);
|
||||
if (err != null) {
|
||||
throw PlatformException(code: code, message: err);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> start(String stateDir, String authKey, String hostname) async {
|
||||
final dirC = stateDir.toNativeUtf8();
|
||||
final keyC = authKey.toNativeUtf8();
|
||||
final hostC = hostname.toNativeUtf8();
|
||||
try {
|
||||
final err = _start(dirC, keyC, hostC);
|
||||
_checkError(err, 'START_FAILED');
|
||||
} finally {
|
||||
malloc.free(dirC);
|
||||
malloc.free(keyC);
|
||||
malloc.free(hostC);
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> startProxy(String remoteIP, int remotePort) async {
|
||||
final ipC = remoteIP.toNativeUtf8();
|
||||
final portOut = malloc<Int32>();
|
||||
try {
|
||||
final err = _startProxy(ipC, remotePort, portOut);
|
||||
_checkError(err, 'PROXY_FAILED');
|
||||
return portOut.value;
|
||||
} finally {
|
||||
malloc.free(ipC);
|
||||
malloc.free(portOut);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stopProxy() async {
|
||||
final err = _stopProxy();
|
||||
_checkError(err, 'STOP_PROXY_FAILED');
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
final err = _stop();
|
||||
_checkError(err, 'STOP_FAILED');
|
||||
}
|
||||
|
||||
Future<String?> status() async {
|
||||
final errOut = malloc<Pointer<Utf8>>();
|
||||
try {
|
||||
errOut.value = nullptr;
|
||||
final json = _status(errOut);
|
||||
final err = _consumeString(errOut.value);
|
||||
if (err != null) throw PlatformException(code: 'STATUS_FAILED', message: err);
|
||||
return _consumeString(json);
|
||||
} finally {
|
||||
malloc.free(errOut);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> tailscaleIP() async {
|
||||
final errOut = malloc<Pointer<Utf8>>();
|
||||
try {
|
||||
errOut.value = nullptr;
|
||||
final ip = _tailscaleIP(errOut);
|
||||
final err = _consumeString(errOut.value);
|
||||
if (err != null) throw PlatformException(code: 'IP_FAILED', message: err);
|
||||
return _consumeString(ip) ?? '';
|
||||
} finally {
|
||||
malloc.free(errOut);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,16 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:tsnet_flutter/tsnet_ffi.dart';
|
||||
|
||||
/// Embedded Tailscale tsnet client for Flutter.
|
||||
///
|
||||
/// Provides a userspace WireGuard tunnel via Tailscale's tsnet library,
|
||||
/// without requiring the user to install the Tailscale app or a VPN entitlement.
|
||||
///
|
||||
/// Architecture: Go static library (tsnet) ↔ Swift C bridge ↔ MethodChannel ↔ Dart
|
||||
/// - iOS/macOS: Swift MethodChannel → C bridge → Go static library
|
||||
/// - Android/Linux: Dart FFI → Go shared library (.so)
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
@ -19,52 +22,93 @@ import 'package:flutter/services.dart';
|
||||
class TsnetFlutter {
|
||||
static const _channel = MethodChannel('tsnet_flutter');
|
||||
|
||||
/// Join the Tailnet with the given auth key.
|
||||
///
|
||||
/// [authKey] is a Tailscale auth key (reusable, tag-scoped).
|
||||
/// [hostname] is the name this device will appear as in the Tailnet.
|
||||
///
|
||||
/// The auth key is typically received from the heater during BLE provisioning.
|
||||
/// State is persisted to the app's documents directory so the device
|
||||
/// identity survives app restarts.
|
||||
Future<void> start({
|
||||
required String authKey,
|
||||
String hostname = 'constellation-phone',
|
||||
}) async {
|
||||
await _channel.invokeMethod('start', {
|
||||
'authKey': authKey,
|
||||
'hostname': hostname,
|
||||
});
|
||||
/// FFI backend for Android/Linux — initialized lazily.
|
||||
TsnetFfi? _ffi;
|
||||
|
||||
bool get _useFfi => Platform.isAndroid || Platform.isLinux;
|
||||
|
||||
TsnetFfi get _ensureFfi => _ffi ??= TsnetFfi();
|
||||
|
||||
/// State directory for persisting Tailscale identity.
|
||||
String? _stateDir;
|
||||
|
||||
Future<String> _getStateDir() async {
|
||||
if (_stateDir != null) return _stateDir!;
|
||||
|
||||
// Use a directory relative to the current working directory or temp
|
||||
// For proper production use, pass a path from path_provider
|
||||
late final Directory dir;
|
||||
if (Platform.isAndroid) {
|
||||
// Android: use the app's internal storage via the temp directory parent
|
||||
final temp = Directory.systemTemp;
|
||||
dir = Directory('${temp.parent.path}/tailscale_state');
|
||||
} else if (Platform.isLinux) {
|
||||
final home = Platform.environment['HOME'] ?? '/tmp';
|
||||
dir = Directory('$home/.local/share/tailscale_state');
|
||||
} else {
|
||||
dir = Directory('${Directory.systemTemp.path}/tailscale_state');
|
||||
}
|
||||
await dir.create(recursive: true);
|
||||
_stateDir = dir.path;
|
||||
return _stateDir!;
|
||||
}
|
||||
|
||||
/// Create a localhost TCP proxy to a heater's Tailscale IP.
|
||||
///
|
||||
/// Returns the local port number. Connect gRPC to `127.0.0.1:<port>`.
|
||||
/// Traffic is forwarded through the Tailscale WireGuard tunnel to
|
||||
/// [heaterTailscaleIP]:[port] on the heater.
|
||||
///
|
||||
/// Uses userspace netstack — no VPN entitlement needed on iOS.
|
||||
Future<int> startProxy(String heaterTailscaleIP, {int port = 5050}) async {
|
||||
final localPort = await _channel.invokeMethod<int>('startProxy', {
|
||||
'ip': heaterTailscaleIP,
|
||||
'port': port,
|
||||
});
|
||||
return localPort!;
|
||||
/// Join the Tailnet with the given auth key.
|
||||
Future<void> start({
|
||||
required String authKey,
|
||||
String hostname = 'tsnet-flutter',
|
||||
}) async {
|
||||
if (_useFfi) {
|
||||
final dir = await _getStateDir();
|
||||
await _ensureFfi.start(dir, authKey, hostname);
|
||||
} else {
|
||||
await _channel.invokeMethod('start', {
|
||||
'authKey': authKey,
|
||||
'hostname': hostname,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a localhost TCP proxy to a remote Tailscale IP.
|
||||
/// Returns the local port number.
|
||||
Future<int> startProxy(String remoteIP, {int port = 5050}) async {
|
||||
if (_useFfi) {
|
||||
return await _ensureFfi.startProxy(remoteIP, port);
|
||||
} else {
|
||||
final localPort = await _channel.invokeMethod<int>('startProxy', {
|
||||
'ip': remoteIP,
|
||||
'port': port,
|
||||
});
|
||||
return localPort!;
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop the local TCP proxy.
|
||||
Future<void> stopProxy() async {
|
||||
await _channel.invokeMethod('stopProxy');
|
||||
if (_useFfi) {
|
||||
await _ensureFfi.stopProxy();
|
||||
} else {
|
||||
await _channel.invokeMethod('stopProxy');
|
||||
}
|
||||
}
|
||||
|
||||
/// Disconnect from the Tailnet and clean up.
|
||||
Future<void> stop() async {
|
||||
await _channel.invokeMethod('stop');
|
||||
if (_useFfi) {
|
||||
await _ensureFfi.stop();
|
||||
} else {
|
||||
await _channel.invokeMethod('stop');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current Tailscale connection status.
|
||||
Future<TailscaleStatus> status() async {
|
||||
final json = await _channel.invokeMethod<String>('status');
|
||||
String? json;
|
||||
if (_useFfi) {
|
||||
json = await _ensureFfi.status();
|
||||
} else {
|
||||
json = await _channel.invokeMethod<String>('status');
|
||||
}
|
||||
if (json == null) {
|
||||
return TailscaleStatus(state: TailscaleState.stopped);
|
||||
}
|
||||
@ -73,8 +117,12 @@ class TsnetFlutter {
|
||||
|
||||
/// Get this device's Tailscale IPv4 address (100.x.x.x).
|
||||
Future<String> tailscaleIP() async {
|
||||
final ip = await _channel.invokeMethod<String>('tailscaleIP');
|
||||
return ip!;
|
||||
if (_useFfi) {
|
||||
return await _ensureFfi.tailscaleIP();
|
||||
} else {
|
||||
final ip = await _channel.invokeMethod<String>('tailscaleIP');
|
||||
return ip!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
8
linux/CMakeLists.txt
Normal file
8
linux/CMakeLists.txt
Normal file
@ -0,0 +1,8 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(tsnet_flutter_library VERSION 0.2.0 LANGUAGES C)
|
||||
|
||||
# Bundle the pre-built Go shared library with the Flutter app.
|
||||
# The .so is built by build_go.sh and placed in this directory.
|
||||
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/libtailscale.so"
|
||||
DESTINATION "${CMAKE_INSTALL_PREFIX}/lib"
|
||||
COMPONENT Runtime)
|
||||
BIN
linux/libtailscale.so
Normal file
BIN
linux/libtailscale.so
Normal file
Binary file not shown.
@ -49,6 +49,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
||||
@ -12,6 +12,7 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
ffi: ^2.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@ -25,3 +26,8 @@ flutter:
|
||||
pluginClass: TsnetFlutterPlugin
|
||||
macos:
|
||||
pluginClass: TsnetFlutterPlugin
|
||||
android:
|
||||
package: io.svrnty.tsnet_flutter
|
||||
ffiPlugin: true
|
||||
linux:
|
||||
ffiPlugin: true
|
||||
|
||||
Loading…
Reference in New Issue
Block a user