April 15, 2026
12 min read

Building ArjunaOS #2: Understanding LineageOS Architecture

Android Build System
ArjunaOS
LineageOS
AOSP
Repo
PPrasad Manoj Parulkar
Prasad Manoj Parulkar

AOSP Engineer

Before we download a huge source tree and start building ArjunaOS, we need a proper mental map of how LineageOS is organized. In this article we walk through repo, manifests, the source tree, device support layout, and the high-level path from lunch to a flashable build.

In the first article, Building ArjunaOS #1: Welcome to Your Custom Android ROM, I was mostly answering the big question: why even build ArjunaOS on top of LineageOS in the first place?

This one is different. Here I care less about motivation and more about shape. Before we install packages, before we sync a huge tree, before we start typing commands like obedient parrots, we need to know what kind of codebase we are walking into.

If you skip this part, the next articles can still work in a narrow way. You can copy commands. You can wait for builds. You can even boot something maybe. But then one night you will open frameworks/, packages/, device/, vendor/, .repo/, build/, and wonder why all of them exist and which one is responsible for the bug in front of you. I have been that person already. It is not fun.

So this article is the map before the road.

Why this comes before setup

When people say, "I built LineageOS", it sounds simple. One clone, one target, one build command, done.

Real Android platform work is not shaped like that at all.

LineageOS is a large multi-repository workspace. Platform code is split into many projects. Device support is split again. Kernel code may live somewhere else. Vendor files may live somewhere else again. The build logic has its own layout too. What looks like one source tree in your terminal is actually many repositories assembled into one working directory.

That mismatch is what confuses beginners so early. The code is hard, yes, but the first shock usually is not the code. It is the shape.

I still remember the first time I opened a proper Android source tree and tried to make sense of it. It did not feel like opening a project. It felt like walking into a warehouse and somebody saying, "yes, all the important things are here, good luck."

Figure 1: The broad relationship between the Android base, LineageOS, device support, and the ArjunaOS layer we will keep building through this series.

Small warning: If you come from Android Studio app work, drop the expectation that one repo means one project and one build file explains the world. Android platform development is more spread out than that. Once you accept this, things become less mysterious.

Why this tutorial starts from LineageOS, not raw AOSP

AOSP is the real base. Framework, services, core apps, build tools, libraries, all of that foundation matters.

But this series is not trying to study pure AOSP in an academic way. We are trying to build ArjunaOS as a real custom ROM tutorial. For that goal, raw AOSP alone is a poor place to begin. It is too bare, too thin on ROM-oriented patterns, and too eager to make you rebuild plumbing before you even touch customization.

What we need is a practical base where device support patterns already exist, build targets already make sense, and adding custom behavior is normal work instead of some weird side quest. That is where LineageOS helps.

Choosing LineageOS here is not laziness. It is just using the right layer so we spend our time on ArjunaOS work, not on rebuilding integration work that LineageOS already carries.

And honestly, many people say "just start from AOSP" very casually. They are not wrong in theory. They are just not the ones sitting beside you when some product config is broken and nothing boots.

LineageOS is not one repository

This is the first wrong mental model many people carry into ROM work.

They hear "LineageOS source" and imagine one giant Git repo. Nice idea. Easier life. Not real.

LineageOS is made from many Git repositories. Android has been organized like this for a long time. Different projects live separately, then get assembled into one working tree. That is why the repo tool exists.

repo sits on top of Git. It does not replace Git. Think of it more like a coordinator. It reads a manifest, understands which repositories belong in the workspace, and checks them out into the right paths.

So not this:

git clone <one-big-repository>

More like this:

repo init -u <manifest-url> -b <branch>
repo sync

The first command sets up manifest metadata and .repo/ state. The second one brings down the actual projects described by that manifest.

If you come from regular development, this feels weird at first. You expect one project, one repository, maybe a few submodules if life is already annoying. Android platform work is not built around that model. It is closer to a big workspace, and the manifest is the thing telling repo what belongs where.

Figure 2: repo init establishes the manifest state, repo sync fetches the listed Git projects, and the result becomes one local working tree.

What the upstream manifest actually looks like

I am still keeping this article conceptual. Article 3 and Article 4 own the real machine setup and first sync. Here we only need a grounded preview of the upstream pattern.

The official LineageOS manifest README shows an init line in this shape:

repo init -u https://github.com/LineageOS/android.git -b lineage-23.2 --git-lfs

Figure 3: The official LineageOS manifest README. This gives us the real repo init pattern before we later teach how ArjunaOS gets its own manifest entry point.

The exact branch will change later. Do not marry yourself to lineage-23.2. The important part is the pattern: manifest repo URL, branch, then repo sync.

I am deliberately keeping these screenshots on the upstream LineageOS side for now. We have not taught our own manifest flow yet. First understand the model. Later we start owning pieces of it.

What a manifest actually does

People in ROM circles say "manifest" all the time and beginners usually nod like they understand. Most of them do not. That is normal.

A manifest is basically the recipe for the source tree.

It tells repo which projects belong in the checkout, where each project should land, which remote should be used, and which revision should be followed by default. Without a manifest, repo does not know what a LineageOS tree even means.

The real file is large. In the current upstream branch, default.xml is more than 100 KB. You are not going to memorize that file. You do not need to. What matters is recognizing the shape.

A tiny simplified fragment, based on the real manifest, looks like this:

<manifest>
  <remote name="github" fetch=".." review="review.lineageos.org" />
  <remote name="aosp" fetch="https://android.googlesource.com" />
  <default remote="github" revision="refs/heads/lineage-23.2" />

  <project
      path="build/make"
      name="LineageOS/android_build" />

  <project
      path="packages/apps/Settings"
      name="LineageOS/android_packages_apps_Settings" />

  <project
      path="device/generic/goldfish"
      name="LineageOS/android_device_generic_goldfish" />
</manifest>

That is not the whole file, obviously. But it shows the important idea. One repo gets mapped to one path. Then another. Then another. Repeat that enough times and a whole source tree appears.

Figure 4: The official LineageOS default.xml. You can already see the real remotes, default revision, and the project-to-path mapping structure that repo uses to assemble the tree.

One detail I like from the real file is that build/make also uses linkfile entries so pieces such as build/envsetup.sh land where the build expects them. That is a good reminder that this tree is not just many repos dumped into one folder. It is a designed layout.

That matters later for ArjunaOS too. Once your ROM grows, you stop thinking only in terms of "which file should I edit?" You also start thinking, "where should this project live?" and "how do I keep my custom work clean when upstream changes arrive?"

Sloppy ROM work starts hurting exactly there.

A small map of the top-level tree

The next beginner mistake is trying to understand every top-level directory before touching anything. Please do not do that to yourself. It is not useful. It is just mentally expensive. What you need first is a rough map of the major zones we will keep returning to. Also, not every target exposes every directory in exactly the same way. A kernel tree may be in-tree for one target and absent for another. That part matters later, but it should not scare you now.

Figure 5: The simplified source tree we care about for ArjunaOS. Not everything is shown, and not every target will expose every directory in the same way, but these are the areas we will keep coming back to.

The practical meaning matters more than the formal one. frameworks/, packages/, and system/ are where the platform itself starts feeling real. A lot of behavior, services, and system apps live there.

Then you have the parts that shape how the ROM gets assembled for one target: build/, device/, and vendor/. kernel/ is the kernel side when it lives in-tree. .repo/ is only repo metadata, not platform source code in the normal sense.

The key thing to remember is separation of concerns. Framework behavior is not the same job as product config. Editing a system app is not the same job as a vendor overlay. Device-specific work is not the same as platform-wide customization.

When beginners mix all of that together, their tree becomes ugly very fast. Then they say Android source is messy. Sometimes that is true. Sometimes the developer is also helping the mess grow.

Where device support actually lives

For a physical phone, support rarely means one folder and one magic switch. Usually there are a few layers working together:

  • a device tree
  • a vendor tree or proprietary vendor files
  • often a kernel tree
  • sometimes common trees shared across similar hardware

The device tree usually carries product definitions, board config, inheritance, overlays, and other target-specific details. The vendor side carries proprietary pieces you are not rebuilding from source. The kernel side is the kernel source if you maintain it yourself. Common trees reduce duplication across related targets.

This series stays emulator-first on purpose. That keeps the focus on ArjunaOS structure, framework changes, system apps, and ROM organization instead of drowning early in bring-up pain. But I still want these words to sound normal to you now. Later, when someone says "this is a device tree problem" or "the common tree is inheriting the wrong thing", I do not want that to sound like secret cult language.

Also, this is why I keep saying ArjunaOS should not be taught as one phone-specific trick. The work we do should live above the worst of the device-specific maze whenever possible, so another developer can reuse the same ideas for a different target later.

That is the real value of emulator-first teaching here.

From manifest to build output

Later articles will keep using commands like these:

source build/envsetup.sh
lunch lineage_<target>-userdebug
m bacon

Do not memorize them yet. That is not the point of this article.

What matters now is the pipeline behind them. envsetup.sh loads shell helpers for Android builds. lunch selects the product and variant. After that, the build system resolves product configuration, device configuration, module dependencies, and target-specific behavior. Then Soong, Ninja, and the older make-based layers do the actual heavy work and produce images or packages.

At a high level, the flow looks like this:

  1. manifest defines the project set
  2. repo sync materializes the tree locally
  3. build helpers are loaded
  4. target is selected with lunch
  5. product and device config are resolved
  6. framework, apps, services, and target-specific code are compiled
  7. output artifacts are produced

Figure 6: The high-level path from manifest and sync through target selection, config resolution, compilation, and final output artifacts. We keep it simple here. Article 6 will go deeper into the build system itself.

There is a lot hidden inside those steps, yes. But even this simple map helps. When something breaks later, the better questions become: was it the manifest, the product config, one module build, packaging, or something device-specific?

Debugging does not suddenly become easy. It becomes less random.

Why any of this matters for ArjunaOS

Up to now, parts of this article may sound generic. That is fine. ArjunaOS needs the platform map before the customization map.

We do not want to build a ROM by editing random files across the tree and praying the next repo sync does not hurt us. We want platform layers that make sense, product choices that are deliberate, and ArjunaOS additions that stay organized so later Settings, SystemUI, framework, and system app work still have a clean home.

If the map is clear, the next article feels much less like guessing. You are not wandering into Ubuntu setup and repo tooling blind. You know what kind of workspace you are preparing the machine for.

Coming up next

In the next article, we get fully hands-on again.

We install the packages we need on Ubuntu, prepare the environment for Android platform work, set up the repo tool, and get the machine ready for the first real sync. That is where the build journey starts feeling physical instead of theoretical.

If the top-level tree feels a little less mysterious than when you started this article, then this article did something useful.

Article 2 of 30

Building ArjunaOS: Create Your Own Custom Android OS

Learn to build your own custom Android OS from scratch. This series walks you through creating ArjunaOS, a custom ROM based on LineageOS, starting from setting up your build environment and compiling your first build, through branding and system customization, to adding custom system services and advanced features. Covers the complete journey across three parts: building the OS, adding features, and deep customization.

Series Progress7%

Comments (0)

Sign in to join the conversation

Loading comments...