An extensible, on-device IDE framework for Android/Java development. The framework models projects abstractly enough to support multiple build systems and multiple language backends behind stable interfaces, owns its own project structure, and runs entirely on device — where a full Gradle runtime is impractical.
Modules have one-directional dependencies. Platform modules expose stable API plus minimal implementation; domain-specific behavior ships as plugins against published extension points — including the bundled Android support and build systems.
UI layer (editors, project view, build console)
▲
Plugins: android-support, language backends, build systems
▲ (implement extension points)
Platform SPI: project-model, build-api, language-api, deps-api,
vfs-api, extension framework, analysis-api
▲
Platform core: extension registry, VFS, model store,
task scheduler, read/write lock, message bus
See modules.md for the concrete module map and responsibilities.
The project model is the spine of the framework: the build system, language backends, indexing, and navigation all read from it. It is abstract, extensible via facets, observable through a message bus, and mutated only through transactions.
Workspace the set of open projects; orchestrates linked builds
└── Project a buildable unit bound to ONE build system
├── buildSystem: BuildSystemId (native | gradle-compat | …)
├── Module unit of compilation (a "subproject")
│ ├── type: ModuleType (android-app | android-lib | java-lib | java-cli | …)
│ ├── Facet domain config (AndroidFacet, JavaFacet, …)
│ ├── SourceSet (main, test, debug, release, …)
│ │ └── ContentRoot (dir + roles: source/resource/android-res/aidl/assets/…)
│ ├── dependencies: OrderEntry (module | library | sdk)
│ └── languageLevel, outputs, …
└── variants, settings
├── LibraryTable (workspace-scoped, interned shared libraries)
└── SdkTable (installed Android/Java SDKs)
module-info.java module; JPMS support,
if added, is a JavaFacet concern.).so), generated, excluded). Roles, rather than fixed fields, keep it extensible.AndroidFacet holds the manifest path, SDK levels, build types, and flavors; JavaFacet holds the
language level and annotation processors.ModuleType is itself an extension point, so new module types (for example, a Kotlin library) are
registered by a plugin rather than added to the core.
Dependencies are an ordered list of order entries per module, each carrying a DependencyScope that
mirrors Gradle/Maven (API, IMPLEMENTATION, COMPILE_ONLY, RUNTIME_ONLY, TEST_IMPLEMENTATION).
An exported flag reproduces Gradle’s api vs implementation distinction: an exported entry is
visible to downstream modules’ compile classpaths; a non-exported one is not.
Module.classpath(scope, transitive) walks the order entries and produces a deduplicated, ordered,
content-hashed ClasspathSnapshot:
COMPILE_ONLY SDK dependency (such as android.jar) is placed on the compile classpath and
filtered out of the packaged/runtime classpath.Because classpath correctness lives in the model, build systems and language backends simply consume a
ClasspathSnapshot — they never re-derive dependency semantics. The snapshot’s content hash is a
build-cache key and a language-backend cache key, so a classpath change invalidates both compilation
and editor analysis.
Readers see an immutable, consistent snapshot. Structural changes go through a modifiable model: stage edits, then commit atomically under the write lock.
val tx = project.beginModification() // requires write lock
val mod = tx.addModule("feature-login", AndroidLibModuleType)
mod.addDependency(ModuleDependency("core", scope = API, exported = true))
tx.commit() // atomic; publishes a new snapshot + events
On commit, the store swaps in a new snapshot and publishes typed events (ModuleAdded,
DependenciesChanged, …) on the message bus. A build that started before the commit keeps using its
captured snapshot until it finishes.
In native mode the IDE owns the project structure, so it persists a declarative manifest per module
(module.toml) plus workspace-level tables. The format is human-diffable, fast to parse, and not an
executable script — there is no script interpreter on the sync path. Derived state (build cache,
indices, resolved dependencies) lives under a disposable caches/ directory. Writes go to a temp file
and are atomically renamed, so a model file is never left half-written.
Loading is fault-isolating rather than all-or-nothing, because the files can be damaged from outside the
IDE: on-device projects live in browsable external app storage, so a module directory can be deleted or
moved while workspace.json still lists it. A module whose module.toml is missing or unparsable is left
out of the model and reported, and so is an unreadable project entry; the derived workspace tables
(libraries.json, sdks.json) fall back to empty and are rebuilt. Nothing damaged is rewritten, so a file
that failed to parse stays on disk exactly as it is and can be repaired by hand. Only a missing or
unparsable workspace.json, or a schema version from a newer build, fails the open outright.
In gradle-compat mode the truth lives in the Gradle files; the framework holds only a derived model snapshot and caches.
Two dependency graphs are kept distinct so that linked projects with their own build systems can be built in parallel or in sequence.
A → B means A depends on B’s
published outputs. Each node carries its own build-system id, so the graph can mix a native project
and a gradle-compat project. Edges record dependency substitution (an external coordinate resolves
to a locally built sibling project).A workspace build request is planned in nested layers: resolve the target to the set of projects
needed, order the project graph into levels, build each level’s projects (each delegating to its own
build system), and within a project run the task graph over the module graph. Parallelism happens at
both levels, bounded by device-aware limits. A CoordinationPolicy
(PARALLEL_BY_LEVEL / STRICTLY_SEQUENTIAL / MANUAL_ORDER) selects the strategy.
The design separates three concerns that a typical build tool bundles together:
BuildSystem SPI: a stable contract the rest of the IDE talks to (supports,
createBuildGraph, tasks, runTasks/actionFor). Reading a project model out of a foreign build
system’s files is a separate SPI (ProjectImporter), and build logic can be contributed to any graph
through BuildPlugin.The task engine is the durable idea: a build is a DAG of tasks with typed inputs/outputs, up-to-date checking by fingerprint, and a persistent cache. Editing one file re-runs only the affected subgraph. See build-system.md for the engine and the native pipelines.
A single process edits files, parses them, mutates the model, and builds — concurrently — so a threading discipline keeps the model and ASTs consistent:
The ordering rule that ties it together: mutate under the write lock, publish an event, and let consumers invalidate or recompute under a read action on a background activity.
Everything domain-specific plugs in through a minimal extension-point registry. Module types, build systems, language backends, analyzers, quick-fixes, file icons, project templates, and block mappings are all contributed this way. See extension-points.md.