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.
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) │
└──────────────────────────────────────────────────────────────────┘
Every file in cmd/ defines one Cobra command. Each command:
- Parses flags and arguments
- Loads config via
pkg/config - Calls into
internal/orpkg/packages for business logic - Outputs results via
pkg/ui
Commands self-register in their init() functions via rootCmd.AddCommand().
Responsible for finding and installing developer tools:
CheckADB()— SearchesPATH, common install directories, and the config file foradb/adb.exe. Returns the path and version.CheckJava()— SearchesJAVA_HOME,PATH, and known JDK install directories. Returns the path and version.CheckAndroidSDK()— SearchesANDROID_HOME,ANDROID_SDK_ROOT, and common paths.DownloadPlatformTools()— Downloads the officialplatform-toolszip from Google and extracts it to~/.fas/.FindSDKPath()— Heuristic search for the SDK root directory.
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 viasdkmanagerandavdmanageron Windowswaydroid/— Manages Waydroid containers on Linuxdockeremu/— Managesbudtmo/docker-androidcontainers 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.
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
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
A Go wrapper around the adb CLI. Key methods:
ListDevices()— Parsesadb devices -loutput into typedDevicestructsInstallAPK()— Runsadb install -rLaunchApp()— Launches viaadb shell monkey(avoids needing to know the activity name)Exec()/ExecOnDevice()— General-purpose command execution
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"FindProjectRoot()— Walks up from the current directory looking forsettings.gradleorsettings.gradle.ktsDetectWrapper()— Checks forgradlew/gradlew.batDetectAppModules()— Scanssettings.gradleto find module names, then checks each forbuild.gradlewithcom.android.applicationpluginExtractApplicationID()— Parsesbuild.gradle/build.gradle.ktsto findapplicationIdRunTask()— Constructs anexec.Cmdfor running Gradle tasksFindAPK()— Locates the built APK inbuild/outputs/apk/
GetLocalIP()— Returns the machine's LAN IP addressNewFileServer()— Simple HTTP file server for APK sharing
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
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.
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.
The Makefile provides a build-all target that produces 6 binaries:
windows/amd64,windows/arm64darwin/amd64,darwin/arm64linux/amd64,linux/arm64
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.
- 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
Android Studio is 2+ GB and designed for interactive use. FAS is designed for automation and scriptability. They serve different audiences with different needs.
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.
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.