Dala Dev Architecture Guide

Copy Markdown View Source

Complete technical reference for the dala_dev architecture, explaining how each layer works and interacts with other components.

Table of Contents


Introduction

dala_dev is the build/deploy/devices toolkit for the Dala ecosystem — the BEAM-on-device mobile framework for Elixir.

Three-Repo Topology

As described in the system-wide AGENTS.md, the dala ecosystem uses three repositories:

RepositoryPurpose
dalaCore framework: Elixir/OTP runtime for mobile, LiveView integration, NIF bridges
dala_devDevelopment tooling: Mix tasks, device discovery, deployment, emulators, provisioning
dala_newProject generator: Hex archive, templates for new Dala projects

dala_dev works alongside the core framework and project generator, providing all the local development workflows.


System Context

Integration Points


                    Developer Machine                        
                                                             
            
    dala (app)  dala_dev     dala_new      
    Elixir src       (this repo)      (generator)   
            
                                                           
                                                           
    
                Platform Tools                           
    Android: adb, Gradle, Android SDK                  
    iOS: xcrun, xcodebuild, libimobiledevice          
    

                            
                            
  
 Android Devices    iOS Devices     
 (physical/emul)    (physical/sim)  
  

Key Responsibilities

  • Device Discovery: Find connected Android/iOS devices and emulators
  • Tunnel Management: Enable Erlang distribution between dev machine and devices
  • Deployment: Push BEAM files and native apps to devices
  • Build Support: Manage native builds and OTP runtimes
  • Development Dashboard: Web UI for device management and logs
  • Provisioning: iOS certificate and profile management
  • Benchmarking: Battery and performance testing utilities

Core Architectural Layers

Discovery Layer

Modules: DalaDev.Discovery.Android, DalaDev.Discovery.IOS, DalaDev.Device

File transfer: DalaDev.FileTransfer

Purpose

Discovers connected devices using platform-specific tools and returns normalized device structs.

Android Discovery (DalaDev.Discovery.Android)

Uses adb devices -l to list devices:

$ adb devices -l
List of devices attached
emulator-5554          device product:sdk_gphone64_x86_64 model:Android_SDK_built_for_x86_64 device:emulator64_x86_64
R3CR50LJN1W            device usb:1-2 product:SM-G998B model:SM-G998B device:SM-G998B

Parsing: parse_devices_output/1 converts this to DalaDev.Device structs with fields:

  • platform: :android
  • serial: ADB serial (e.g., emulator-5554, R3CR50LJN1W)
  • type: :emulator or :physical
  • name: Device model (e.g., Android_SDK_built_for_x86_64)
  • status: :connected, :unauthorized, :discovered

iOS Discovery (DalaDev.Discovery.IOS)

Uses multiple tools depending on device type:

Simulators (macOS only):

xcrun simctl list -j devices booted

Parsed by parse_simctl_json/1 and parse_simctl_text/1.

Physical Devices (requires libimobiledevice):

ideviceinfo -k UniqueDeviceID
ideviceinfo -k DeviceName

Returns DalaDev.Device structs with:

  • platform: :ios
  • serial: UDID (e.g., abc123def456...)
  • type: :simulator or :physical
  • host_ip: Device IP address (if available)

Unified Device Struct

%DalaDev.Device{
  platform: :android | :ios,
  type: :physical | :simulator | :emulator,
  serial: String.t(),
  name: String.t() | nil,
  version: String.t() | nil,
  status: :connected | :booted | :discovered | :unauthorized | :error,
  host_ip: String.t() | nil,
  node: atom() | nil,
  # ... other fields
}

Public API Seams (for testing)


Tunnel Layer

Module: DalaDev.Tunnel

Purpose

Establishes network tunnels for Erlang distribution between the development machine and devices.

How Erlang Distribution Works

Erlang nodes communicate via TCP using:

  • EPMD (Erlang Port Mapper Daemon): Maps node names to ports (default port 4369)
  • Distribution port: Each node listens on a random port for inter-node communication

Android Tunneling

Uses adb forward and adb reverse:

# Device → Mac (EPMD)
adb reverse tcp:4369 tcp:4369

# Mac → Device (distribution port)
adb forward tcp:9100 tcp:9100

This allows:

  • Device EPMD to register with the Mac's EPMD
  • Mac to connect to device's distribution port

iOS Tunneling

Simulators: Share the Mac's network stack — no tunneling needed. The simulator uses the Mac's network interfaces directly.

Physical Devices:

  1. USB: iproxy (from libimobiledevice) forwards ports:

    iproxy 4369 4369 &  # EPMD
    iproxy 9100 9100 &  # Distribution
    
  2. WiFi: Device and Mac on same network — direct TCP connection using device IP

  3. Tailscale: Mesh VPN — works over any network including cellular

Connection Priority (iOS Physical)

The BEAM on the device auto-detects the best connection at startup:

PriorityConnectionWhen
1WiFi / LANSame network as Mac
1TailscaleAny network (install Tailscale on both)
2USB onlyCable plugged in, no WiFi
3NoneNo network available

Deployment Layer

Modules: DalaDev.Deployer, DalaDev.HotPush, DalaDev.Connector

Two Deployment Modes

1. Full Deploy (DalaDev.Deployer)

Complete pipeline: native app build → install → push BEAMs → restart.

DalaDev.Deployer.deploy_all(
  restart: true,
  platforms: [:android, :ios],
  device: "emulator-5554"
)

Steps:

  1. Build native app (if --native):
    • Android: ./gradlew assembleDebug
    • iOS: xcodebuild -scheme ... build
  2. Install native app:
    • Android: adb install -r app.apk
    • iOS: xcrun simctl install booted app.app
  3. Push BEAM files:
    • If distribution reachable: Hot-push via RPC
    • Else: File push (adb push / simctl spawn cp)
  4. Restart app (unless --no-restart)

2. Hot Push (DalaDev.HotPush)

Push only changed BEAM files without restarting the app:

# Equivalent to :rpc.call(node, :code, :load_binary, [Module, path, beam_binary])
DalaDev.HotPush.push_beams(node, changed_modules)

Uses Erlang's :code.load_binary/3 to load BEAMs into the running VM.

Key Function: narrow_platforms_for_device/2

Module: DalaDev.NativeBuild.narrow_platforms_for_device/2

This is the single source of truth for:

  • Determining which platforms to build for
  • Validating deployment targets
# Example: User passes --device emulator-5554
platforms = DalaDev.NativeBuild.narrow_platforms_for_device([:android, :ios], "emulator-5554")
# Returns [:android] — iOS is filtered out

Why it matters: Without this function, you'd get spurious "No device matched" warnings and might build for the wrong platform.

Fallback Logic

If Erlang distribution isn't reachable (app not running, network issue):

  1. Try file push:
    • Android: adb push beam_files /data/data/<pkg>/files/lib/
    • iOS: xcrun simctl spawn <udid> cp beam_files <app_bundle>/
  2. Restart app to load new BEAMs

Build Layer

Modules: DalaDev.NativeBuild, DalaDev.OtpDownloader

Native Builds

Android (using Gradle):

DalaDev.NativeBuild.build_android(platform: :android, device: nil)
# Runs: ./gradlew assembleDebug
# Output: android/app/build/outputs/apk/debug/app-debug.apk

iOS (using xcodebuild):

DalaDev.NativeBuild.build_ios(platform: :ios, device: nil)
# Runs: xcodebuild -scheme <app> -destination 'platform=iOS Simulator,...' build
# Output: ios/build/Products/Debug-iphonesimulator/<app>.app

OTP Management (DalaDev.OtpDownloader)

Downloads and caches pre-built OTP tarballs for mobile platforms:

PlatformABICache Location
Androidarm64~/.dala/cache/otp_android_arm64.tar.gz
Androidarm32~/.dala/cache/otp_android_arm32.tar.gz
iOSsimulator (x86_64)~/.dala/cache/otp_ios_sim_x86_64.tar.gz
iOSsimulator (arm64)~/.dala/cache/otp_ios_sim_arm64.tar.gz
iOSdevice (arm64)~/.dala/cache/otp_ios_device_arm64.tar.gz

Schema Versioning: valid_otp_dir?/2 checks the schema version (not OTP hash) to invalidate caches when tarball structure changes.

# Bump schema version in valid_otp_dir?/2 to invalidate all caches
def valid_otp_dir?(dir, schema_version) do
  # schema_version is the knob for cache invalidation
end

Cross-Compilation Scripts (scripts/release/)

For building OTP from source:

  • xcompile_android.sh — Android arm64/arm32 using NDK toolchain
  • xcompile_ios_sim.sh — iOS simulator (x86_64/arm64)
  • xcompile_ios_device.sh — iOS device (arm64)

Patches (scripts/release/patches/):

  • forker_start — Skip forker_start (fork issues on iOS)
  • epmd_no_daemon — EPMD NO_DAEMON guard (prevents daemonization)

Dashboard Layer

Modules: DalaDev.Server, DalaDev.Server.Endpoint, DalaDev.Server.DevicePoller

Architecture


              Dala Dev Server (localhost:4040)           
                                                          
   
            Phoenix + Bandit (no Cowboy)               
    - Bandit.PhoenixAdapter                           
    - LiveView for real-time UI                       
   
                                                         
    
                                                      
        
              Phoenix.PubSub                         
      (live updates via WebSocket)                   
        
                                                      
          
                                                  
      DevicePoller (polls adb/xcrun)              
      LogStreamerSupervisor (logcat/simctl)       
      WatchWorker (file watcher)                   
          
    

Key Components

DalaDev.Server.Endpoint:

  • Bandit HTTP server on port 4040 (configurable)
  • Serves LiveView pages and static assets
  • Uses Bandit.PhoenixAdapter instead of Cowboy

DalaDev.Server.DevicePoller:

  • Periodically polls adb devices -l and xcrun simctl list -j
  • Publishes device updates via Phoenix.PubSub

DalaDev.Server.LogStreamerSupervisor:

  • Streams logs from devices:
    • Android: adb logcat
    • iOS: xcrun simctl spawn <udid> log stream
  • Buffers logs in DalaDev.Server.LogBuffer

DalaDev.Server.WatchWorker:

  • Watches Elixir source files for changes
  • Auto-deploys changed BEAMs via DalaDev.HotPush

Watch Mode

When you run mix dala.watch or enable watch in the dashboard:

  1. FileSystem (Elixir library) monitors lib/ for changes
  2. On change, recompiles the specific module
  3. Pushes BEAM to all connected devices via hot-push
  4. No app restart needed

Other Key Modules

DalaDev.Emulators

Manages Android AVDs and iOS simulators:

# List all
DalaDev.Emulators.list_android()  # Returns {:ok, [%Emulators{}]}
DalaDev.Emulators.list_ios()

# Start/stop
DalaDev.Emulators.start_android("Pixel_8_API_34")
DalaDev.Emulators.stop_ios("abc123...")

DalaDev.Provision

Handles iOS provisioning via xcodebuild:

# Development profile
Mix.Tasks.Dala.Provision.run([])

# Distribution profile
Mix.Tasks.Dala.Provision.run(["--distribution"])

Uses xcodebuild -allowProvisioningUpdates build to:

  1. Register bundle ID with Apple
  2. Create provisioning profile
  3. Download to ~/Library/Developer/Xcode/.../Provisioning Profiles/

DalaDev.CrashDump

Parses Erlang crash dumps from devices:

DalaDev.CrashDump.parse_file("erl_crash.dump")
# Returns structured data: %{header: ..., nodes: ..., ...}

DalaDev.CrashDump.html_report(crash_dump)
# Generates HTML report for debugging

DalaDev.Bench.*

Battery benchmarking modules:


Data Flow Examples

mix dala.deploy Flow

User runs: mix dala.deploy --native --device emulator-5554
          
          

 Parse args (OptionParser)   
 --native, --device, etc.    

          
          

 Resolve platforms:                          
 DalaDev.NativeBuild.narrow_platforms_for_device/2 
 Input: [:android, :ios], "emulator-5554"   
 Output: [:android] (iOS filtered out)       

          
          

 Mix.Task.run("compile")     
 Compile Elixir sources     

          
          

 If --native:                               
 DalaDev.NativeBuild.build_all/1             
    Android: ./gradlew assembleDebug      
    iOS: xcodebuild ... build             

          
          

 DalaDev.Deployer.deploy_all/1               
    Discover devices (Discovery Layer)    
    Set up tunnels (Tunnel Layer)          
    Push BEAMs:                           
        If dist reachable: HotPush (RPC) 
        Else: File push (adb/simctl)     
    Restart app (unless --no-restart)      

          
          

 Print results to user       
 (deployed, failed counts)  

mix dala.connect Flow

User runs: mix dala.connect
          
          

 Mix.Task.run("app.config")  
 Load project configuration  

          
          

 DalaDev.Connector.connect_all/1             
    Discover devices (Discovery Layer)    
    Set up tunnels (Tunnel Layer)          
    Restart apps on devices                
    Wait for Erlang nodes to come online   

          
          

 Start IEx session:                         
    Node.start(:"dala_dev@127.0.0.1")    
    Node.set_cookie(:dala_secret)          
    Node.connect(device_node) for each     

          
          

 User interacts with IEx:    
    Node.list()            
    nl(MyModule)           
    Dala.Test.* calls      

mix dala.battery_bench_android Flow

User runs: mix dala.battery_bench_android --duration 3600 --device 192.168.1.42:5555
          
          

 Preflight checks (DalaDev.Bench.Preflight): 
    adb connected?                        
    App installed?                        
    BEAM running?                         
    RPC reachable?                        

          
          

 If --native or no APK:                     
    Build benchmark APK                   
    Install via adb install               

          
          

 Start benchmark:                           
    DalaDev.Bench.Probe.start/1           
        Poll battery stats every 10 sec  
    DalaDev.Bench.Logger.start/1           
        Write CSV to _build/bench/       
    DalaDev.Bench.DeviceObserver.start/1  
         Monitor app state changes         

          
          

 Run for --duration seconds (3600 = 1 hour) 
    Every 10 sec: probe battery, log CSV  

          
          

 Post-run:                                  
    DalaDev.Bench.Summary.analyze/1       
        Calculate mAh, averages, etc.    
    Print summary to user                  

Key Design Decisions

Test-Driven Development

As mandated in AGENTS.md:

  • Write tests first: Before implementing a new function or fixing a bug
  • Every function needs tests: Public API, parsing logic, platform-specific code
  • Keep the suite green: All tests must pass before merging
  • Integration tests: Tagged with @tag :integration, require devices, excluded by default

Example test for DalaDev.Discovery.Android.parse_devices_output/1:

test "parses emulator device" do
  output = """
  List of devices attached
  emulator-5554          device product:sdk_gphone64_x86_64 model:Android_SDK_built_for_x86_64 device:emulator64_x86_64
  """

  devices = DalaDev.Discovery.Android.parse_devices_output(output)
  assert [%DalaDev.Device{serial: "emulator-5554", type: :emulator}] = devices
end

Public API Seams

Intentional public functions for testing (DO NOT make private):

These allow testing parsing and logic in isolation without real devices.

Error Handling

Module: DalaDev.Error

Standardized error formatting:

DalaDev.Error.format(:adb_not_found)
# Returns: "adb not found — install Android platform-tools"

DalaDev.Error.format(:xcodebuild_failed, output: "CODE_SIGNING_REQUIRED")
# Returns: "xcodebuild failed: CODE_SIGNING_REQUIRED\nHint: Run mix dala.provision"

iOS xcodebuild Diagnostics (DalaDev.Provision.diagnose_xcodebuild_failure/1):

Pattern matches Apple's cryptic errors into actionable hints:

DalaDev.Provision.diagnose_xcodebuild_failure("CODE_SIGNING_REQUIRED")
# Returns:
# %{
#   original: "CODE_SIGNING_REQUIRED",
#   hint: "No provisioning profile found. Run mix dala.provision first.",
#   fix: "mix dala.provision"
# }

Configuration

Module: DalaDev.Config

Reads from dala.exs in the project root:

# dala.exs
config :dala_dev,
  dala_dir: "/path/to/dala",
  bundle_id: "com.example.myapp",
  beam_flags: "-S 1:1 -sbwt none",
  liveview_port: 4000

Also supports environment variables:

  • DALA_CACHE_DIR — Override OTP cache location (default: ~/.dala/cache/)

Cross-Cutting Concerns

OTP Management

Caching Strategy:

  • Pre-built tarballs cached in ~/.dala/cache/
  • Reused across all Dala projects on the machine
  • Schema versioning (not OTP hash) for cache invalidation

Schema Version Bump (when tarball structure changes):

# In DalaDev.OtpDownloader.valid_otp_dir?/2
def valid_otp_dir?(dir, schema_version) do
  # Bump this when you change tarball structure:
  current_schema = 3

  if schema_version != current_schema do
    false  # Cache invalid, re-download
  else
    # Check tarball contents...
  end
end

Device ID Resolution

Problem: Multiple identifiers for the same device (serial, UDID, display ID).

Solution: DalaDev.Device provides consistent accessors:

%DalaDev.Device{serial: "emulator-5554", platform: :android}
|> DalaDev.Device.display_id()
# Returns: "emulator-5554"

%DalaDev.Device{serial: "abc123...", platform: :ios}
|> DalaDev.Device.display_id()
# Returns: "abc123de" (first 8 hex chars, dashes removed)

Matching: DalaDev.Device.match_id?/2 handles all ID formats:

device = %DalaDev.Device{serial: "emulator-5554", ...}
DalaDev.Device.match_id?(device, "emulator-5554")  # true
DalaDev.Device.match_id?(device, "5554")           # true (partial match)

Platform-Specific Logic

macOS-only features:

defp macos? do
  match?({:unix, :darwin}, :os.type())
end

if macos?() do
  # iOS provisioning, simctl, etc.
else
  # Skip iOS features, show hint
end

Graceful degradation:

  • If adb not found: Skip Android features, print hint
  • If xcrun not found: Skip iOS features, print hint
  • If libimobiledevice not installed: Skip physical iOS device features

Gotchas and Operational Considerations

(Detailed in AGENTS.md)

1. Compile-time Regex Literals

Problem: Regex literals in module attributes compile at compile time, causing issues with OTP 28.0+.

Solution: Use runtime compilation:

# ❌ DON'T
@pattern ~r/foo.*bar/

# ✅ DO
@pattern Regex.compile!("foo.*bar", "")

2. Device ID Resolution

Always use DalaDev.NativeBuild.narrow_platforms_for_device/2 when resolving device IDs. Bypassing it causes:

  • Spurious "No device matched" warnings
  • Building for wrong platform

3. Xcodebuild Error Diagnostics

Update DalaDev.Provision.diagnose_xcodebuild_failure/1 when encountering new Apple error strings.

4. OTP Tarball Schema Versioning

  • DO NOT bump OTP hash for cache invalidation
  • DO bump schema version in valid_otp_dir?/2

5. Release Script Assumptions

scripts/release/ scripts assume ~/code/otp exists with cross-compile output.


References