Skip to content

Latest commit

 

History

History
204 lines (148 loc) · 9.03 KB

File metadata and controls

204 lines (148 loc) · 9.03 KB

FAS Architecture

This document describes the internal architecture of FAS — how the binary is structured, how data flows through commands, and the design decisions behind the major subsystems.


High-Level Overview

FAS is a single Go binary built with Cobra for CLI parsing. It has no runtime dependencies — every tool it needs (Gradle wrapper, ADB paths, SDK paths) is either discovered automatically or embedded in the binary at compile time.

┌──────────────────────────────────────────────────────────────────┐
│                          User Terminal                           │
├──────────────────────────────────────────────────────────────────┤
│                       cmd/ (Cobra Commands)                      │
│  setup | doctor | run | watch | logs | wifi | cap | emulator ... │
├──────────┬───────────┬──────────────┬────────────────────────────┤
│ internal/│ internal/ │ internal/    │ internal/                  │
│ bootstrap│ cache     │ emulator/    │ tunnel/                    │
│          │           │ ├── avd/     │                            │
│          │           │ ├── waydroid/│                            │
│          │           │ └── dockeremu│                            │
├──────────┴───────────┴──────────────┴────────────────────────────┤
│                        pkg/ (Shared Libraries)                    │
│           adb/ | config/ | gradle/ | network/ | ui/              │
├──────────────────────────────────────────────────────────────────┤
│                     embedded/ (Gradle wrapper JAR + scripts)      │
└──────────────────────────────────────────────────────────────────┘

Package Responsibilities

cmd/ — CLI Layer

Every file in cmd/ defines one Cobra command. Each command:

  1. Parses flags and arguments
  2. Loads config via pkg/config
  3. Calls into internal/ or pkg/ packages for business logic
  4. Outputs results via pkg/ui

Commands self-register in their init() functions via rootCmd.AddCommand().

internal/bootstrap/ — Environment Detection

Responsible for finding and installing developer tools:

  • CheckADB() — Searches PATH, common install directories, and the config file for adb / adb.exe. Returns the path and version.
  • CheckJava() — Searches JAVA_HOME, PATH, and known JDK install directories. Returns the path and version.
  • CheckAndroidSDK() — Searches ANDROID_HOME, ANDROID_SDK_ROOT, and common paths.
  • DownloadPlatformTools() — Downloads the official platform-tools zip from Google and extracts it to ~/.fas/.
  • FindSDKPath() — Heuristic search for the SDK root directory.

internal/emulator/ — Emulator Orchestration

Defines the EmulatorManager interface that all backends implement:

type EmulatorManager interface {
    Install(opts InstallOptions) error
    Start(opts StartOptions) error
    Stop() error
    Wipe() error
    Uninstall() error
    Status() (EmulatorStatus, error)
    ADBSerial() (string, error)
    CheckPrerequisites() []PrereqResult
}

Three implementations exist:

  • avd/ — Manages Android Virtual Devices via sdkmanager and avdmanager on Windows
  • waydroid/ — Manages Waydroid containers on Linux
  • dockeremu/ — Manages budtmo/docker-android containers via the Docker CLI

Backend selection happens in cmd/emulator.go via the getManager() function, which checks the --backend flag and falls back to OS detection.

internal/cache/ — Build Cache Server

Implements the Gradle Remote Build Cache HTTP protocol:

  • GET /cache/<key> — Returns cached artifact (cache hit)
  • PUT /cache/<key> — Stores a new artifact
  • Tracks hit/miss statistics
  • Generates Gradle init scripts that point builds at the cache

internal/tunnel/ — Device Tunneling

A TCP proxy that forwards ADB protocol traffic over the network:

  • Host mode: Listens on a port and proxies connections to the local ADB server
  • Client mode: Connects to a remote host and makes the remote device appear local
  • Supports CIDR-based access control

pkg/adb/ — ADB Client

A Go wrapper around the adb CLI. Key methods:

  • ListDevices() — Parses adb devices -l output into typed Device structs
  • InstallAPK() — Runs adb install -r
  • LaunchApp() — Launches via adb shell monkey (avoids needing to know the activity name)
  • Exec() / ExecOnDevice() — General-purpose command execution

pkg/config/ — Configuration

Two config scopes:

Global config (~/.fas/config.yaml):

adb_path: "/path/to/adb"
sdk_path: "/path/to/sdk"
java_home: "/path/to/jdk"
auto_launch: true
log_level: info
cache:
  dir: "/path/to/cache"

Project config (.fas.yaml in project root — optional):

package_id: "com.example.app"
build_variant: "debug"
module: "app"

pkg/gradle/ — Gradle Integration

  • FindProjectRoot() — Walks up from the current directory looking for settings.gradle or settings.gradle.kts
  • DetectWrapper() — Checks for gradlew / gradlew.bat
  • DetectAppModules() — Scans settings.gradle to find module names, then checks each for build.gradle with com.android.application plugin
  • ExtractApplicationID() — Parses build.gradle / build.gradle.kts to find applicationId
  • RunTask() — Constructs an exec.Cmd for running Gradle tasks
  • FindAPK() — Locates the built APK in build/outputs/apk/

pkg/network/ — Network Utilities

  • GetLocalIP() — Returns the machine's LAN IP address
  • NewFileServer() — Simple HTTP file server for APK sharing

pkg/ui/ — Terminal UI

Built on Lipgloss for styled terminal output. Provides:

  • Color helpers: Cyan(), Green(), Red(), Yellow(), Dim(), Bold()
  • Status helpers: Info(), Success(), Fail(), Warn(), Pending()
  • Layout helpers: Banner(), SectionHeader(), KeyValue(), BoxStyle, SuccessBoxStyle, ErrorBoxStyle

embedded/ — Compiled-in Assets

The Gradle wrapper files (gradlew, gradlew.bat, gradle-wrapper.jar, gradle-wrapper.properties) are embedded into the binary using Go's embed package. This means fas setup can inject a working Gradle wrapper into any project without downloading anything.


Build System

Version Injection

The main.go file declares three variables:

var (
    version   = "dev"
    commit    = "unknown"
    buildTime = "unknown"
)

These are overridden at build time via -ldflags:

go build -ldflags "-s -w -X main.version=1.2.5 -X main.commit=8aaa1b3" -o fas .

The Makefile automates this by reading from git describe and git rev-parse.

Cross-Compilation

The Makefile provides a build-all target that produces 6 binaries:

  • windows/amd64, windows/arm64
  • darwin/amd64, darwin/arm64
  • linux/amd64, linux/arm64

Design Decisions

Why a single binary?

Developers should be able to download one file and start using it. No package managers, no pip install, no npm install -g. Just download and run.

Why Go?

  • Compiles to a static binary with zero runtime dependencies
  • Fast compilation (developers can build from source in seconds)
  • Excellent cross-compilation support (6 targets from one machine)
  • Native concurrency for file watching and network servers
  • Strong standard library for HTTP, OS operations, and process management

Why not wrap Android Studio?

Android Studio is 2+ GB and designed for interactive use. FAS is designed for automation and scriptability. They serve different audiences with different needs.

Why embed the Gradle wrapper?

Many Android projects in the wild have broken or missing Gradle wrappers. By embedding the wrapper files in the binary, FAS can fix this instantly without an internet connection.

Why monkey for app launching?

Launching an app via ADB normally requires knowing the main activity class name (e.g., com.example.app/.MainActivity). This requires parsing AndroidManifest.xml, which is complex and error-prone. Using adb shell monkey -p <package> -c android.intent.category.LAUNCHER 1 launches the default launcher activity without needing to know its name.