diff --git a/docs/WASM_MOBILE_BUILD_PLAN.md b/docs/WASM_MOBILE_BUILD_PLAN.md new file mode 100644 index 0000000..8b9f98f --- /dev/null +++ b/docs/WASM_MOBILE_BUILD_PLAN.md @@ -0,0 +1,417 @@ +# Plan: Enable js/wasm, iOS, and Android Builds + +This document outlines the work required to enable ORLY and the nostr library to build successfully for WebAssembly (js/wasm), iOS (ios/arm64), and Android (android/arm64). + +## Current Build Status + +| Platform | Status | Notes | +|----------|--------|-------| +| linux/amd64 | ✅ Works | Uses libsecp256k1 via purego | +| darwin/arm64 | ✅ Works | Uses pure Go p256k1 | +| darwin/amd64 | ✅ Works | Uses pure Go p256k1 | +| windows/amd64 | ✅ Works | Uses pure Go p256k1 | +| android/arm64 | ✅ Works | Uses pure Go p256k1 | +| js/wasm | ❌ Fails | Missing platform stubs (planned for hackpadfs work) | +| ios/arm64 | ⚠️ Requires gomobile | See iOS section below | + +--- + +## Issue 1: js/wasm Build Failures + +### Problem +Two packages fail to compile for js/wasm due to missing platform-specific implementations: + +1. **`next.orly.dev/pkg/utils/interrupt`** - Missing `Restart()` function +2. **`git.mleku.dev/mleku/nostr/ws`** - Missing `getConnectionOptions()` function + +### Root Cause Analysis + +#### 1.1 interrupt package +The `Restart()` function is defined with build tags: +- `restart.go` → `//go:build linux` +- `restart_darwin.go` → `//go:build darwin` +- `restart_windows.go` → `//go:build windows` + +But `main.go` calls `Restart()` unconditionally on line 66, causing undefined symbol on js/wasm. + +#### 1.2 ws package +The `getConnectionOptions()` function is defined in `connection_options.go` with: +```go +//go:build !js +``` + +This correctly excludes js/wasm, but no alternative implementation exists for js/wasm, so `connection.go` line 28 fails. + +### Solution + +#### 1.1 Fix interrupt package (ORLY) + +Create a new file `restart_other.go`: + +```go +//go:build !linux && !darwin && !windows + +package interrupt + +import ( + "lol.mleku.dev/log" + "os" +) + +// Restart is not supported on this platform - just exit +func Restart() { + log.W.Ln("restart not supported on this platform, exiting") + os.Exit(0) +} +``` + +#### 1.2 Fix ws package (nostr library) + +Create a new file `connection_options_js.go`: + +```go +//go:build js + +package ws + +import ( + "crypto/tls" + "net/http" +) + +// getConnectionOptions returns nil on js/wasm as we use browser WebSocket API +func getConnectionOptions( + requestHeader http.Header, tlsConfig *tls.Config, +) *websocket.Dialer { + // On js/wasm, gorilla/websocket doesn't work - need to use browser APIs + // This is a stub that allows compilation; actual WebSocket usage would + // need a js/wasm-compatible implementation + return nil +} +``` + +**However**, this alone won't make WebSocket work - the entire `ws` package uses `gorilla/websocket` which doesn't support js/wasm. A proper fix requires: + +Option A: Use conditional compilation to swap in a js/wasm WebSocket implementation (e.g., `nhooyr.io/websocket` which supports js/wasm) + +Option B: Make the `ws` package optional with build tags so js/wasm builds exclude it entirely + +**Recommended**: Option B - exclude the ws client package on js/wasm since ORLY is a server, not a client. + +--- + +## Issue 2: iOS Build Failure + +### Problem +``` +ios/arm64 requires external (cgo) linking, but cgo is not enabled +``` + +### Root Cause +iOS requires CGO for all executables due to Apple's linking requirements. This is a fundamental Go limitation - you cannot build iOS binaries with `CGO_ENABLED=0`. + +### Solution + +#### Option A: Accept CGO requirement for iOS +Build with CGO enabled and provide a cross-compilation toolchain: + +```bash +CGO_ENABLED=1 CC=clang GOOS=ios GOARCH=arm64 go build +``` + +This requires: +1. Xcode with iOS SDK installed +2. Cross-compilation from macOS (or complex cross-toolchain setup) + +#### Option B: Create a library instead of executable +Instead of building a standalone binary, build ORLY as a Go library that can be called from Swift/Objective-C: + +```bash +CGO_ENABLED=1 GOOS=ios GOARCH=arm64 go build -buildmode=c-archive -o liborly.a +``` + +This creates a static library usable in iOS apps. + +#### Option C: Use gomobile +Use the `gomobile` tool which handles iOS cross-compilation: + +```bash +gomobile bind -target=ios ./pkg/... +``` + +**Recommendation**: Option A or B depending on use case. For a relay server, iOS support may not be practical anyway (iOS backgrounding restrictions, network limitations). + +--- + +## Issue 3: Android Build Failure (RESOLVED) + +### Problem +``` +# github.com/ebitengine/purego +dlfcn_android.go:21:13: undefined: cgo.Dlopen +``` + +### Root Cause +Android uses the Linux kernel, so Go's `GOOS=android` still matches the `linux` build tag. This meant our `*_linux.go` files (which import purego) were being compiled for Android. + +### Solution (Implemented) + +Updated all build tags in `crypto/p8k/` to explicitly exclude Android: + +**Linux files** (`*_linux.go`): +```go +//go:build linux && !android && !purego +``` + +**Other platform files** (`*_other.go`): +```go +//go:build !linux || android || purego +``` + +This ensures Android uses the pure Go `p256k1.mleku.dev` implementation instead of trying to load libsecp256k1 via purego. + +### Verification +```bash +GOOS=android GOARCH=arm64 CGO_ENABLED=0 go build -o orly-android-arm64 +# Successfully produces 33MB ARM64 ELF binary +``` + +--- + +## Implementation Plan + +### Phase 1: js/wasm Support (Low effort) + +| Task | Repository | Effort | +|------|------------|--------| +| Create `restart_other.go` stub | ORLY | 5 min | +| Create `connection_options_js.go` stub OR exclude ws package | nostr | 15 min | +| Test js/wasm build compiles | Both | 5 min | + +**Note**: This enables *compilation* but not *functionality*. Running ORLY in a browser would require significant additional work (no filesystem, no listening sockets, etc.). + +### Phase 2: Android Support (Medium effort) + +| Task | Repository | Effort | +|------|------------|--------| +| Audit purego imports - ensure Linux-only | nostr | 30 min | +| Add build tags to any files importing purego | nostr | 15 min | +| Test android/arm64 build | Both | 5 min | + +### Phase 3: iOS Support (High effort, questionable value) + +| Task | Repository | Effort | +|------|------------|--------| +| Set up iOS cross-compilation environment | - | 2-4 hours | +| Modify build scripts for CGO_ENABLED=1 | ORLY | 30 min | +| Create c-archive or gomobile bindings | ORLY | 2-4 hours | +| Test on iOS simulator/device | - | 1-2 hours | + +**Recommendation**: iOS support should be deprioritized unless there's a specific use case. A Nostr relay is a server, and iOS imposes severe restrictions on background network services. + +--- + +## Quick Wins (Do First) + +### 1. Create `restart_other.go` in ORLY + +```go +//go:build !linux && !darwin && !windows + +package interrupt + +import ( + "lol.mleku.dev/log" + "os" +) + +func Restart() { + log.W.Ln("restart not supported on this platform, exiting") + os.Exit(0) +} +``` + +### 2. Exclude ws package from js/wasm in nostr library + +Modify `connection.go` to have a build tag: +```go +//go:build !js + +package ws +// ... rest of file +``` + +Create `connection_js.go`: +```go +//go:build js + +package ws + +// Stub package for js/wasm - WebSocket client not supported +// Use browser's native WebSocket API instead +``` + +### 3. Audit purego usage + +Ensure all files that import `github.com/ebitengine/purego` have: +```go +//go:build linux && !purego +``` + +--- + +## Estimated Total Effort + +| Platform | Compilation | Full Functionality | +|----------|-------------|-------------------| +| js/wasm | 1 hour | Not practical (server) | +| android/arm64 | 1-2 hours | Possible with NDK | +| ios/arm64 | 4-8 hours | Limited (iOS restrictions) | + +--- + +--- + +## iOS with gomobile + +Since iOS requires CGO and you cannot use Xcode without an Apple ID, the `gomobile` approach is the best option. This creates a framework that can be integrated into iOS apps. + +### Prerequisites + +1. **Install gomobile**: + ```bash + go install golang.org/x/mobile/cmd/gomobile@latest + gomobile init + ``` + +2. **Create a bindable package**: + gomobile can only bind packages that export types and functions suitable for mobile. You'll need to create a simplified API layer. + +### Creating a Bindable API + +Create a new package (e.g., `pkg/mobile/`) with a simplified interface: + +```go +// pkg/mobile/relay.go +package mobile + +import ( + "context" + // ... minimal imports +) + +// Relay represents an embedded Nostr relay +type Relay struct { + // internal fields +} + +// NewRelay creates a new relay instance +func NewRelay(dataDir string, port int) (*Relay, error) { + // Initialize relay with mobile-friendly defaults +} + +// Start begins accepting connections +func (r *Relay) Start() error { + // Start the relay server +} + +// Stop gracefully shuts down the relay +func (r *Relay) Stop() error { + // Shutdown +} + +// GetPublicKey returns the relay's public key +func (r *Relay) GetPublicKey() string { + // Return npub +} +``` + +### Building the iOS Framework + +```bash +# Build iOS framework (requires macOS) +gomobile bind -target=ios -o ORLY.xcframework ./pkg/mobile + +# This produces ORLY.xcframework which can be added to Xcode projects +``` + +### Limitations of gomobile + +1. **Only certain types are bindable**: + - Basic types (int, float, string, bool, []byte) + - Structs with exported fields of bindable types + - Interfaces with methods using bindable types + - Error return values + +2. **No channels or goroutines in API**: + The public API must be synchronous or use callbacks + +3. **Callbacks require interfaces**: + ```go + // Define callback interface + type EventHandler interface { + OnEvent(eventJSON string) + } + + // Accept callback in API + func (r *Relay) SetEventHandler(h EventHandler) { + // Store and use callback + } + ``` + +### Alternative: Building a Static Library + +If you want more control, build as a C archive: + +```bash +# From macOS with Xcode command line tools +CGO_ENABLED=1 GOOS=ios GOARCH=arm64 \ + go build -buildmode=c-archive -o liborly.a ./pkg/mobile + +# This produces: +# - liborly.a (static library) +# - liborly.h (C header file) +``` + +This can be linked into any iOS project using the C header. + +### Recommended Next Steps for iOS + +1. Create `pkg/mobile/` with a minimal, mobile-friendly API +2. Test gomobile binding on Linux first: `gomobile bind -target=android ./pkg/mobile` +3. Once Android binding works, the iOS binding will use the same API +4. Find someone with macOS to run `gomobile bind -target=ios` + +--- + +## Appendix: File Changes Summary + +### nostr Repository (`git.mleku.dev/mleku/nostr`) - COMPLETED + +| File | Change | +|------|--------| +| `crypto/p8k/secp_linux.go` | Build tag: `linux && !android && !purego` | +| `crypto/p8k/schnorr_linux.go` | Build tag: `linux && !android && !purego` | +| `crypto/p8k/ecdh_linux.go` | Build tag: `linux && !android && !purego` | +| `crypto/p8k/recovery_linux.go` | Build tag: `linux && !android && !purego` | +| `crypto/p8k/utils_linux.go` | Build tag: `linux && !android && !purego` | +| `crypto/p8k/secp_other.go` | Build tag: `!linux \|\| android \|\| purego` | +| `crypto/p8k/schnorr_other.go` | Build tag: `!linux \|\| android \|\| purego` | +| `crypto/p8k/ecdh_other.go` | Build tag: `!linux \|\| android \|\| purego` | +| `crypto/p8k/recovery_other.go` | Build tag: `!linux \|\| android \|\| purego` | +| `crypto/p8k/utils_other.go` | Build tag: `!linux \|\| android \|\| purego` | +| `crypto/p8k/constants.go` | NEW - shared constants (no build tags) | + +### ORLY Repository (`next.orly.dev`) + +| File | Change | +|------|--------| +| `go.mod` | Added `replace` directive for local nostr library | + +### Future Work (js/wasm) + +| File | Action Needed | +|------|---------------| +| `pkg/utils/interrupt/restart_other.go` | CREATE - stub `Restart()` for unsupported platforms | +| `nostr/ws/connection.go` | MODIFY - add `//go:build !js` or exclude package | +| `nostr/ws/connection_js.go` | CREATE - stub for js/wasm | diff --git a/go.mod b/go.mod index 0c1d97c..91ce7dd 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module next.orly.dev go 1.25.3 require ( - git.mleku.dev/mleku/nostr v1.0.4 + git.mleku.dev/mleku/nostr v1.0.7 github.com/adrg/xdg v0.5.3 github.com/dgraph-io/badger/v4 v4.8.0 github.com/dgraph-io/dgo/v230 v230.0.1 @@ -82,6 +82,7 @@ require ( google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + p256k1.mleku.dev v1.0.3 // indirect ) retract v1.0.3 diff --git a/go.sum b/go.sum index fbac882..9b755e8 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -git.mleku.dev/mleku/nostr v1.0.4 h1:QKJlqUubLPeMpYpxHODSvfSlL+F6UhjBiBuze9FGRKo= -git.mleku.dev/mleku/nostr v1.0.4/go.mod h1:swI7bWLc7yU1jd7PLCCIrIcUR3Ug5O+GPvpub/w6eTY= +git.mleku.dev/mleku/nostr v1.0.7 h1:BXWsAAiGu56JXR4rIn0kaVOE+RtMmA9MPvAs8y/BjnI= +git.mleku.dev/mleku/nostr v1.0.7/go.mod h1:iYTlg2WKJXJ0kcsM6QBGOJ0UDiJidMgL/i64cHyPjZc= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= @@ -302,3 +302,5 @@ lol.mleku.dev v1.0.5/go.mod h1:JlsqP0CZDLKRyd85XGcy79+ydSRqmFkrPzYFMYxQ+zs= lukechampine.com/frand v1.5.1 h1:fg0eRtdmGFIxhP5zQJzM1lFDbD6CUfu/f+7WgAZd5/w= lukechampine.com/frand v1.5.1/go.mod h1:4VstaWc2plN4Mjr10chUD46RAVGWhpkZ5Nja8+Azp0Q= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +p256k1.mleku.dev v1.0.3 h1:2SBEH9XhNAotO1Ik8ejODjChTqc06Z/6ncQhrYkAdRA= +p256k1.mleku.dev v1.0.3/go.mod h1:cWkZlx6Tu7CTmIxonFbdjhdNfkY3VbjjY5TFEILiTnY= diff --git a/pkg/version/version b/pkg/version/version index 6a16700..9e5a5c6 100644 --- a/pkg/version/version +++ b/pkg/version/version @@ -1 +1 @@ -v0.31.4 \ No newline at end of file +v0.31.5