April 17, 2026
10 min read

Building ArjunaOS #4: Syncing the Source

ArjunaOS
AOSP
repo
LineageOS
Manifest
Android Build
PPrasad Manoj Parulkar
Prasad Manoj Parulkar

AOSP Engineer

This is the point where ArjunaOS stops being a prepared machine and becomes a real source tree. We fork the upstream LineageOS manifest repository, initialize the workspace from our own fork, let the first repo sync run, and verify that the checkout now contains the platform layers we talked about earlier.

In the last article, we got the host ready.

That article was intentionally narrow. We checked the machine, installed the packages, made sure Git was sane, and got the repo launcher working from a normal shell. Useful work, yes. But it still was not the Android tree. It was only the floor under it.

This article is where the tree actually starts to exist.

We are going to fork the manifest repository first, then create the workspace, initialize it from our own fork, and let the first repo sync begin. This is the moment where the clean mental map from Article 2 stops being abstract and starts turning into directories, metadata, and a lot of downloaded source code.

Figure 1: The ownership flow for this article. We start from upstream, take control with our own fork, initialize the workspace from that fork, and only then let the local tree take shape. This diagram uses generic placeholders. The concrete values from this series run appear below and in the screenshots.

Pick a workspace and stay consistent

Before the first command, decide where this checkout will live.

You can put it somewhere else if you want. There is nothing sacred about my path. But do not be casual about it either. Android trees get large fast, and they are annoying to move later if you already started building on top of them.

In your own setup, this should be whichever stable path you want to live with for the rest of the series. So write it down conceptually like this:

<path-to-your-workspace>

For this series run, that path was:

/home/prasad/Desktop/Work/arjunaos

That is why the screenshots in this article show that exact location.

Figure 2: The real workspace path used for this series run. Yours can be different, but pick one early and keep it stable.

If you have multiple disks, this is the time to make a smart decision. Put the tree on the faster drive with the healthier free space, not wherever your shell happened to be open.

Fork first, then initialize from your fork

Before repo init, there is one teaching choice I want to make very explicit.

For this series, we are not inventing a custom manifest repository from day one. We are first taking the upstream LineageOS/android manifest repository and forking it into our own GitHub namespace. That means the starting point is still the normal LineageOS manifest layout, just under a fork we control.

Why bother if we are not modifying it yet?

Because this is a series about building your own OS, not just compiling somebody else's tree once and walking away. Using your own fork from the start gives you a stable place to carry future manifest changes, local tracking, and ArjunaOS-specific repo decisions later without changing the basic workflow halfway through the series.

At this stage though, the important point is this: the fork is still being used in an upstream-equivalent way for manifest purposes. The repository already has ArjunaOS README and documentation edits, but we have not started changing the actual manifest structure or project definitions yet.

So the flow is:

  1. fork LineageOS/android on GitHub
  2. use your fork URL in repo init
  3. keep the first sync behavior identical to upstream as much as possible

That is exactly why the command below uses your fork URL, not upstream directly.

If your own fork still shows the original upstream README, that is fine. That is a very normal fork state. In my case, the fork already has ArjunaOS README edits, but the rule is the same either way: use your fork URL, not the upstream URL, when you initialize the workspace.

Now we run the first command that makes this feel real:

repo init -u https://github.com/<your-github-username-or-org>/android.git -b lineage-23.2 --git-lfs

For this series run, that URL was:

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

Let us break down what this is doing.

  • -u points at the manifest repository
  • -b lineage-23.2 selects the branch we are following for this series
  • --git-lfs tells repo to handle LFS-backed content properly from the beginning

For this series run, that manifest repository lived under AOSPInsider/android.git. The important bit is not the exact name. It is that we are initializing from our fork of upstream, not from LineageOS directly. The branch still tracks the LineageOS 23.2 world we talked about earlier, and while the repo already contains ArjunaOS README edits, we have not started changing the actual manifest layout yet.

This does not download the full Android source tree yet. That confusion trips people a lot. repo init is the manifest step. It teaches the workspace what it is supposed to become. The real bulk download comes next.

The first run also tells you one more useful thing: which Git identity repo sees. If that is wrong, fix it now, not after you start creating local commits.

Figure 3: AOSPInsider/android is the fork used for this series. It already contains ArjunaOS README changes, but the manifest is still being used in an upstream-equivalent way for the first sync flow.

Figure 4: After forking, copy the URL from your fork, not the upstream manifest remote. That is the URL repo init should use here, even if older docs or README snippets still show the LineageOS URL.

Figure 5: repo init does not build the full tree yet. It initializes the manifest-driven workspace from our forked manifest repository and confirms the branch and Git identity being used.

One small detail worth noticing in the output is that repo first downloads its own source from Google's Gerrit infrastructure. That is normal. You are still initializing the workspace against the manifest repository you gave it. repo itself just updates from its own upstream source.

Look at what changed after repo init

After repo init, the directory still looks almost empty from the outside. That can feel strange if you expected hundreds of source folders immediately.

Run:

ls -la

What you should see is the beginning of the hidden control structure:

  • . and .. like normal
  • a new .repo/ directory

That hidden directory is where the manifest metadata and repo management state live. In other words, the workspace now knows what to sync, even though most of the source projects are still missing.

Figure 6: This is the structural difference the terminal output is hinting at. repo init creates the control state. repo sync is what turns that into a populated source tree. This diagram uses a generic workspace placeholder; the real path for this series run is shown in the screenshots.

Figure 7: Right after repo init, the visible workspace is still small. The important change is the new .repo/ directory, which means the manifest state now exists locally.

This is a good place to stop for a second and remember Article 2. We talked there about many Git repositories pretending to be one tree. .repo/ is the control room for that illusion.

Start the first real sync

This is the heavy command:

repo sync -c --no-tags --no-clone-bundle -j$(nproc --all)

This is where the machine starts paying the bill for everything we prepared in Article 3.

The flags matter:

  • -c keeps the sync focused on the current branch
  • --no-tags skips downloading a pile of tag history we do not need right now
  • --no-clone-bundle avoids clone bundles that sometimes behave badly on unstable connections
  • -j$(nproc --all) scales parallel jobs to the machine instead of hardcoding a random number

That last one is practical, but not magical. More jobs are not always better if your disk or network becomes the bottleneck. Still, it is a sane place to start for a modern local machine.

Also, this is the step where people finally understand why the disk warning in Article 3 was not just me being dramatic.

Figure 8: The first repo sync is where the LineageOS tree starts taking real shape locally. It is normal for this to run a long time and chew through bandwidth and disk space.

If your sync is slow, that does not automatically mean something is broken. Android source is large. A first sync can take a while even on a decent connection. What matters is whether progress is moving and the workspace is gradually filling out the tree.

Do not start randomly killing repo sync jobs just because the numbers look scary. A slow first sync is normal. A repeatedly failing sync is different, and that is where we start troubleshooting.

Prove that the tree is real

Once the sync has done enough work, you want one quick sanity check that this is now a real assembled source tree and not just a manifest shell.

One easy way is:

repo list | grep -E 'build/make|frameworks/base|packages/apps/Settings|device/generic/goldfish'

I like this check because it proves different layers at once:

  • build/make for the build system side
  • frameworks/base for the platform core
  • packages/apps/Settings for a major system app
  • device/generic/goldfish for emulator-oriented device support

That gives you a much better gut check than only staring at a progress counter.

Figure 9: A quick sanity check after sync. This is enough to show that the workspace now contains real projects across build, framework, app, and emulator device layers.

This is also the point where Article 2 and Article 4 connect very nicely. In Article 2 we said the Android tree is many repositories arranged into one working structure. Here you can finally see pieces of that structure by name, on your own machine.

What success looks like here

By the end of this article, you should have all of this:

  • a chosen workspace path
  • a fork of the upstream manifest repository under your own account
  • a successful repo init
  • a visible .repo/ directory
  • a running or completed first repo sync
  • proof that real LineageOS projects now exist in the assembled tree

That is enough for Article 4.

Notice what we still have not done yet:

  • no lunch
  • no envsetup.sh
  • no actual build output
  • no booted emulator
  • no ArjunaOS customization yet

That is also on purpose.

This article should own workspace creation and the first sync. If we also cram the first build into the same post, the main point gets blurred. The reader needs one clean moment where the tree is now local and real before we ask the machine to compile it.

Coming up next

In the next article, we use this synced tree for the first real build.

That means sourcing the build environment, choosing an emulator target, and seeing whether the checkout we just spent all this time downloading can actually turn into something bootable.

That is the point where ArjunaOS stops being a source tree on disk and starts becoming a system image.

Article 4 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 Progress13%

Comments (0)

Sign in to join the conversation

Loading comments...