Replace C Tor with Rust Arti for Tor integration

- Replace C Tor (0.4.8.21) with Rust Arti (1.9.0/arti-client 0.38)
- 70% smaller binary: 21MB xcframework vs 67MB (6.9MB vs 14MB per slice)
- Memory-safe Rust implementation with modern async (tokio)
- Same SOCKS5 proxy interface at 127.0.0.1:39050 for drop-in compatibility
- FFI wrapper (arti-bitchat crate) with C-compatible exports
- Swift TorManager maintains identical public API
- Aggressive size optimization: opt-level=z, lto=fat, panic=abort, strip
- Supports iOS device, iOS simulator (Apple Silicon), and macOS

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jack
2026-01-13 16:18:00 -10:00
co-authored by Claude Opus 4.5
parent 7b9ffe464a
commit 23d63ab4df
2260 changed files with 6907 additions and 354558 deletions
+3
View File
@@ -0,0 +1,3 @@
/target/
/.build/
/.swiftpm/
+5046
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
[workspace]
resolver = "2"
members = ["arti-bitchat"]
[profile.release]
opt-level = "z"
lto = "fat"
codegen-units = 1
panic = "abort"
strip = "symbols"
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AvailableLibraries</key>
<array>
<dict>
<key>BinaryPath</key>
<string>libarti_bitchat.a</string>
<key>HeadersPath</key>
<string>Headers</string>
<key>LibraryIdentifier</key>
<string>macos-arm64</string>
<key>LibraryPath</key>
<string>libarti_bitchat.a</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
</array>
<key>SupportedPlatform</key>
<string>macos</string>
</dict>
<dict>
<key>BinaryPath</key>
<string>libarti_bitchat.a</string>
<key>HeadersPath</key>
<string>Headers</string>
<key>LibraryIdentifier</key>
<string>ios-arm64</string>
<key>LibraryPath</key>
<string>libarti_bitchat.a</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
</dict>
<dict>
<key>BinaryPath</key>
<string>libarti_bitchat.a</string>
<key>HeadersPath</key>
<string>Headers</string>
<key>LibraryIdentifier</key>
<string>ios-arm64-simulator</string>
<key>LibraryPath</key>
<string>libarti_bitchat.a</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>simulator</string>
</dict>
</array>
<key>CFBundlePackageType</key>
<string>XFWK</string>
<key>XCFrameworkFormatVersion</key>
<string>1.0</string>
</dict>
</plist>
@@ -0,0 +1,78 @@
#ifndef ARTI_H
#define ARTI_H
#include <stdint.h>
#include <stdbool.h>
/**
* Start Arti with a SOCKS5 proxy.
*
* # Arguments
* * `data_dir` - Path to data directory for Tor state (C string)
* * `socks_port` - Port for SOCKS5 proxy (e.g., 39050)
*
* # Returns
* * 0 on success
* * -1 if already running
* * -2 if data_dir is invalid
* * -3 if runtime initialization failed
* * -4 if bootstrap failed
*/
int arti_start(const char *data_dir, uint16_t socks_port);
/**
* Stop Arti gracefully.
*
* # Returns
* * 0 on success
* * -1 if not running
*/
int arti_stop(void);
/**
* Check if Arti is currently running.
*
* # Returns
* * 1 if running
* * 0 if not running
*/
int arti_is_running(void);
/**
* Get the current bootstrap progress (0-100).
*/
int arti_bootstrap_progress(void);
/**
* Get the current bootstrap summary string.
*
* # Arguments
* * `buf` - Buffer to write the summary into
* * `len` - Length of the buffer
*
* # Returns
* * Number of bytes written (not including null terminator)
* * -1 if buffer is null or too small
*/
int arti_bootstrap_summary(char *buf, int len);
/**
* Signal Arti to go dormant (reduce resource usage).
* This is a hint; Arti may not fully support dormant mode yet.
*
* # Returns
* * 0 on success
* * -1 if not running
*/
int arti_go_dormant(void);
/**
* Signal Arti to wake from dormant mode.
*
* # Returns
* * 0 on success
* * -1 if not running
*/
int arti_wake(void);
#endif /* ARTI_H */
@@ -0,0 +1,78 @@
#ifndef ARTI_H
#define ARTI_H
#include <stdint.h>
#include <stdbool.h>
/**
* Start Arti with a SOCKS5 proxy.
*
* # Arguments
* * `data_dir` - Path to data directory for Tor state (C string)
* * `socks_port` - Port for SOCKS5 proxy (e.g., 39050)
*
* # Returns
* * 0 on success
* * -1 if already running
* * -2 if data_dir is invalid
* * -3 if runtime initialization failed
* * -4 if bootstrap failed
*/
int arti_start(const char *data_dir, uint16_t socks_port);
/**
* Stop Arti gracefully.
*
* # Returns
* * 0 on success
* * -1 if not running
*/
int arti_stop(void);
/**
* Check if Arti is currently running.
*
* # Returns
* * 1 if running
* * 0 if not running
*/
int arti_is_running(void);
/**
* Get the current bootstrap progress (0-100).
*/
int arti_bootstrap_progress(void);
/**
* Get the current bootstrap summary string.
*
* # Arguments
* * `buf` - Buffer to write the summary into
* * `len` - Length of the buffer
*
* # Returns
* * Number of bytes written (not including null terminator)
* * -1 if buffer is null or too small
*/
int arti_bootstrap_summary(char *buf, int len);
/**
* Signal Arti to go dormant (reduce resource usage).
* This is a hint; Arti may not fully support dormant mode yet.
*
* # Returns
* * 0 on success
* * -1 if not running
*/
int arti_go_dormant(void);
/**
* Signal Arti to wake from dormant mode.
*
* # Returns
* * 0 on success
* * -1 if not running
*/
int arti_wake(void);
#endif /* ARTI_H */
@@ -0,0 +1,78 @@
#ifndef ARTI_H
#define ARTI_H
#include <stdint.h>
#include <stdbool.h>
/**
* Start Arti with a SOCKS5 proxy.
*
* # Arguments
* * `data_dir` - Path to data directory for Tor state (C string)
* * `socks_port` - Port for SOCKS5 proxy (e.g., 39050)
*
* # Returns
* * 0 on success
* * -1 if already running
* * -2 if data_dir is invalid
* * -3 if runtime initialization failed
* * -4 if bootstrap failed
*/
int arti_start(const char *data_dir, uint16_t socks_port);
/**
* Stop Arti gracefully.
*
* # Returns
* * 0 on success
* * -1 if not running
*/
int arti_stop(void);
/**
* Check if Arti is currently running.
*
* # Returns
* * 1 if running
* * 0 if not running
*/
int arti_is_running(void);
/**
* Get the current bootstrap progress (0-100).
*/
int arti_bootstrap_progress(void);
/**
* Get the current bootstrap summary string.
*
* # Arguments
* * `buf` - Buffer to write the summary into
* * `len` - Length of the buffer
*
* # Returns
* * Number of bytes written (not including null terminator)
* * -1 if buffer is null or too small
*/
int arti_bootstrap_summary(char *buf, int len);
/**
* Signal Arti to go dormant (reduce resource usage).
* This is a hint; Arti may not fully support dormant mode yet.
*
* # Returns
* * 0 on success
* * -1 if not running
*/
int arti_go_dormant(void);
/**
* Signal Arti to wake from dormant mode.
*
* # Returns
* * 0 on success
* * -1 if not running
*/
int arti_wake(void);
#endif /* ARTI_H */
@@ -0,0 +1,78 @@
#ifndef ARTI_H
#define ARTI_H
#include <stdint.h>
#include <stdbool.h>
/**
* Start Arti with a SOCKS5 proxy.
*
* # Arguments
* * `data_dir` - Path to data directory for Tor state (C string)
* * `socks_port` - Port for SOCKS5 proxy (e.g., 39050)
*
* # Returns
* * 0 on success
* * -1 if already running
* * -2 if data_dir is invalid
* * -3 if runtime initialization failed
* * -4 if bootstrap failed
*/
int arti_start(const char *data_dir, uint16_t socks_port);
/**
* Stop Arti gracefully.
*
* # Returns
* * 0 on success
* * -1 if not running
*/
int arti_stop(void);
/**
* Check if Arti is currently running.
*
* # Returns
* * 1 if running
* * 0 if not running
*/
int arti_is_running(void);
/**
* Get the current bootstrap progress (0-100).
*/
int arti_bootstrap_progress(void);
/**
* Get the current bootstrap summary string.
*
* # Arguments
* * `buf` - Buffer to write the summary into
* * `len` - Length of the buffer
*
* # Returns
* * Number of bytes written (not including null terminator)
* * -1 if buffer is null or too small
*/
int arti_bootstrap_summary(char *buf, int len);
/**
* Signal Arti to go dormant (reduce resource usage).
* This is a hint; Arti may not fully support dormant mode yet.
*
* # Returns
* * 0 on success
* * -1 if not running
*/
int arti_go_dormant(void);
/**
* Signal Arti to wake from dormant mode.
*
* # Returns
* * 0 on success
* * -1 if not running
*/
int arti_wake(void);
#endif /* ARTI_H */
+46
View File
@@ -0,0 +1,46 @@
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "Tor", // Keep name "Tor" for drop-in compatibility
platforms: [
.iOS(.v16),
.macOS(.v13),
],
products: [
.library(
name: "Tor",
targets: ["Tor"]
),
],
dependencies: [
.package(path: "../BitLogger"),
],
targets: [
// Main Swift target
.target(
name: "Tor",
dependencies: [
"arti",
.product(name: "BitLogger", package: "BitLogger"),
],
path: "Sources",
exclude: ["C"],
sources: [
"TorManager.swift",
"TorURLSession.swift",
"TorNotifications.swift",
],
linkerSettings: [
.linkedLibrary("resolv"),
.linkedLibrary("z"),
.linkedLibrary("sqlite3"),
]
),
// Binary framework containing the Rust static library
.binaryTarget(
name: "arti",
path: "Frameworks/arti.xcframework"
),
]
)
+3
View File
@@ -0,0 +1,3 @@
// Empty shim file to satisfy SPM target requirements.
// The actual implementation is in the Rust static library (arti.xcframework).
// This file exists only to make SPM happy with a C target.
@@ -0,0 +1,71 @@
#ifndef ARTI_H
#define ARTI_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Start Arti with a SOCKS5 proxy.
*
* @param data_dir Path to data directory for Tor state (C string)
* @param socks_port Port for SOCKS5 proxy (e.g., 39050)
* @return 0 on success, negative on error:
* -1: already running
* -2: invalid data_dir
* -3: runtime initialization failed
* -4: bootstrap failed
*/
int32_t arti_start(const char *data_dir, uint16_t socks_port);
/**
* Stop Arti gracefully.
*
* @return 0 on success, -1 if not running
*/
int32_t arti_stop(void);
/**
* Check if Arti is currently running.
*
* @return 1 if running, 0 if not running
*/
int32_t arti_is_running(void);
/**
* Get the current bootstrap progress (0-100).
*
* @return Progress percentage
*/
int32_t arti_bootstrap_progress(void);
/**
* Get the current bootstrap summary string.
*
* @param buf Buffer to write the summary into
* @param len Length of the buffer
* @return Number of bytes written, -1 on error
*/
int32_t arti_bootstrap_summary(char *buf, int32_t len);
/**
* Signal Arti to go dormant (reduce resource usage).
*
* @return 0 on success, -1 if not running
*/
int32_t arti_go_dormant(void);
/**
* Signal Arti to wake from dormant mode.
*
* @return 0 on success, -1 if not running
*/
int32_t arti_wake(void);
#ifdef __cplusplus
}
#endif
#endif /* ARTI_H */
@@ -0,0 +1,4 @@
module ArtiC {
header "arti.h"
export *
}
+498
View File
@@ -0,0 +1,498 @@
import BitLogger
import Foundation
#if canImport(Network)
import Network
#endif
#if !canImport(Network)
private final class NWPathMonitor {
var pathUpdateHandler: ((Any) -> Void)?
func start(queue: DispatchQueue) {
// Path monitoring is unavailable on this platform; nothing to do.
}
}
#endif
// FFI declarations for Arti (Rust)
@_silgen_name("arti_start")
private func arti_start(_ dataDir: UnsafePointer<CChar>, _ socksPort: UInt16) -> Int32
@_silgen_name("arti_stop")
private func arti_stop() -> Int32
@_silgen_name("arti_is_running")
private func arti_is_running() -> Int32
@_silgen_name("arti_bootstrap_progress")
private func arti_bootstrap_progress() -> Int32
@_silgen_name("arti_bootstrap_summary")
private func arti_bootstrap_summary(_ buf: UnsafeMutablePointer<CChar>, _ len: Int32) -> Int32
@_silgen_name("arti_go_dormant")
private func arti_go_dormant() -> Int32
@_silgen_name("arti_wake")
private func arti_wake() -> Int32
/// Arti-based Tor integration for BitChat.
/// - Boots a local Arti client and exposes a SOCKS5 proxy
/// on 127.0.0.1:socksPort. All app networking should await readiness and
/// route via this proxy. Fails closed by default when Tor is unavailable.
@MainActor
public final class TorManager: ObservableObject {
public static let shared = TorManager()
// SOCKS endpoint where Arti listens
let socksHost: String = "127.0.0.1"
let socksPort: Int = 39050
// State
@Published private(set) public var isReady: Bool = false
@Published private(set) var isStarting: Bool = false
@Published private(set) var lastError: Error?
@Published private(set) var bootstrapProgress: Int = 0
@Published private(set) var bootstrapSummary: String = ""
// Internal readiness trackers
private var socksReady: Bool = false { didSet { recomputeReady() } }
private var restarting: Bool = false
// Whether the app must enforce Tor for all connections (fail-closed).
public var torEnforced: Bool {
#if BITCHAT_DEV_ALLOW_CLEARNET
return false
#else
return true
#endif
}
// Returns true only when Tor is actually up (or dev fallback is compiled).
var networkPermitted: Bool {
if torEnforced { return isReady }
return true
}
private var didStart = false
private var bootstrapMonitorStarted = false
private var pathMonitor: NWPathMonitor?
private var isAppForeground: Bool = true
private var isDormant: Bool = false
private var lastRestartAt: Date? = nil
private var startedAt: Date? = nil // Tracks initial startup time for grace period
private(set) var allowAutoStart: Bool = false
private init() {}
// MARK: - Public API
public func startIfNeeded() {
guard allowAutoStart else { return }
guard isAppForeground else { return }
guard !didStart else { return }
didStart = true
isDormant = false
isStarting = true
startedAt = Date() // Track startup time for grace period
SecureLogger.debug("TorManager: startIfNeeded() - startedAt set", category: .session)
lastError = nil
NotificationCenter.default.post(name: .TorWillStart, object: nil)
ensureFilesystemLayout()
startArti()
startPathMonitorIfNeeded()
}
public func setAppForeground(_ foreground: Bool) {
isAppForeground = foreground
}
public func isForeground() -> Bool { isAppForeground }
nonisolated
public func awaitReady(timeout: TimeInterval = 25.0) async -> Bool {
await MainActor.run {
if self.isAppForeground { self.startIfNeeded() }
}
let deadline = Date().addingTimeInterval(timeout)
if await MainActor.run(body: { self.networkPermitted }) { return true }
while Date() < deadline {
try? await Task.sleep(nanoseconds: 200_000_000)
if await MainActor.run(body: { self.networkPermitted }) { return true }
}
return await MainActor.run(body: { self.networkPermitted })
}
// MARK: - Filesystem
func dataDirectoryURL() -> URL? {
do {
let base = try FileManager.default.url(
for: .applicationSupportDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true
)
let dir = base.appendingPathComponent("bitchat/arti", isDirectory: true)
return dir
} catch {
return nil
}
}
private func ensureFilesystemLayout() {
guard let dir = dataDirectoryURL() else { return }
do {
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
} catch {
// Non-fatal; Arti will surface errors during start if paths are missing
}
}
// MARK: - Arti Integration
private func startArti() {
guard let dir = dataDirectoryURL()?.path else {
isStarting = false
lastError = NSError(domain: "TorManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "No data directory"])
return
}
// Check if already running
if arti_is_running() != 0 {
SecureLogger.info("TorManager: Arti already running", category: .session)
startBootstrapMonitor()
return
}
let result = dir.withCString { dptr in
arti_start(dptr, UInt16(socksPort))
}
if result != 0 {
SecureLogger.error("TorManager: arti_start failed rc=\(result)", category: .session)
isStarting = false
lastError = NSError(domain: "TorManager", code: Int(result), userInfo: [NSLocalizedDescriptionKey: "Arti start failed"])
return
}
SecureLogger.info("TorManager: arti_start OK (SOCKS \(socksHost):\(socksPort))", category: .session)
startBootstrapMonitor()
// Start SOCKS readiness probe
Task.detached(priority: .userInitiated) { [weak self] in
guard let self else { return }
let ready = await self.waitForSocksReady(timeout: 60.0)
await MainActor.run {
self.socksReady = ready
if ready {
SecureLogger.info("TorManager: SOCKS ready at \(self.socksHost):\(self.socksPort)", category: .session)
} else {
self.lastError = NSError(domain: "TorManager", code: -14, userInfo: [NSLocalizedDescriptionKey: "SOCKS not reachable"])
SecureLogger.error("TorManager: SOCKS not reachable (timeout)", category: .session)
}
}
}
}
private func waitForSocksReady(timeout: TimeInterval) async -> Bool {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
if await probeSocksOnce() { return true }
try? await Task.sleep(nanoseconds: 250_000_000)
}
return false
}
private func probeSocksOnce() async -> Bool {
#if canImport(Network)
await withCheckedContinuation { cont in
let params = NWParameters.tcp
let host = NWEndpoint.Host.ipv4(.loopback)
guard let port = NWEndpoint.Port(rawValue: UInt16(socksPort)) else {
cont.resume(returning: false)
return
}
let endpoint = NWEndpoint.hostPort(host: host, port: port)
let conn = NWConnection(to: endpoint, using: params)
var resumed = false
let resumeOnce: (Bool) -> Void = { value in
if !resumed {
resumed = true
cont.resume(returning: value)
}
}
conn.stateUpdateHandler = { state in
switch state {
case .ready:
resumeOnce(true)
conn.cancel()
case .failed, .cancelled:
resumeOnce(false)
conn.cancel()
default:
break
}
}
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 1.0) {
resumeOnce(false)
conn.cancel()
}
conn.start(queue: DispatchQueue.global(qos: .utility))
}
#else
return false
#endif
}
// MARK: - Bootstrap Monitoring
private func startBootstrapMonitor() {
guard !bootstrapMonitorStarted else { return }
bootstrapMonitorStarted = true
Task.detached(priority: .utility) { [weak self] in
await self?.bootstrapPollLoop()
}
}
private func bootstrapPollLoop() async {
let deadline = Date().addingTimeInterval(75)
while Date() < deadline {
let progress = Int(arti_bootstrap_progress())
let summary = getBootstrapSummary()
await MainActor.run {
self.bootstrapProgress = progress
self.bootstrapSummary = summary
if progress >= 100 { self.isStarting = false }
self.recomputeReady()
}
if progress >= 100 { break }
try? await Task.sleep(nanoseconds: 1_000_000_000)
}
}
private func getBootstrapSummary() -> String {
var buf = [CChar](repeating: 0, count: 256)
let len = arti_bootstrap_summary(&buf, Int32(buf.count))
if len > 0 {
return String(cString: buf)
}
return ""
}
// MARK: - Foreground/Background
public func ensureRunningOnForeground() {
if !allowAutoStart { return }
Task.detached(priority: .userInitiated) { [weak self] in
guard let self = self else { return }
let claimed: Bool = await MainActor.run {
if self.isStarting || self.restarting { return false }
self.restarting = true
return true
}
if !claimed { return }
// Try to wake if dormant
if await self.wakeFromDormant() {
await MainActor.run { self.restarting = false }
return
}
// Check if already ready
let alreadyReady = await MainActor.run { self.isReady }
if alreadyReady {
await MainActor.run { self.restarting = false }
return
}
// Restart
await self.restartArti()
await MainActor.run { self.restarting = false }
}
}
public func goDormantOnBackground() {
SecureLogger.debug("TorManager: goDormantOnBackground() called", category: .session)
Task.detached { [weak self] in
guard let self = self else { return }
let result = arti_go_dormant()
if result == 0 {
SecureLogger.info("TorManager: signalled DORMANT", category: .session)
await MainActor.run {
self.isDormant = true
self.isReady = false
self.socksReady = false
self.isStarting = false
}
} else {
// Dormant not supported, do full shutdown
SecureLogger.warning("TorManager: DORMANT failed; shutting down", category: .session)
_ = arti_stop()
await MainActor.run {
self.isDormant = false
self.isReady = false
self.socksReady = false
self.bootstrapProgress = 0
self.bootstrapSummary = ""
self.isStarting = false
self.didStart = false
self.bootstrapMonitorStarted = false
// Note: Don't clear startedAt - it will be set fresh on next start
}
}
}
}
public func shutdownCompletely() {
SecureLogger.debug("TorManager: shutdownCompletely() called", category: .session)
Task.detached { [weak self] in
guard let self = self else { return }
_ = arti_stop()
// Wait for shutdown
var waited = 0
while arti_is_running() != 0 && waited < 50 {
try? await Task.sleep(nanoseconds: 100_000_000)
waited += 1
}
await MainActor.run {
self.isDormant = false
self.isReady = false
self.socksReady = false
self.bootstrapProgress = 0
self.bootstrapSummary = ""
self.isStarting = false
self.didStart = false
self.restarting = false
self.bootstrapMonitorStarted = false
// Note: Don't clear startedAt here - it will be set fresh on next startIfNeeded()
// Clearing it here races with startup and defeats the grace period
}
}
}
private func wakeFromDormant() async -> Bool {
let wasDormant = await MainActor.run { self.isDormant }
if !wasDormant { return false }
let result = arti_wake()
if result != 0 { return false }
await MainActor.run {
self.isDormant = false
self.isStarting = true
self.socksReady = false
}
let ready = await waitForSocksReady(timeout: 12.0)
await MainActor.run {
self.socksReady = ready
self.isStarting = !ready
}
if ready {
SecureLogger.info("TorManager: woke from dormant", category: .session)
}
return ready
}
private func restartArti() async {
SecureLogger.debug("TorManager: restartArti() starting", category: .session)
await MainActor.run {
NotificationCenter.default.post(name: .TorWillRestart, object: nil)
self.isReady = false
self.socksReady = false
self.bootstrapProgress = 0
self.bootstrapSummary = ""
self.isStarting = true
self.isDormant = false
self.lastRestartAt = Date()
}
_ = arti_stop()
// Wait for stop
var waited = 0
while arti_is_running() != 0 && waited < 40 {
try? await Task.sleep(nanoseconds: 100_000_000)
waited += 1
}
await MainActor.run {
self.bootstrapMonitorStarted = false
self.didStart = false
}
await MainActor.run { self.startIfNeeded() }
}
private func recomputeReady() {
let ready = socksReady && bootstrapProgress >= 100
if ready != isReady {
if !ready {
SecureLogger.debug("TorManager: isReady -> false (socksReady=\(socksReady), bootstrap=\(bootstrapProgress))", category: .session)
}
isReady = ready
if ready {
NotificationCenter.default.post(name: .TorDidBecomeReady, object: nil)
}
}
}
private func startPathMonitorIfNeeded() {
#if canImport(Network)
guard pathMonitor == nil else { return }
let monitor = NWPathMonitor()
pathMonitor = monitor
let queue = DispatchQueue(label: "TorPathMonitor")
monitor.pathUpdateHandler = { [weak self] _ in
Task { @MainActor in
guard let self = self else { return }
if self.isAppForeground {
self.pokeTorOnPathChange()
}
}
}
monitor.start(queue: queue)
#endif
}
private func pokeTorOnPathChange() {
// Skip if we recently restarted
if let last = lastRestartAt, Date().timeIntervalSince(last) < 3.0 {
SecureLogger.debug("TorManager: pokeTorOnPathChange() skipped - recent restart", category: .session)
return
}
// Skip during initial startup grace period (15s) to avoid race conditions
if let started = startedAt, Date().timeIntervalSince(started) < 15.0 {
SecureLogger.debug("TorManager: pokeTorOnPathChange() skipped - startup grace period (\(Int(Date().timeIntervalSince(started)))s)", category: .session)
return
}
if isStarting || restarting {
SecureLogger.debug("TorManager: pokeTorOnPathChange() skipped - isStarting=\(isStarting) restarting=\(restarting)", category: .session)
return
}
if isReady { return }
SecureLogger.debug("TorManager: pokeTorOnPathChange() - Arti not ready, initiating recovery", category: .session)
ensureRunningOnForeground()
}
}
// MARK: - Start policy configuration
extension TorManager {
@MainActor
public func setAutoStartAllowed(_ allow: Bool) {
allowAutoStart = allow
}
@MainActor
public func isAutoStartAllowed() -> Bool { allowAutoStart }
}
@@ -0,0 +1,8 @@
import Foundation
public extension Notification.Name {
static let TorDidBecomeReady = Notification.Name("TorDidBecomeReady")
static let TorWillRestart = Notification.Name("TorWillRestart")
static let TorWillStart = Notification.Name("TorWillStart")
static let TorUserPreferenceChanged = Notification.Name("TorUserPreferenceChanged")
}
@@ -0,0 +1,63 @@
import Foundation
#if os(macOS)
import CFNetwork
#endif
/// Provides a shared URLSession that routes traffic via Tor's SOCKS5 proxy
/// when Tor is enforced/ready. Allows swapping between proxied and direct
/// sessions so UI can toggle Tor usage at runtime.
public final class TorURLSession {
public static let shared = TorURLSession()
// Default (no proxy) session for direct Nostr access when Tor is disabled.
private var defaultSession: URLSession = TorURLSession.makeDefaultSession()
// Proxied (SOCKS5) session that routes through Tor.
private var torSession: URLSession = TorURLSession.makeTorSession()
private var useTorProxy: Bool = true
public var session: URLSession {
useTorProxy ? torSession : defaultSession
}
// Recreate sessions so new clients bind to the fresh SOCKS/control ports after a Tor restart.
public func rebuild() {
defaultSession = TorURLSession.makeDefaultSession()
torSession = TorURLSession.makeTorSession()
}
public func setProxyMode(useTor: Bool) {
guard useTorProxy != useTor else { return }
useTorProxy = useTor
rebuild()
}
private static func makeTorSession() -> URLSession {
let cfg = URLSessionConfiguration.ephemeral
cfg.waitsForConnectivity = true
// Keep in sync with TorManager defaults
let host = "127.0.0.1"
let port = 39050
#if os(macOS)
cfg.connectionProxyDictionary = [
kCFNetworkProxiesSOCKSEnable as String: 1,
kCFNetworkProxiesSOCKSProxy as String: host,
kCFNetworkProxiesSOCKSPort as String: port
]
#else
// iOS: CFNetwork SOCKS proxy keys are unavailable at compile time.
cfg.connectionProxyDictionary = [
"SOCKSEnable": 1,
"SOCKSProxy": host,
"SOCKSPort": port
]
#endif
return URLSession(configuration: cfg)
}
private static func makeDefaultSession() -> URLSession {
let cfg = URLSessionConfiguration.default
cfg.waitsForConnectivity = true
return URLSession(configuration: cfg)
}
}
@@ -0,0 +1,38 @@
[package]
name = "arti-bitchat"
version = "0.1.0"
edition = "2021"
rust-version = "1.86"
[lib]
crate-type = ["staticlib"]
[dependencies]
# Arti core - minimal features for client-only SOCKS proxy
arti-client = { version = "0.38", default-features = false, features = [
"tokio",
"rustls",
] }
# Async runtime
tokio = { version = "1", default-features = false, features = [
"rt-multi-thread",
"net",
"sync",
"time",
"macros",
] }
# Tor runtime compatibility
tor-rtcompat = { version = "0.38", default-features = false, features = ["tokio"] }
# FFI utilities
libc = "0.2"
once_cell = "1"
# Logging (minimal)
tracing = "0.1"
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] }
[features]
default = []
@@ -0,0 +1,13 @@
language = "C"
include_guard = "ARTI_H"
no_includes = true
sys_includes = ["stdint.h", "stdbool.h"]
[export]
include = ["arti_start", "arti_stop", "arti_is_running", "arti_bootstrap_progress", "arti_bootstrap_summary", "arti_go_dormant", "arti_wake"]
[fn]
args = "Auto"
[parse]
parse_deps = false
+315
View File
@@ -0,0 +1,315 @@
//! arti-bitchat: Minimal FFI wrapper around arti-client for BitChat
//!
//! Provides a C-compatible interface for embedding Arti (Rust Tor) in iOS/macOS apps.
//! Exposes a SOCKS5 proxy on localhost that Swift code can route traffic through.
use std::ffi::{c_char, c_int, CStr};
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use std::sync::{Arc, Mutex};
use arti_client::TorClient;
use once_cell::sync::OnceCell;
use tokio::net::TcpListener;
use tokio::runtime::Runtime;
use tokio::sync::oneshot;
use tor_rtcompat::PreferredRuntime;
mod socks;
/// Global state for the Arti instance
struct ArtiState {
/// Tokio runtime (owned, single instance)
runtime: Runtime,
/// Shutdown signal sender
shutdown_tx: Option<oneshot::Sender<()>>,
/// TorClient handle for status queries
client: Option<Arc<TorClient<PreferredRuntime>>>,
}
static ARTI_STATE: OnceCell<Mutex<ArtiState>> = OnceCell::new();
static BOOTSTRAP_PROGRESS: AtomicI32 = AtomicI32::new(0);
static IS_RUNNING: AtomicBool = AtomicBool::new(false);
static BOOTSTRAP_SUMMARY: Mutex<String> = Mutex::new(String::new());
/// Initialize the global state with a new runtime
fn init_state() -> Result<(), &'static str> {
ARTI_STATE.get_or_try_init(|| -> Result<Mutex<ArtiState>, &'static str> {
let runtime = Runtime::new().map_err(|_| "Failed to create tokio runtime")?;
Ok(Mutex::new(ArtiState {
runtime,
shutdown_tx: None,
client: None,
}))
})?;
Ok(())
}
/// Start Arti with a SOCKS5 proxy.
///
/// # Arguments
/// * `data_dir` - Path to data directory for Tor state (C string)
/// * `socks_port` - Port for SOCKS5 proxy (e.g., 39050)
///
/// # Returns
/// * 0 on success
/// * -1 if already running
/// * -2 if data_dir is invalid
/// * -3 if runtime initialization failed
/// * -4 if bootstrap failed
#[no_mangle]
pub extern "C" fn arti_start(data_dir: *const c_char, socks_port: u16) -> c_int {
// Check if already running
if IS_RUNNING.load(Ordering::SeqCst) {
return -1;
}
// Parse data directory
let data_path = match unsafe { CStr::from_ptr(data_dir) }.to_str() {
Ok(s) => PathBuf::from(s),
Err(_) => return -2,
};
// Initialize runtime if needed
if let Err(_) = init_state() {
return -3;
}
let state = match ARTI_STATE.get() {
Some(s) => s,
None => return -3,
};
let mut guard = match state.lock() {
Ok(g) => g,
Err(_) => return -3,
};
// Create shutdown channel
let (shutdown_tx, shutdown_rx) = oneshot::channel();
guard.shutdown_tx = Some(shutdown_tx);
let socks_addr: SocketAddr = format!("127.0.0.1:{}", socks_port)
.parse()
.expect("valid addr");
// Spawn the main Arti task
let data_path_clone = data_path.clone();
guard.runtime.spawn(async move {
match run_arti(data_path_clone, socks_addr, shutdown_rx).await {
Ok(_) => {
tracing::info!("Arti shutdown cleanly");
}
Err(e) => {
tracing::error!("Arti error: {}", e);
update_summary(&format!("Error: {}", e));
}
}
IS_RUNNING.store(false, Ordering::SeqCst);
BOOTSTRAP_PROGRESS.store(0, Ordering::SeqCst);
});
IS_RUNNING.store(true, Ordering::SeqCst);
BOOTSTRAP_PROGRESS.store(0, Ordering::SeqCst);
update_summary("Starting...");
0
}
/// Stop Arti gracefully.
///
/// # Returns
/// * 0 on success
/// * -1 if not running
#[no_mangle]
pub extern "C" fn arti_stop() -> c_int {
if !IS_RUNNING.load(Ordering::SeqCst) {
return -1;
}
let state = match ARTI_STATE.get() {
Some(s) => s,
None => return -1,
};
let mut guard = match state.lock() {
Ok(g) => g,
Err(_) => return -1,
};
// Send shutdown signal
if let Some(tx) = guard.shutdown_tx.take() {
let _ = tx.send(());
}
// Clear client reference
guard.client = None;
// Give async tasks time to complete
std::thread::sleep(std::time::Duration::from_millis(200));
IS_RUNNING.store(false, Ordering::SeqCst);
BOOTSTRAP_PROGRESS.store(0, Ordering::SeqCst);
update_summary("");
0
}
/// Check if Arti is currently running.
///
/// # Returns
/// * 1 if running
/// * 0 if not running
#[no_mangle]
pub extern "C" fn arti_is_running() -> c_int {
if IS_RUNNING.load(Ordering::SeqCst) {
1
} else {
0
}
}
/// Get the current bootstrap progress (0-100).
#[no_mangle]
pub extern "C" fn arti_bootstrap_progress() -> c_int {
BOOTSTRAP_PROGRESS.load(Ordering::SeqCst)
}
/// Get the current bootstrap summary string.
///
/// # Arguments
/// * `buf` - Buffer to write the summary into
/// * `len` - Length of the buffer
///
/// # Returns
/// * Number of bytes written (not including null terminator)
/// * -1 if buffer is null or too small
#[no_mangle]
pub extern "C" fn arti_bootstrap_summary(buf: *mut c_char, len: c_int) -> c_int {
if buf.is_null() || len <= 0 {
return -1;
}
let summary = match BOOTSTRAP_SUMMARY.lock() {
Ok(s) => s.clone(),
Err(_) => return -1,
};
let bytes = summary.as_bytes();
let copy_len = std::cmp::min(bytes.len(), (len - 1) as usize);
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, copy_len);
*buf.add(copy_len) = 0; // null terminator
}
copy_len as c_int
}
/// Signal Arti to go dormant (reduce resource usage).
/// This is a hint; Arti may not fully support dormant mode yet.
///
/// # Returns
/// * 0 on success
/// * -1 if not running
#[no_mangle]
pub extern "C" fn arti_go_dormant() -> c_int {
if !IS_RUNNING.load(Ordering::SeqCst) {
return -1;
}
// Arti doesn't have explicit dormant mode yet, but we can note the intent
update_summary("Dormant");
0
}
/// Signal Arti to wake from dormant mode.
///
/// # Returns
/// * 0 on success
/// * -1 if not running
#[no_mangle]
pub extern "C" fn arti_wake() -> c_int {
if !IS_RUNNING.load(Ordering::SeqCst) {
return -1;
}
update_summary("Active");
0
}
fn update_summary(s: &str) {
if let Ok(mut guard) = BOOTSTRAP_SUMMARY.lock() {
guard.clear();
guard.push_str(s);
}
}
/// Main async entry point for Arti
async fn run_arti(
data_dir: PathBuf,
socks_addr: SocketAddr,
mut shutdown_rx: oneshot::Receiver<()>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Ensure data directory exists
std::fs::create_dir_all(&data_dir)?;
update_summary("Configuring...");
// Build Arti configuration with custom directories
let cache_dir = data_dir.join("cache");
let state_dir = data_dir.join("state");
// Use from_directories which sets up storage correctly
use arti_client::config::TorClientConfigBuilder;
let config = TorClientConfigBuilder::from_directories(state_dir, cache_dir)
.build()?;
update_summary("Bootstrapping...");
// Create and bootstrap the Tor client
let client = TorClient::create_bootstrapped(config).await?;
let client = Arc::new(client);
// Store client reference for status queries
if let Some(state) = ARTI_STATE.get() {
if let Ok(mut guard) = state.lock() {
guard.client = Some(client.clone());
}
}
// Mark bootstrap complete
BOOTSTRAP_PROGRESS.store(100, Ordering::SeqCst);
update_summary("Ready");
// Bind SOCKS listener
let listener = TcpListener::bind(socks_addr).await?;
tracing::info!("SOCKS5 proxy listening on {}", socks_addr);
// Accept connections until shutdown
loop {
tokio::select! {
accept_result = listener.accept() => {
match accept_result {
Ok((stream, peer_addr)) => {
let client = client.clone();
tokio::spawn(async move {
if let Err(e) = socks::handle_socks_connection(stream, peer_addr, client).await {
tracing::debug!("SOCKS connection error from {}: {}", peer_addr, e);
}
});
}
Err(e) => {
tracing::warn!("Accept error: {}", e);
}
}
}
_ = &mut shutdown_rx => {
tracing::info!("Shutdown signal received");
break;
}
}
}
update_summary("Shutting down...");
Ok(())
}
@@ -0,0 +1,208 @@
//! SOCKS5 protocol handler for Arti
//!
//! Implements a minimal SOCKS5 server that forwards connections through Tor.
use std::io;
use std::net::SocketAddr;
use std::sync::Arc;
use arti_client::{TorClient, IntoTorAddr};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tor_rtcompat::PreferredRuntime;
// SOCKS5 constants
const SOCKS5_VERSION: u8 = 0x05;
const SOCKS5_AUTH_NONE: u8 = 0x00;
const SOCKS5_CMD_CONNECT: u8 = 0x01;
const SOCKS5_ATYP_IPV4: u8 = 0x01;
const SOCKS5_ATYP_DOMAIN: u8 = 0x03;
const SOCKS5_ATYP_IPV6: u8 = 0x04;
const SOCKS5_REP_SUCCESS: u8 = 0x00;
const SOCKS5_REP_FAILURE: u8 = 0x01;
const SOCKS5_REP_CONN_REFUSED: u8 = 0x05;
/// Handle a single SOCKS5 connection
pub async fn handle_socks_connection(
mut stream: TcpStream,
peer_addr: SocketAddr,
client: Arc<TorClient<PreferredRuntime>>,
) -> io::Result<()> {
// --- Greeting ---
// Client sends: VER | NMETHODS | METHODS
let mut greeting = [0u8; 2];
stream.read_exact(&mut greeting).await?;
if greeting[0] != SOCKS5_VERSION {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Not SOCKS5",
));
}
let nmethods = greeting[1] as usize;
let mut methods = vec![0u8; nmethods];
stream.read_exact(&mut methods).await?;
// We only support no-auth
if !methods.contains(&SOCKS5_AUTH_NONE) {
// Send failure: no acceptable methods
stream.write_all(&[SOCKS5_VERSION, 0xFF]).await?;
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"No acceptable auth methods",
));
}
// Accept no-auth
stream.write_all(&[SOCKS5_VERSION, SOCKS5_AUTH_NONE]).await?;
// --- Request ---
// Client sends: VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT
let mut request_header = [0u8; 4];
stream.read_exact(&mut request_header).await?;
if request_header[0] != SOCKS5_VERSION {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Invalid SOCKS5 request version",
));
}
let cmd = request_header[1];
let atyp = request_header[3];
if cmd != SOCKS5_CMD_CONNECT {
// We only support CONNECT
send_reply(&mut stream, SOCKS5_REP_FAILURE).await?;
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"Only CONNECT supported",
));
}
// Parse destination address
let (dest_host, dest_port) = match atyp {
SOCKS5_ATYP_IPV4 => {
let mut addr = [0u8; 4];
stream.read_exact(&mut addr).await?;
let mut port_buf = [0u8; 2];
stream.read_exact(&mut port_buf).await?;
let port = u16::from_be_bytes(port_buf);
let host = format!("{}.{}.{}.{}", addr[0], addr[1], addr[2], addr[3]);
(host, port)
}
SOCKS5_ATYP_DOMAIN => {
let mut len_buf = [0u8; 1];
stream.read_exact(&mut len_buf).await?;
let len = len_buf[0] as usize;
let mut domain = vec![0u8; len];
stream.read_exact(&mut domain).await?;
let mut port_buf = [0u8; 2];
stream.read_exact(&mut port_buf).await?;
let port = u16::from_be_bytes(port_buf);
let host = String::from_utf8_lossy(&domain).to_string();
(host, port)
}
SOCKS5_ATYP_IPV6 => {
let mut addr = [0u8; 16];
stream.read_exact(&mut addr).await?;
let mut port_buf = [0u8; 2];
stream.read_exact(&mut port_buf).await?;
let port = u16::from_be_bytes(port_buf);
// Format IPv6 address
let segments: Vec<String> = addr
.chunks(2)
.map(|c| format!("{:02x}{:02x}", c[0], c[1]))
.collect();
let host = format!("[{}]", segments.join(":"));
(host, port)
}
_ => {
send_reply(&mut stream, SOCKS5_REP_FAILURE).await?;
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Unsupported address type",
));
}
};
tracing::debug!("SOCKS5 CONNECT from {} to {}:{}", peer_addr, dest_host, dest_port);
// Connect through Tor
let tor_addr = format!("{}:{}", dest_host, dest_port);
let tor_addr = match tor_addr.as_str().into_tor_addr() {
Ok(a) => a,
Err(e) => {
tracing::debug!("Invalid Tor address: {}", e);
send_reply(&mut stream, SOCKS5_REP_FAILURE).await?;
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Invalid Tor address: {}", e),
));
}
};
let tor_stream = match client.connect(tor_addr).await {
Ok(s) => s,
Err(e) => {
tracing::debug!("Tor connect failed: {}", e);
send_reply(&mut stream, SOCKS5_REP_CONN_REFUSED).await?;
return Err(io::Error::new(
io::ErrorKind::ConnectionRefused,
e.to_string(),
));
}
};
// Send success reply
// Reply: VER | REP | RSV | ATYP | BND.ADDR | BND.PORT
// We use 0.0.0.0:0 as the bound address since we're proxying
let reply = [
SOCKS5_VERSION,
SOCKS5_REP_SUCCESS,
0x00, // RSV
SOCKS5_ATYP_IPV4,
0, 0, 0, 0, // BND.ADDR
0, 0, // BND.PORT
];
stream.write_all(&reply).await?;
// Bidirectional copy
let (mut client_read, mut client_write) = stream.into_split();
let (mut tor_read, mut tor_write) = tor_stream.split();
let client_to_tor = async {
tokio::io::copy(&mut client_read, &mut tor_write).await
};
let tor_to_client = async {
tokio::io::copy(&mut tor_read, &mut client_write).await
};
tokio::select! {
result = client_to_tor => {
if let Err(e) = result {
tracing::debug!("Client to Tor copy error: {}", e);
}
}
result = tor_to_client => {
if let Err(e) = result {
tracing::debug!("Tor to client copy error: {}", e);
}
}
}
Ok(())
}
async fn send_reply(stream: &mut TcpStream, rep: u8) -> io::Result<()> {
let reply = [
SOCKS5_VERSION,
rep,
0x00, // RSV
SOCKS5_ATYP_IPV4,
0, 0, 0, 0, // BND.ADDR
0, 0, // BND.PORT
];
stream.write_all(&reply).await
}
+311
View File
@@ -0,0 +1,311 @@
#!/bin/bash
#
# Build arti-bitchat for iOS/macOS with aggressive size optimization
#
# Output: Frameworks/arti.xcframework containing static libraries for:
# - aarch64-apple-ios (iOS device)
# - aarch64-apple-ios-sim (iOS simulator, Apple Silicon)
# - x86_64-apple-ios (iOS simulator, Intel - optional)
# - aarch64-apple-darwin (macOS)
#
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Configuration
CRATE_NAME="arti-bitchat"
LIB_NAME="libarti_bitchat.a"
FRAMEWORK_NAME="arti"
OUTPUT_DIR="$SCRIPT_DIR/Frameworks"
# Targets to build
TARGETS=(
"aarch64-apple-ios" # iOS device
"aarch64-apple-ios-sim" # iOS simulator (Apple Silicon)
"aarch64-apple-darwin" # macOS
)
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Check prerequisites
check_prerequisites() {
log_info "Checking prerequisites..."
if ! command -v rustc &> /dev/null; then
log_error "Rust is not installed. Please install via rustup."
exit 1
fi
if ! command -v cargo &> /dev/null; then
log_error "Cargo is not installed. Please install via rustup."
exit 1
fi
# Check/install targets
for target in "${TARGETS[@]}"; do
if ! rustup target list --installed | grep -q "$target"; then
log_info "Installing target: $target"
rustup target add "$target"
fi
done
# Install cbindgen if needed
if ! command -v cbindgen &> /dev/null; then
log_info "Installing cbindgen..."
cargo install cbindgen
fi
log_info "Prerequisites OK"
}
# Set up aggressive size optimization flags and deployment targets
setup_rustflags() {
local target="$1"
# Base flags for size optimization
export RUSTFLAGS="-C opt-level=z -C lto=fat -C codegen-units=1 -C panic=abort -C strip=symbols"
# Set deployment targets to suppress linker warnings about version mismatches
case "$target" in
*-apple-ios-sim*)
export IPHONEOS_DEPLOYMENT_TARGET="16.0"
# Simulator uses iPhone SDK but needs the sim target
;;
*-apple-ios*)
export IPHONEOS_DEPLOYMENT_TARGET="16.0"
;;
*-apple-darwin*)
export MACOSX_DEPLOYMENT_TARGET="13.0"
;;
esac
log_info "RUSTFLAGS: $RUSTFLAGS"
log_info "Deployment target: MACOSX=$MACOSX_DEPLOYMENT_TARGET IPHONEOS=$IPHONEOS_DEPLOYMENT_TARGET"
}
# Build for a single target
build_target() {
local target="$1"
log_info "Building for target: $target"
setup_rustflags "$target"
# Build release
cargo build --release --target "$target" -p "$CRATE_NAME"
# Check output
local lib_path="target/$target/release/$LIB_NAME"
if [[ -f "$lib_path" ]]; then
local size=$(du -h "$lib_path" | cut -f1)
log_info "Built $lib_path ($size)"
else
log_error "Build failed: $lib_path not found"
exit 1
fi
}
# Create xcframework from built libraries
create_xcframework() {
log_info "Creating xcframework..."
local xcframework_path="$OUTPUT_DIR/$FRAMEWORK_NAME.xcframework"
# Remove existing xcframework
rm -rf "$xcframework_path"
mkdir -p "$OUTPUT_DIR"
# Build the xcodebuild command
local cmd="xcodebuild -create-xcframework"
for target in "${TARGETS[@]}"; do
local lib_path="$SCRIPT_DIR/target/$target/release/$LIB_NAME"
if [[ -f "$lib_path" ]]; then
# Strip the library for additional size reduction
log_info "Stripping $target library..."
strip -x "$lib_path" 2>/dev/null || true
cmd="$cmd -library $lib_path"
# Add headers if they exist
local header_dir="$OUTPUT_DIR/include"
if [[ -d "$header_dir" ]]; then
cmd="$cmd -headers $header_dir"
fi
else
log_warn "Skipping missing library: $lib_path"
fi
done
cmd="$cmd -output $xcframework_path"
log_info "Running: $cmd"
eval "$cmd"
if [[ -d "$xcframework_path" ]]; then
local size=$(du -sh "$xcframework_path" | cut -f1)
log_info "Created $xcframework_path ($size)"
else
log_error "Failed to create xcframework"
exit 1
fi
}
# Generate C header using cbindgen
generate_header() {
log_info "Generating C header..."
local header_dir="$OUTPUT_DIR/include"
local header_path="$header_dir/arti.h"
mkdir -p "$header_dir"
# Create cbindgen.toml if it doesn't exist
if [[ ! -f "$CRATE_NAME/cbindgen.toml" ]]; then
cat > "$CRATE_NAME/cbindgen.toml" << 'EOF'
language = "C"
include_guard = "ARTI_H"
no_includes = true
sys_includes = ["stdint.h", "stdbool.h"]
[export]
include = ["arti_start", "arti_stop", "arti_is_running", "arti_bootstrap_progress", "arti_bootstrap_summary", "arti_go_dormant", "arti_wake"]
[fn]
args = "Auto"
[parse]
parse_deps = false
EOF
fi
cbindgen --config "$CRATE_NAME/cbindgen.toml" \
--crate "$CRATE_NAME" \
--output "$header_path"
if [[ -f "$header_path" ]]; then
log_info "Generated $header_path"
cat "$header_path"
else
log_warn "cbindgen did not generate header, creating manually..."
# Fallback: create header manually
cat > "$header_path" << 'EOF'
#ifndef ARTI_H
#define ARTI_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Start Arti with a SOCKS5 proxy.
*
* @param data_dir Path to data directory for Tor state (C string)
* @param socks_port Port for SOCKS5 proxy (e.g., 39050)
* @return 0 on success, negative on error
*/
int32_t arti_start(const char *data_dir, uint16_t socks_port);
/**
* Stop Arti gracefully.
*
* @return 0 on success, -1 if not running
*/
int32_t arti_stop(void);
/**
* Check if Arti is currently running.
*
* @return 1 if running, 0 if not running
*/
int32_t arti_is_running(void);
/**
* Get the current bootstrap progress (0-100).
*
* @return Progress percentage
*/
int32_t arti_bootstrap_progress(void);
/**
* Get the current bootstrap summary string.
*
* @param buf Buffer to write the summary into
* @param len Length of the buffer
* @return Number of bytes written, -1 on error
*/
int32_t arti_bootstrap_summary(char *buf, int32_t len);
/**
* Signal Arti to go dormant (reduce resource usage).
*
* @return 0 on success, -1 if not running
*/
int32_t arti_go_dormant(void);
/**
* Signal Arti to wake from dormant mode.
*
* @return 0 on success, -1 if not running
*/
int32_t arti_wake(void);
#ifdef __cplusplus
}
#endif
#endif /* ARTI_H */
EOF
log_info "Created manual header at $header_path"
fi
}
# Print size report
print_size_report() {
log_info "=== Size Report ==="
for target in "${TARGETS[@]}"; do
local lib_path="$SCRIPT_DIR/target/$target/release/$LIB_NAME"
if [[ -f "$lib_path" ]]; then
local size=$(du -h "$lib_path" | cut -f1)
echo " $target: $size"
fi
done
local xcframework_path="$OUTPUT_DIR/$FRAMEWORK_NAME.xcframework"
if [[ -d "$xcframework_path" ]]; then
local total_size=$(du -sh "$xcframework_path" | cut -f1)
echo " xcframework total: $total_size"
fi
}
# Main
main() {
log_info "Building arti-bitchat for iOS/macOS"
log_info "=================================="
check_prerequisites
generate_header
for target in "${TARGETS[@]}"; do
build_target "$target"
done
create_xcframework
print_size_report
log_info "Build complete!"
log_info "xcframework: $OUTPUT_DIR/$FRAMEWORK_NAME.xcframework"
}
# Run
main "$@"