core: shared Rust sync engine (tethercore) + wire into iOS via UniFFI
Introduce core/ — a Rust crate owning the wire protocol (Message codec, SSE client, reconnect/backoff, send) behind a 4-method seam exported via UniFFI: Engine::new / start(handler) / send / stop. This is the single sync engine meant to back all clients; desktop links it directly, iOS binds an .xcframework, Android will bind an .aar. RTC mesh + the signal envelope are deliberately out of scope for v1 (SSE-only). Verified end-to-end against the live server: examples/roundtrip.rs has a subscriber engine receive a message published by a sender engine. iOS now links it: TetherStore drops TetherKit's TetherClient/SSEClient/ Message and drives tethercore.Engine instead (RoomIdentity still from TetherKit via selective import to avoid the Message name clash). The app builds and links the Rust staticlib for device. Artifacts (xcframework, Swift bindings) are generated by core/build-ios.sh and git-ignored; commit source + script, not the ~80MB static libs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
4
core/.gitignore
vendored
Normal file
4
core/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/target/
|
||||
/bindings/
|
||||
/build-xcframework/
|
||||
/*.xcframework
|
||||
1939
core/Cargo.lock
generated
Normal file
1939
core/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
core/Cargo.toml
Normal file
24
core/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "tethercore"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Shared tether sync engine: codec + SSE client + reconnect, FFI-exported via UniFFI."
|
||||
|
||||
[lib]
|
||||
# lib → examples/tests can link it natively
|
||||
# staticlib → iOS .a / desktop agent
|
||||
# cdylib → Android .so / bindgen library mode
|
||||
crate-type = ["lib", "staticlib", "cdylib"]
|
||||
name = "tethercore"
|
||||
|
||||
[[bin]]
|
||||
name = "uniffi-bindgen"
|
||||
path = "src/bin/uniffi-bindgen.rs"
|
||||
|
||||
[dependencies]
|
||||
uniffi = { version = "0.28", features = ["cli"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "stream", "json"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "time", "sync", "macros"] }
|
||||
futures-util = "0.3"
|
||||
38
core/build-ios.sh
Executable file
38
core/build-ios.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds the tethercore Rust engine for iOS and refreshes the artifacts the
|
||||
# Xcode app consumes: the Swift bindings + the TetherCore.xcframework.
|
||||
# These outputs are git-ignored (binaries) / regenerated — run this after any
|
||||
# change to core/src, then build the app in Xcode.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
APP_DIR="../ios/TetherApp"
|
||||
DEVICE=aarch64-apple-ios
|
||||
SIM=aarch64-apple-ios-sim
|
||||
|
||||
echo "▸ building static libs ($DEVICE, $SIM)…"
|
||||
rustup target add "$DEVICE" "$SIM" >/dev/null 2>&1 || true
|
||||
cargo build --release --target "$DEVICE" --lib
|
||||
cargo build --release --target "$SIM" --lib
|
||||
|
||||
echo "▸ generating Swift bindings…"
|
||||
cargo build --release --lib # host dylib for library-mode bindgen
|
||||
cargo run --release --bin uniffi-bindgen -- \
|
||||
generate --library "target/release/libtethercore.dylib" \
|
||||
--language swift --out-dir bindings/swift
|
||||
|
||||
echo "▸ assembling TetherCore.xcframework…"
|
||||
rm -rf build-xcframework && mkdir -p build-xcframework/headers
|
||||
cp bindings/swift/tethercoreFFI.h build-xcframework/headers/
|
||||
cp bindings/swift/tethercoreFFI.modulemap build-xcframework/headers/module.modulemap
|
||||
rm -rf "$APP_DIR/Vendor/TetherCore.xcframework"
|
||||
mkdir -p "$APP_DIR/Vendor"
|
||||
xcodebuild -create-xcframework \
|
||||
-library "target/$DEVICE/release/libtethercore.a" -headers build-xcframework/headers \
|
||||
-library "target/$SIM/release/libtethercore.a" -headers build-xcframework/headers \
|
||||
-output "$APP_DIR/Vendor/TetherCore.xcframework"
|
||||
|
||||
echo "▸ refreshing Swift wrapper in app…"
|
||||
cp bindings/swift/tethercore.swift "$APP_DIR/Sources/tethercore.swift"
|
||||
|
||||
echo "✅ done — open $APP_DIR and build."
|
||||
53
core/examples/roundtrip.rs
Normal file
53
core/examples/roundtrip.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
//! Proves the engine round-trips against a live tether server, no FFI involved.
|
||||
//! cargo run --example roundtrip -- http://192.168.11.233:8765
|
||||
//! Subscriber engine (from=rx) listens; sender engine (from=tx) publishes;
|
||||
//! the message must come back through on_message.
|
||||
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use tethercore::{Engine, Message, MessageHandler};
|
||||
|
||||
struct Collector(mpsc::Sender<Message>);
|
||||
impl MessageHandler for Collector {
|
||||
fn on_message(&self, msg: Message) {
|
||||
let _ = self.0.send(msg);
|
||||
}
|
||||
fn on_status(&self, connected: bool) {
|
||||
eprintln!("[rx] connected={connected}");
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let server = std::env::args()
|
||||
.nth(1)
|
||||
.unwrap_or_else(|| "http://192.168.11.233:8765".into());
|
||||
let room = "rust-spike".to_string();
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let sub = Engine::new(server.clone(), room.clone(), "rx".into(), "rust-rx".into());
|
||||
sub.start(Box::new(Collector(tx)));
|
||||
|
||||
// Let the SSE subscription establish before publishing.
|
||||
std::thread::sleep(Duration::from_millis(800));
|
||||
|
||||
let payload = "hello-from-rust-core";
|
||||
let pubr = Engine::new(server, room, "tx".into(), "rust-tx".into());
|
||||
pubr.send(payload.to_string());
|
||||
|
||||
match rx.recv_timeout(Duration::from_secs(5)) {
|
||||
Ok(m) if m.text == payload => {
|
||||
println!("✅ round-trip OK: kind={} from={} source={} ts={}\n text={:?}",
|
||||
m.kind, m.from, m.source, m.ts, m.text);
|
||||
}
|
||||
Ok(m) => {
|
||||
println!("⚠️ received unexpected message: {m:?}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Err(_) => {
|
||||
println!("❌ timed out — no message received (is the server up at the URL?)");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
sub.stop();
|
||||
}
|
||||
7
core/src/bin/uniffi-bindgen.rs
Normal file
7
core/src/bin/uniffi-bindgen.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
// Library-mode binding generator. Build the crate, then:
|
||||
// cargo run --bin uniffi-bindgen generate \
|
||||
// --library target/<triple>/release/libtethercore.dylib \
|
||||
// --language swift --out-dir bindings/swift
|
||||
fn main() {
|
||||
uniffi::uniffi_bindgen_main()
|
||||
}
|
||||
224
core/src/lib.rs
Normal file
224
core/src/lib.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
//! tethercore — the shared sync engine.
|
||||
//!
|
||||
//! Owns the wire protocol (codec + SSE client + reconnect/backoff + send) and
|
||||
//! nothing else: no clipboard, no UI. Those stay native on each platform and
|
||||
//! talk to this engine through a 4-method seam exported via UniFFI:
|
||||
//!
|
||||
//! Engine::new(server, room, from, source)
|
||||
//! engine.start(handler) // inbound clipboard → handler.on_message
|
||||
//! engine.send(text)
|
||||
//! engine.stop()
|
||||
//!
|
||||
//! Desktop links it directly; iOS binds the .xcframework, Android the .aar.
|
||||
//! RTC mesh (the `signal` envelope) is intentionally out of scope for v1.
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
uniffi::setup_scaffolding!();
|
||||
|
||||
/// 1:1 with the Go server `Message`, minus the RTC `signal` blob.
|
||||
/// `type` is renamed to `kind` (reserved word in the target languages).
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, uniffi::Record)]
|
||||
pub struct Message {
|
||||
#[serde(rename = "type", default)]
|
||||
pub kind: String,
|
||||
#[serde(default)]
|
||||
pub text: String,
|
||||
#[serde(default)]
|
||||
pub from: String,
|
||||
#[serde(default)]
|
||||
pub to: String,
|
||||
#[serde(default)]
|
||||
pub role: String,
|
||||
#[serde(default)]
|
||||
pub source: String,
|
||||
#[serde(default)]
|
||||
pub room: String,
|
||||
#[serde(default)]
|
||||
pub ts: i64,
|
||||
}
|
||||
|
||||
/// Implemented on the foreign side (Swift/Kotlin) or natively (desktop agent).
|
||||
/// All callbacks fire on the engine's runtime thread — marshal to the UI thread
|
||||
/// on the consumer side.
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait MessageHandler: Send + Sync {
|
||||
/// A clipboard message from another device in the room (self-echo filtered).
|
||||
fn on_message(&self, msg: Message);
|
||||
/// Connection state changed: true once subscribed, false on drop/error.
|
||||
fn on_status(&self, connected: bool);
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Config {
|
||||
server: String,
|
||||
room: String,
|
||||
from: String,
|
||||
source: String,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Engine {
|
||||
cfg: Config,
|
||||
http: reqwest::Client,
|
||||
rt: Arc<tokio::runtime::Runtime>,
|
||||
running: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl Engine {
|
||||
/// `server` is the bus base URL, e.g. "http://192.168.11.233:8765".
|
||||
/// `from` is this device's stable id (used for echo suppression);
|
||||
/// `source` is a human label like "macos" / "iphone".
|
||||
#[uniffi::constructor]
|
||||
pub fn new(server: String, room: String, from: String, source: String) -> Arc<Self> {
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(2)
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("tokio runtime");
|
||||
Arc::new(Self {
|
||||
cfg: Config { server, room, from, source },
|
||||
http: reqwest::Client::new(),
|
||||
rt: Arc::new(rt),
|
||||
running: Arc::new(AtomicBool::new(false)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Begin streaming. Idempotent: a second call while running is a no-op.
|
||||
pub fn start(&self, handler: Box<dyn MessageHandler>) {
|
||||
if self.running.swap(true, Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
let handler: Arc<dyn MessageHandler> = Arc::from(handler);
|
||||
let http = self.http.clone();
|
||||
let cfg = self.cfg.clone();
|
||||
let running = self.running.clone();
|
||||
self.rt.spawn(async move {
|
||||
run_loop(http, cfg, running, handler).await;
|
||||
});
|
||||
}
|
||||
|
||||
/// Publish clipboard text to the room. Fire-and-forget.
|
||||
pub fn send(&self, text: String) {
|
||||
let http = self.http.clone();
|
||||
let cfg = self.cfg.clone();
|
||||
self.rt.spawn(async move {
|
||||
if let Err(e) = post(&http, &cfg, &text).await {
|
||||
eprintln!("tethercore: send failed: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Stop streaming. The reconnect loop exits at its next checkpoint.
|
||||
pub fn stop(&self) {
|
||||
self.running.store(false, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_loop(
|
||||
http: reqwest::Client,
|
||||
cfg: Config,
|
||||
running: Arc<AtomicBool>,
|
||||
handler: Arc<dyn MessageHandler>,
|
||||
) {
|
||||
let min = Duration::from_millis(500);
|
||||
let max = Duration::from_secs(10);
|
||||
let mut backoff = min;
|
||||
while running.load(Ordering::SeqCst) {
|
||||
match connect_stream(&http, &cfg, &running, &handler).await {
|
||||
Ok(()) => backoff = min, // clean EOF — reconnect promptly
|
||||
Err(e) => {
|
||||
handler.on_status(false);
|
||||
eprintln!("tethercore: stream error: {e}");
|
||||
}
|
||||
}
|
||||
if running.load(Ordering::SeqCst) {
|
||||
tokio::time::sleep(backoff).await;
|
||||
backoff = (backoff * 2).min(max);
|
||||
}
|
||||
}
|
||||
handler.on_status(false);
|
||||
}
|
||||
|
||||
async fn connect_stream(
|
||||
http: &reqwest::Client,
|
||||
cfg: &Config,
|
||||
running: &AtomicBool,
|
||||
handler: &Arc<dyn MessageHandler>,
|
||||
) -> Result<(), String> {
|
||||
let url = format!(
|
||||
"{}/api/stream?room={}",
|
||||
cfg.server.trim_end_matches('/'),
|
||||
cfg.room
|
||||
);
|
||||
let resp = http
|
||||
.get(&url)
|
||||
.header("X-Tether-Client", &cfg.from)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("HTTP {}", resp.status()));
|
||||
}
|
||||
handler.on_status(true);
|
||||
|
||||
// Byte-buffer the SSE stream; lines split on '\n' (ASCII, so chunk-safe).
|
||||
let mut stream = resp.bytes_stream();
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
let mut data: Vec<u8> = Vec::new();
|
||||
while let Some(chunk) = stream.next().await {
|
||||
if !running.load(Ordering::SeqCst) {
|
||||
return Ok(());
|
||||
}
|
||||
buf.extend_from_slice(&chunk.map_err(|e| e.to_string())?);
|
||||
while let Some(nl) = buf.iter().position(|&b| b == b'\n') {
|
||||
let mut line: Vec<u8> = buf.drain(..=nl).collect();
|
||||
line.pop(); // '\n'
|
||||
if line.last() == Some(&b'\r') {
|
||||
line.pop();
|
||||
}
|
||||
if line.is_empty() {
|
||||
// Event boundary: flush accumulated data field.
|
||||
if !data.is_empty() {
|
||||
if let Ok(m) = serde_json::from_slice::<Message>(&data) {
|
||||
if m.from != cfg.from {
|
||||
handler.on_message(m);
|
||||
}
|
||||
}
|
||||
data.clear();
|
||||
}
|
||||
} else if let Some(rest) = line.strip_prefix(b"data:") {
|
||||
let rest = rest.strip_prefix(b" ").unwrap_or(rest);
|
||||
data.extend_from_slice(rest);
|
||||
}
|
||||
// ':' comments (keepalives) and event:/id: lines are ignored.
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn post(http: &reqwest::Client, cfg: &Config, text: &str) -> Result<(), String> {
|
||||
let url = format!("{}/api/send", cfg.server.trim_end_matches('/'));
|
||||
let body = serde_json::json!({
|
||||
"type": "clipboard",
|
||||
"text": text,
|
||||
"from": cfg.from,
|
||||
"source": cfg.source,
|
||||
"room": cfg.room,
|
||||
});
|
||||
http.post(&url)
|
||||
.header("X-Tether-Source", &cfg.source)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.error_for_status()
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
9
ios/.gitignore
vendored
9
ios/.gitignore
vendored
@@ -9,3 +9,12 @@ DerivedData/
|
||||
# Generated Xcode projects (XcodeGen owns project.yml as source of truth)
|
||||
# Tracked intentionally so cloning without xcodegen still opens — keep pbxproj.
|
||||
.DS_Store
|
||||
|
||||
# Generated by core/build-ios.sh — regenerate, don't commit
|
||||
TetherApp/Vendor/
|
||||
TetherApp/Sources/tethercore.swift
|
||||
|
||||
# editor state
|
||||
*.xcuserstate
|
||||
xcuserdata/
|
||||
.swiftpm/
|
||||
|
||||
@@ -1,21 +1,33 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import TetherKit
|
||||
import enum TetherKit.RoomIdentity // selective: avoids TetherKit.Message clashing
|
||||
// with the engine's Message (compiled into this target)
|
||||
// Engine + Message + MessageHandler come from the generated tethercore.swift,
|
||||
// built directly into this module — no `import` needed.
|
||||
|
||||
/// Observable bridge between `TetherClient` (transport) and SwiftUI.
|
||||
/// Owns the receive task and the local clipboard side-effects.
|
||||
/// UI-facing clipboard item. Decouples the view from the FFI `Message` record
|
||||
/// and supplies the `id`/`date` SwiftUI's List needs.
|
||||
struct FeedItem: Identifiable, Equatable {
|
||||
let id = UUID()
|
||||
let text: String
|
||||
let source: String
|
||||
let date: Date
|
||||
}
|
||||
|
||||
/// Observable bridge between the shared `tethercore.Engine` and SwiftUI.
|
||||
/// The engine owns the wire protocol; this owns the clipboard side-effects.
|
||||
@MainActor @Observable
|
||||
final class TetherStore {
|
||||
var serverURL: String = UserDefaults.standard.string(forKey: "serverURL") ?? "http://tether.lan:8765"
|
||||
var room: String = RoomIdentity.derive()
|
||||
var feed: [Message] = []
|
||||
var feed: [FeedItem] = []
|
||||
var connected = false
|
||||
var lastError: String?
|
||||
|
||||
private let deviceID = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
|
||||
private var client: TetherClient?
|
||||
private var receiveTask: Task<Void, Never>?
|
||||
private var engine: Engine?
|
||||
private var bridge: EngineBridge?
|
||||
|
||||
func connectIfSaved() {
|
||||
guard UserDefaults.standard.string(forKey: "serverURL") != nil else { return }
|
||||
@@ -23,31 +35,26 @@ final class TetherStore {
|
||||
}
|
||||
|
||||
func connect() {
|
||||
guard let base = URL(string: serverURL.trimmingCharacters(in: .whitespaces)),
|
||||
!room.isEmpty else {
|
||||
let url = serverURL.trimmingCharacters(in: .whitespaces)
|
||||
guard !url.isEmpty, !room.isEmpty else {
|
||||
lastError = "Set a server URL first."
|
||||
return
|
||||
}
|
||||
UserDefaults.standard.set(serverURL, forKey: "serverURL")
|
||||
disconnect()
|
||||
|
||||
let cfg = TetherClient.Config(baseURL: base, room: room, from: deviceID)
|
||||
let c = TetherClient(config: cfg)
|
||||
client = c
|
||||
connected = true
|
||||
let b = EngineBridge(store: self)
|
||||
let e = Engine(server: url, room: room, from: deviceID, source: "ios")
|
||||
e.start(handler: b)
|
||||
engine = e
|
||||
bridge = b
|
||||
lastError = nil
|
||||
receiveTask = Task {
|
||||
for await msg in await c.clipboardStream() {
|
||||
feed.insert(msg, at: 0)
|
||||
if feed.count > 100 { feed.removeLast() }
|
||||
}
|
||||
connected = false
|
||||
}
|
||||
}
|
||||
|
||||
func disconnect() {
|
||||
receiveTask?.cancel()
|
||||
receiveTask = nil
|
||||
engine?.stop()
|
||||
engine = nil
|
||||
bridge = nil
|
||||
connected = false
|
||||
}
|
||||
|
||||
@@ -62,15 +69,41 @@ final class TetherStore {
|
||||
}
|
||||
|
||||
func send(_ text: String) {
|
||||
guard let c = client else { return }
|
||||
Task {
|
||||
do { try await c.send(text) }
|
||||
catch { lastError = "Send failed: \(error.localizedDescription)" }
|
||||
}
|
||||
engine?.send(text: text)
|
||||
}
|
||||
|
||||
/// Copy a received message back onto the local pasteboard.
|
||||
func copyToPasteboard(_ msg: Message) {
|
||||
UIPasteboard.general.string = msg.text
|
||||
func copyToPasteboard(_ item: FeedItem) {
|
||||
UIPasteboard.general.string = item.text
|
||||
}
|
||||
|
||||
// Called from the engine bridge, already hopped onto the main actor.
|
||||
fileprivate func ingest(_ item: FeedItem) {
|
||||
feed.insert(item, at: 0)
|
||||
if feed.count > 100 { feed.removeLast() }
|
||||
}
|
||||
|
||||
fileprivate func setConnected(_ value: Bool) {
|
||||
connected = value
|
||||
}
|
||||
}
|
||||
|
||||
/// Adapts the engine's callback interface (fired on the Rust runtime thread)
|
||||
/// onto the main actor for SwiftUI.
|
||||
private final class EngineBridge: MessageHandler {
|
||||
weak var store: TetherStore?
|
||||
init(store: TetherStore) { self.store = store }
|
||||
|
||||
func onMessage(msg: Message) {
|
||||
let item = FeedItem(
|
||||
text: msg.text,
|
||||
source: msg.source.isEmpty ? "unknown" : msg.source,
|
||||
date: Date(timeIntervalSince1970: Double(msg.ts) / 1000.0)
|
||||
)
|
||||
Task { @MainActor in self.store?.ingest(item) }
|
||||
}
|
||||
|
||||
func onStatus(connected: Bool) {
|
||||
Task { @MainActor in self.store?.setConnected(connected) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,15 @@
|
||||
A0B34D3F9849F9BC614CC058 /* TetherStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D1001BBAABF813AB29AD45 /* TetherStore.swift */; };
|
||||
B00ECEE60ABBEFEDFF548750 /* TetherKit in Frameworks */ = {isa = PBXBuildFile; productRef = 214441C31F40D721B6B47F10 /* TetherKit */; };
|
||||
CCA52ACB37EF809880064207 /* RoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F373509C266AF62E742C792A /* RoomView.swift */; };
|
||||
EEEAB6E2132E74568361D04E /* TetherCore.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 20AC83BF63EB7D2A826BFF91 /* TetherCore.xcframework */; };
|
||||
FF510853346E00FA9D3B7552 /* tethercore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D5C7A947C4BBA8AF0381677 /* tethercore.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
20AC83BF63EB7D2A826BFF91 /* TetherCore.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = TetherCore.xcframework; path = Vendor/TetherCore.xcframework; sourceTree = "<group>"; };
|
||||
331B3970CED74956D9BE87FF /* TetherApp.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = TetherApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
41B6BEFA7CFACAAA3507391B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
6D5C7A947C4BBA8AF0381677 /* tethercore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tethercore.swift; sourceTree = "<group>"; };
|
||||
B59E41534347FCDEDED407D6 /* TetherApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TetherApp.swift; sourceTree = "<group>"; };
|
||||
C4E3CFAF660DDE2C62CD1996 /* TetherKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = TetherKit; path = ../TetherKit; sourceTree = SOURCE_ROOT; };
|
||||
D7D1001BBAABF813AB29AD45 /* TetherStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TetherStore.swift; sourceTree = "<group>"; };
|
||||
@@ -28,6 +32,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B00ECEE60ABBEFEDFF548750 /* TetherKit in Frameworks */,
|
||||
EEEAB6E2132E74568361D04E /* TetherCore.xcframework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -40,16 +45,26 @@
|
||||
41B6BEFA7CFACAAA3507391B /* Info.plist */,
|
||||
F373509C266AF62E742C792A /* RoomView.swift */,
|
||||
B59E41534347FCDEDED407D6 /* TetherApp.swift */,
|
||||
6D5C7A947C4BBA8AF0381677 /* tethercore.swift */,
|
||||
D7D1001BBAABF813AB29AD45 /* TetherStore.swift */,
|
||||
);
|
||||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
47410593516FEA684761565A /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
20AC83BF63EB7D2A826BFF91 /* TetherCore.xcframework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
485FAD0A0B0809694D9B7B77 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5EF696014E5E1F874C5B729B /* Packages */,
|
||||
0565A8CEDDE5C5C0BE113DA3 /* Sources */,
|
||||
47410593516FEA684761565A /* Frameworks */,
|
||||
717BFFF9FC0CFF5C5EFFF376 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
@@ -137,6 +152,7 @@
|
||||
CCA52ACB37EF809880064207 /* RoomView.swift in Sources */,
|
||||
74D3E9952DD8E0913BE6B299 /* TetherApp.swift in Sources */,
|
||||
A0B34D3F9849F9BC614CC058 /* TetherStore.swift in Sources */,
|
||||
FF510853346E00FA9D3B7552 /* tethercore.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -151,6 +167,10 @@
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"Vendor\"",
|
||||
);
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Sources/Info.plist;
|
||||
INFOPLIST_KEY_NSAppTransportSecurity = "";
|
||||
@@ -161,6 +181,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.1.0;
|
||||
OTHER_LDFLAGS = "-framework Security -framework SystemConfiguration -lresolv";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.pecord.tether;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
@@ -238,6 +259,10 @@
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"Vendor\"",
|
||||
);
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Sources/Info.plist;
|
||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "tether connects to your clipboard relay on the local network.";
|
||||
@@ -247,6 +272,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.1.0;
|
||||
OTHER_LDFLAGS = "-framework Security -framework SystemConfiguration -lresolv";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.pecord.tether;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
||||
@@ -16,10 +16,14 @@ targets:
|
||||
sources:
|
||||
- Sources
|
||||
dependencies:
|
||||
- package: TetherKit
|
||||
- package: TetherKit # RoomIdentity only — networking now lives in TetherCore
|
||||
- framework: Vendor/TetherCore.xcframework
|
||||
embed: false # static lib, nothing to embed
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: io.pecord.tether
|
||||
# Rust staticlib pulls in SecRandomCopyBytes (ring) + system config (reqwest).
|
||||
OTHER_LDFLAGS: "-framework Security -framework SystemConfiguration -lresolv"
|
||||
MARKETING_VERSION: "0.1.0"
|
||||
CURRENT_PROJECT_VERSION: "1"
|
||||
GENERATE_INFOPLIST_FILE: true
|
||||
|
||||
Reference in New Issue
Block a user