OverviewFilesCommitsIssuesDocsReleasesPeers

celltron-node commits / 903f379

Initial commit: celltron-node — native Android host APK for the pgh stack (step 1)
Foreground-service host that extracts a proot+glibc rootfs from APK assets and supervises the pgh/bare stack on no-root Android 7 (ASUS ZenFone AR / celltron), replacing the Termux + adb-scripts scaffolding. Step 1 (runtime harness) verified end-to-end on-device. Post-mortem: pear-git#5. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
903f3795c52a6cea427c15d5339d61a577dac345
by plankatron <fleeky@gmail.com> · 2026-07-22T02:04:24.000Z

Changed files (14) +589 -0

.gitignore +8 -0
……@@ -0,0 +1,8 @@
1build/
2.gradle/
3*.apk
4
5# Large runtime-host assets are staged at build time, never committed.
6# See README.md "Staging the runtime host".
7app/src/main/assets/proot
8app/src/main/assets/rootfs.tar
README.md +99 -0
……@@ -0,0 +1,99 @@
1# celltron-node
2
3A native Android host APK that runs the **pgh / bare P2P stack** on a locked,
4no-root Android-7 phone (the ZenFone AR / celltron device) — replacing the
5Termux + Termux:Boot + adb-pushed-scripts scaffolding with a single installable
6app whose foreground service anchors the whole proot tree.
7
8This is **step 1 of 5**: project skeleton + first-run extractor + proot launcher
9that boots `pgh-sidecar`. It does *not* yet include the GUI (WebView over pgh)
10or identity migration.
11
12## Why an APK instead of Termux
13
14The APK does **not** change the substrate — proot + glibc (Bionic wall), abstract
15sockets (SELinux pathname-UDS block), and copy-fallback (SELinux hardlink block)
16all remain. What it replaces is the *scaffolding*:
17
18| Termux setup | This APK |
19|---|---|
20| adb push + relocate + /sdcard bridge + run-as | one APK install; extractor writes in place |
21| tar-relocation breaks rocksdb CORESTORE markers | nothing is moved after creation — no breakage |
22| Termux:Boot debug APKs (matched signing) | own `BootReceiver` |
23| `supervise.sh` + no-setsid reaping trap | foreground service is the anchor (never setsid) |
24| adb needed once to write boot hooks | zero adb |
25| on-device `apt`/`dpkg-deb` for runtime libs | libs baked into `rootfs.tar` |
26
27The `net.celltron.mdns` app already proved this shell (foreground service +
28BootReceiver + wake) works on this exact phone.
29
30## Architecture (step 1)
31
32```
33PghNodeService (foreground, wakelock, START_STICKY) <- the anchor
34 └─ worker thread
35 ├─ Extractor.ensure() assets -> filesDir (idempotent, version-stamped)
36 └─ ProotLauncher.superviseForever()
37 └─ ProcessBuilder: proot -0 -r rootfs ... env -i ... bare pgh-sidecar
38 (child of the service — NOT detached, so it isn't reaped)
39```
40
41- `Extractor.kt` — copies `proot` + `tar` binaries (chmod 0755) and unpacks
42 `rootfs.tar` into `filesDir/rootfs`, once, guarded by `.staged-host.v1`.
43- `ProotLauncher.kt` — mirrors the proven `celltron.sh` invocation; sets
44 `PROOT_TMP_DIR` on proot's own env and `PGH_SIDECAR_SOCK=@pgh-node` (abstract
45 socket) inside the guest `env -i` set; restart-with-backoff supervisor.
46- `PghNodeService.kt` — foreground service + partial wakelock; owns the tree.
47- `BootReceiver.kt` / `MainActivity.kt` — boot autostart + a status screen.
48
49## Staging the runtime host (required before building)
50
51The APK ships three assets, none committed to git (too large / machine-built).
52Place them in `app/src/main/assets/`:
53
54| asset | what | source |
55|---|---|---|
56| `proot` | static aarch64 proot | `proot-v5.3.0-aarch64-static` (SourceForge proot.mirror) |
57| `tar` | static aarch64 tar or busybox | any static aarch64 busybox/tar (used only to unpack the rootfs) |
58| `rootfs.tar` | glibc Ubuntu 22.04 rootfs **with runtime libs + the pgh-sidecar bundle baked in** | see below |
59
60**Building `rootfs.tar`** (one-time, on the host): start from the celltron proot
61rootfs that already runs on the phone, add the runtime libs it needed
62(`libatomic.so.1`, extracted via `apt-get download libatomic1` + `dpkg-deb -x`),
63drop the arm64 sidecar bundle at `/root/pgh-sidecar-bootstrap-bare/`
64(`node scripts/build-bootstrap-bare.js --os linux --arch arm64` from pgh-sidecar,
65which vendors `bare-abstract-socket`), then
66`tar -C <rootfs> -cf rootfs.tar .`.
67
68The guest entrypoint `ProotLauncher.guestCmd` expects:
69`/root/pgh-sidecar-bootstrap-bare/runtime/bare bin/pgh-sidecar.bare.js`.
70
71> **Identity note:** this step-1 build stands up a *fresh* sidecar instance on
72> `@pgh-node` in the app's private sandbox — a throwaway bring-up node. It does
73> **not** touch the existing celltron `~/.hyperident` identity. Migrating the real
74> device identity + registries into the APK (with the CORESTORE-marker fix) is a
75> later step; do not point this at production identity yet.
76
77## Build & install
78
79```bash
80JAVA_HOME=/home/fleeky/android-tools/jdk-17.0.19+10 \
81 /home/fleeky/android-tools/gradle-8.7/bin/gradle assembleDebug --offline
82adb install -r app/build/outputs/apk/debug/app-debug.apk
83```
84
85(No gradle wrapper; `sdk.dir` is in `local.properties`. Same recipe as the mDNS
86app — its offline caches already hold AGP 8.5.2 + Kotlin 1.9.24.)
87
88## Verify (step-1 success criteria)
89
901. Launch the app → status screen shows extraction then sidecar log lines.
912. `adb logcat -s celltron-node` shows `pgh-sidecar listening: socket: @pgh-node`.
923. Reboot → `BootReceiver` restarts the service → sidecar comes back with no adb.
93
94## Next steps
95
962. Lifecycle hardening — `NetworkCallback` reconcile, reboot-to-parity test.
973. GUI (shell-out MVP) — WebView driving `pgh app` list/install/start/stop/log.
984. GUI (native) — Kotlin `LocalSocket` ABSTRACT hyper-svc client to `@pgh-node`.
995. Retire Termux; migrate the real device identity + stores.
app/build.gradle +37 -0
……@@ -0,0 +1,37 @@
1plugins {
2 id 'com.android.application'
3 id 'org.jetbrains.kotlin.android'
4}
5
6android {
7 namespace 'net.celltron.node'
8 compileSdk 34
9
10 defaultConfig {
11 applicationId 'net.celltron.node'
12 minSdk 24
13 targetSdk 30
14 versionCode 1
15 versionName '0.1'
16 }
17
18 compileOptions {
19 sourceCompatibility JavaVersion.VERSION_17
20 targetCompatibility JavaVersion.VERSION_17
21 }
22 kotlinOptions {
23 jvmTarget = '17'
24 }
25 buildTypes {
26 release {
27 minifyEnabled false
28 }
29 }
30 // Don't recompress the already-large runtime-host assets; keeps them
31 // mmap-friendly and avoids AAPT choking on a big rootfs tarball.
32 androidResources {
33 noCompress += ['tar', 'proot']
34 }
35}
36
37dependencies { }
app/src/main/AndroidManifest.xml +36 -0
……@@ -0,0 +1,36 @@
1<?xml version="1.0" encoding="utf-8"?>
2<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3
4 <uses-permission android:name="android.permission.INTERNET"/>
5 <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
6 <uses-permission android:name="android.permission.WAKE_LOCK"/>
7 <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
8 <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
9
10 <application
11 android:allowBackup="false"
12 android:label="celltron node">
13
14 <activity
15 android:name=".MainActivity"
16 android:exported="true">
17 <intent-filter>
18 <action android:name="android.intent.action.MAIN"/>
19 <category android:name="android.intent.category.LAUNCHER"/>
20 </intent-filter>
21 </activity>
22
23 <service
24 android:name=".PghNodeService"
25 android:exported="false"/>
26
27 <receiver
28 android:name=".BootReceiver"
29 android:exported="true">
30 <intent-filter>
31 <action android:name="android.intent.action.BOOT_COMPLETED"/>
32 </intent-filter>
33 </receiver>
34
35 </application>
36</manifest>
app/src/main/assets/.gitkeep +8 -0
……@@ -0,0 +1,8 @@
1Stage the runtime-host assets here before building (see ../../../../README.md):
2 proot static aarch64 proot binary
3 rootfs.tar glibc Ubuntu rootfs (minimal base for the smoke test; later the
4 full base with runtime libs + the pgh bundle baked in)
5
6Extraction uses the platform /system/bin/toybox, so no tar binary is bundled.
7The project compiles without these; PghNodeService fails at runtime if they are
8missing. They are .gitignored (too large + machine-built).
app/src/main/java/net/celltron/node/BootReceiver.kt +20 -0
……@@ -0,0 +1,20 @@
1package net.celltron.node
2
3import android.content.BroadcastReceiver
4import android.content.Context
5import android.content.Intent
6import android.os.Build
7
8/** Start the node service on device boot, so the pgh stack survives reboots. */
9class BootReceiver : BroadcastReceiver() {
10 override fun onReceive(context: Context, intent: Intent) {
11 if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
12 val i = Intent(context, PghNodeService::class.java)
13 if (Build.VERSION.SDK_INT >= 26) {
14 context.startForegroundService(i)
15 } else {
16 context.startService(i)
17 }
18 }
19 }
20}
app/src/main/java/net/celltron/node/Extractor.kt +109 -0
……@@ -0,0 +1,109 @@
1package net.celltron.node
2
3import android.content.Context
4import android.system.Os
5import android.util.Log
6import java.io.File
7
8/**
9 * First-run, idempotent extraction of the runtime host from APK assets into the
10 * app's private filesDir. On API 24 the app_data_file domain is exec-permitted,
11 * which is the whole premise of running proot no-root.
12 *
13 * Everything is written in place and never tar-moved afterwards — that is what
14 * sidesteps the device-file / CORESTORE breakage the Termux relocation approach
15 * hit (a tar-copy of a live rootfs invalidates every rocksdb corestore marker).
16 *
17 * Assets expected (staged at build time — see README):
18 * assets/proot static aarch64 proot binary
19 * assets/rootfs.tar glibc Ubuntu rootfs (minimal base for the harness smoke
20 * test; later, the full base with runtime libs + the pgh
21 * bundle baked in). Unpacked on-device with the platform's
22 * /system/bin/toybox — verified rc=0 with symlinks + perms
23 * intact — so no static tar binary needs bundling.
24 */
25object Extractor {
26 const val TAG = "celltron-node"
27
28 // Bump when the shipped rootfs/host changes, to force a clean re-extract.
29 private const val STAMP = "host.v1"
30
31 fun ensure(ctx: Context): Layout {
32 val root = ctx.filesDir
33 val layout = Layout(root)
34 val stamp = File(root, ".staged-$STAMP")
35 if (stamp.exists()) {
36 Log.i(TAG, "runtime host already staged ($STAMP)")
37 layout.tmp.mkdirs()
38 return layout
39 }
40 Log.i(TAG, "staging runtime host into ${root.absolutePath}")
41 layout.tmp.mkdirs()
42
43 // 1. proot -> filesDir, executable.
44 val proot = copyAsset(ctx, "proot", layout.proot)
45 chmodExec(proot)
46
47 // 2. unpack the rootfs with the platform toybox (present on every
48 // Android). Verified on-device: rc=0, symlinks + perms intact. The
49 // chown-to-0 warnings are harmless — the app can't chown, exactly like
50 // proot -0 fakes root.
51 layout.rootfs.mkdirs()
52 val rootfsTar = copyAsset(ctx, "rootfs.tar", File(root, "rootfs.tar"))
53 val rc = run(
54 "/system/bin/toybox", "tar", "-x", "-f", rootfsTar.absolutePath,
55 "-C", layout.rootfs.absolutePath
56 )
57 // toybox tar returns non-zero on no-root Android because it can't
58 // chown-to-0 or create hardlinks — both SELinux-denied and both
59 // harmless here (proot -0 fakes root; hardlinked aliases like
60 // perl5.34.0 are non-essential). Validate by sentinel, not by rc.
61 val sentinel = File(layout.rootfs, "usr/bin/env")
62 if (!sentinel.exists()) {
63 throw RuntimeException("rootfs extraction incomplete (rc=$rc, missing ${sentinel.path})")
64 }
65 if (rc != 0) {
66 Log.w(TAG, "toybox tar rc=$rc (expected no-root chown/hardlink denials) — sentinel present, continuing")
67 }
68 rootfsTar.delete() // reclaim disk; the asset stays in the APK
69
70 stamp.writeText(System.currentTimeMillis().toString())
71 Log.i(TAG, "runtime host staging complete")
72 return layout
73 }
74
75 private fun copyAsset(ctx: Context, name: String, dest: File): File {
76 ctx.assets.open(name).use { input ->
77 dest.outputStream().use { out -> input.copyTo(out, 1 shl 16) }
78 }
79 return dest
80 }
81
82 private fun chmodExec(f: File) {
83 try {
84 Os.chmod(f.absolutePath, 493) // 0755
85 } catch (e: Exception) {
86 f.setExecutable(true, false)
87 Log.w(TAG, "Os.chmod failed for ${f.name}, fell back to setExecutable: ${e.message}")
88 }
89 }
90
91 private fun run(vararg cmd: String): Int {
92 val p = ProcessBuilder(*cmd).redirectErrorStream(true).start()
93 // Drain output (required, else the pipe buffer fills and the child
94 // blocks) but DON'T mirror it to logcat — toybox emits thousands of
95 // harmless chown/hardlink denials that would flood and rotate the buffer.
96 var lines = 0
97 p.inputStream.bufferedReader().forEachLine { lines++ }
98 val rc = p.waitFor()
99 Log.i(TAG, "extract: rc=$rc ($lines output lines suppressed)")
100 return rc
101 }
102}
103
104/** On-device paths under the app's private filesDir. */
105class Layout(val root: File) {
106 val rootfs = File(root, "rootfs")
107 val tmp = File(root, "tmp")
108 val proot = File(root, "proot")
109}
app/src/main/java/net/celltron/node/MainActivity.kt +47 -0
……@@ -0,0 +1,47 @@
1package net.celltron.node
2
3import android.app.Activity
4import android.content.Intent
5import android.os.Build
6import android.os.Bundle
7import android.os.Handler
8import android.os.Looper
9import android.widget.TextView
10
11/**
12 * Minimal launcher: starts the foreground node service and polls its last log
13 * line so you can watch first-run extraction + sidecar boot on-device. The real
14 * GUI (WebView over pgh) replaces this in a later step.
15 */
16class MainActivity : Activity() {
17
18 private lateinit var tv: TextView
19 private val handler = Handler(Looper.getMainLooper())
20
21 override fun onCreate(savedInstanceState: Bundle?) {
22 super.onCreate(savedInstanceState)
23 tv = TextView(this).apply {
24 textSize = 14f
25 setPadding(48, 96, 48, 48)
26 }
27 setContentView(tv)
28
29 val i = Intent(this, PghNodeService::class.java)
30 if (Build.VERSION.SDK_INT >= 26) startForegroundService(i) else startService(i)
31
32 poll()
33 }
34
35 private fun poll() {
36 tv.text = "celltron node\n\n" +
37 "Runtime host: proot + glibc rootfs (extracted on first launch)\n" +
38 "Service: pgh-sidecar on @pgh-node\n\n" +
39 "Last log:\n${PghNodeService.lastLogSnapshot}"
40 handler.postDelayed({ poll() }, 1000)
41 }
42
43 override fun onDestroy() {
44 handler.removeCallbacksAndMessages(null)
45 super.onDestroy()
46 }
47}
app/src/main/java/net/celltron/node/PghNodeService.kt +96 -0
……@@ -0,0 +1,96 @@
1package net.celltron.node
2
3import android.app.Notification
4import android.app.NotificationChannel
5import android.app.NotificationManager
6import android.app.Service
7import android.content.Context
8import android.content.Intent
9import android.os.Build
10import android.os.IBinder
11import android.os.PowerManager
12import android.util.Log
13
14/**
15 * Foreground service = the anchor for the whole proot/pgh process tree.
16 *
17 * It holds a partial WakeLock (the app-level replacement for Termux's wake-lock
18 * plus the stay_on hack) so Doze doesn't suspend the node, extracts the runtime
19 * host on first launch, then supervises the pgh stack. Because the proot child
20 * is parented to this service and never setsid'd, it lives as long as the
21 * service — the fix for the reaping that killed the Termux setup.
22 *
23 * Step 1 scope: bring the runtime host up and boot pgh-sidecar on @pgh-node.
24 * The GUI bridge (WebView + LocalSocket ABSTRACT client) lands in a later step.
25 */
26class PghNodeService : Service() {
27
28 private var wakeLock: PowerManager.WakeLock? = null
29 private var launcher: ProotLauncher? = null
30 private var worker: Thread? = null
31
32 companion object {
33 /** Last log line from the supervised guest, for the launcher UI to poll. */
34 @Volatile
35 var lastLogSnapshot: String = "starting…"
36 }
37
38 override fun onCreate() {
39 super.onCreate()
40 startForegroundCompat()
41
42 val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
43 wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "celltron-node:wl").apply {
44 setReferenceCounted(false)
45 acquire()
46 }
47 Log.i(Extractor.TAG, "wakelock held=${wakeLock?.isHeld}")
48
49 worker = Thread {
50 try {
51 val layout = Extractor.ensure(applicationContext)
52 launcher = ProotLauncher(layout)
53 launcher!!.superviseForever { line -> lastLogSnapshot = line }
54 } catch (e: Exception) {
55 Log.e(Extractor.TAG, "node failed: ${e.message}", e)
56 lastLogSnapshot = "ERROR: ${e.message}"
57 }
58 }.also {
59 it.isDaemon = true
60 it.name = "celltron-node-supervisor"
61 it.start()
62 }
63 }
64
65 override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int = START_STICKY
66
67 override fun onBind(intent: Intent?): IBinder? = null
68
69 override fun onDestroy() {
70 launcher?.stop()
71 wakeLock?.let { if (it.isHeld) it.release() }
72 super.onDestroy()
73 }
74
75 private fun startForegroundCompat() {
76 val chId = "celltron-node"
77 if (Build.VERSION.SDK_INT >= 26) {
78 val ch = NotificationChannel(chId, "celltron node", NotificationManager.IMPORTANCE_LOW)
79 (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
80 .createNotificationChannel(ch)
81 }
82 val builder = if (Build.VERSION.SDK_INT >= 26) {
83 Notification.Builder(this, chId)
84 } else {
85 @Suppress("DEPRECATION")
86 Notification.Builder(this)
87 }
88 val notif = builder
89 .setContentTitle("celltron node")
90 .setContentText("pgh stack running")
91 .setSmallIcon(android.R.drawable.stat_notify_sync)
92 .setOngoing(true)
93 .build()
94 startForeground(1, notif)
95 }
96}
app/src/main/java/net/celltron/node/ProotLauncher.kt +104 -0
……@@ -0,0 +1,104 @@
1package net.celltron.node
2
3import android.util.Log
4
5/**
6 * Builds and runs the proot invocation that boots the pgh stack inside the glibc
7 * rootfs, and supervises it.
8 *
9 * The proot process is a DIRECT CHILD of the host service process — never
10 * detached (no setsid). That anchoring is deliberate and load-bearing: on
11 * Android a detached process tree gets reaped (the celltron no-setsid lesson —
12 * a `setsid supervise.sh &` had the OS reap the whole tree in ~18 min while the
13 * launcher survived). Keeping proot parented to the foreground service is what
14 * makes the tree live exactly as long as the service.
15 *
16 * Mirrors the proven celltron.sh launcher:
17 * proot -0 -r <rootfs> -b /dev -b /proc -b /sys -b <tmp>:/tmp -w /root \
18 * /usr/bin/env -i HOME=/root ... PATH=... <guest command>
19 * with PROOT_TMP_DIR set on proot's OWN environment (proot reads it there, not
20 * from the guest `env -i` set).
21 */
22class ProotLauncher(private val layout: Layout) {
23
24 @Volatile private var proc: Process? = null
25 @Volatile private var stopped = false
26
27 /**
28 * Step-1 harness smoke test: run a trivial guest command to prove the
29 * extract -> proot -> env -> supervise loop works on-device, decoupled from
30 * the full sidecar rootfs. Flip to false once the full rootfs.tar (with the
31 * pgh-sidecar bundle baked in) is staged.
32 */
33 private val smokeTest = true
34
35 /**
36 * Guest command to run under proot. Smoke: print uname + a marker, then hold
37 * (like a daemon would) so the supervisor doesn't hot-loop. Real: boot the
38 * pgh-sidecar on an abstract socket (pathname UDS is SELinux-denied no-root;
39 * @-names go through bare-abstract-socket, baked into the sidecar bundle).
40 */
41 private val guestCmd: List<String> = if (smokeTest) {
42 listOf("sh", "-c", "uname -a; echo celltron-node-smoke-ok; sleep 86400")
43 } else {
44 listOf(
45 "/root/pgh-sidecar-bootstrap-bare/runtime/bare",
46 "/root/pgh-sidecar-bootstrap-bare/bin/pgh-sidecar.bare.js"
47 )
48 }
49
50 fun command(): List<String> = buildList {
51 add(layout.proot.absolutePath)
52 add("-0") // fake root
53 add("-r"); add(layout.rootfs.absolutePath) // new root
54 add("-b"); add("/dev")
55 add("-b"); add("/proc")
56 add("-b"); add("/sys")
57 add("-b"); add("${layout.tmp.absolutePath}:/tmp")
58 add("-w"); add("/root")
59 add("/usr/bin/env"); add("-i") // clean guest env
60 add("HOME=/root")
61 add("TERM=xterm")
62 add("LANG=C.UTF-8")
63 add("PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin")
64 add("PGH_SIDECAR_SOCK=@pgh-node") // abstract UDS (SELinux-safe)
65 addAll(guestCmd)
66 }
67
68 /** Blocking supervise loop — call on a background thread. Restarts on exit. */
69 fun superviseForever(onLine: (String) -> Unit) {
70 var backoff = 1000L
71 while (!stopped) {
72 val startedAt = System.currentTimeMillis()
73 try {
74 val pb = ProcessBuilder(command()).redirectErrorStream(true)
75 // proot reads PROOT_TMP_DIR from its own env (Android has no /tmp).
76 pb.environment()["PROOT_TMP_DIR"] = layout.tmp.absolutePath
77 Log.i(Extractor.TAG, "launch: ${command().joinToString(" ")}")
78 val p = pb.start().also { proc = it }
79 p.inputStream.bufferedReader().forEachLine { line ->
80 Log.i(Extractor.TAG, line)
81 onLine(line)
82 }
83 val rc = p.waitFor()
84 Log.w(Extractor.TAG, "guest exited rc=$rc")
85 } catch (e: Exception) {
86 Log.e(Extractor.TAG, "supervise error: ${e.message}")
87 }
88 if (stopped) break
89 // Reset backoff after a healthy run (>60s); otherwise grow it so a
90 // crash-on-startup doesn't hot-loop.
91 backoff = if (System.currentTimeMillis() - startedAt > 60_000L) {
92 1000L
93 } else {
94 (backoff * 2).coerceAtMost(30_000L)
95 }
96 try { Thread.sleep(backoff) } catch (e: InterruptedException) { break }
97 }
98 }
99
100 fun stop() {
101 stopped = true
102 try { proc?.destroy() } catch (e: Exception) {}
103 }
104}
build.gradle +4 -0
……@@ -0,0 +1,4 @@
1plugins {
2 id 'com.android.application' version '8.5.2' apply false
3 id 'org.jetbrains.kotlin.android' version '1.9.24' apply false
4}
gradle.properties +4 -0
……@@ -0,0 +1,4 @@
1org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
2android.useAndroidX=false
3kotlin.code.style=official
4org.gradle.caching=true
local.properties +1 -0
……@@ -0,0 +1 @@
1sdk.dir=/home/fleeky/android-tools/sdk
settings.gradle +16 -0
……@@ -0,0 +1,16 @@
1pluginManagement {
2 repositories {
3 google()
4 mavenCentral()
5 gradlePluginPortal()
6 }
7}
8dependencyResolutionManagement {
9 repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 repositories {
11 google()
12 mavenCentral()
13 }
14}
15rootProject.name = 'celltron-node'
16include ':app'