Skip to content

Commit 60f436a

Browse files
friunscursoragent
andcommitted
Add proot and Python to first-run setup, auto-changelog in CI
- Install proot + libtalloc from Termux repos during bootstrap so the codex agent can use proot for dpkg path remapping - Pre-install Python 3.12 + pip during first-run setup - CI release notes now auto-generated from git commit messages since the previous release tag Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 5f75a37 commit 60f436a

3 files changed

Lines changed: 164 additions & 2 deletions

File tree

.github/workflows/build-apk.yml

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ jobs:
1515
steps:
1616
- name: Checkout repository
1717
uses: actions/checkout@v4
18+
with:
19+
fetch-depth: 0
1820

1921
- name: Set up Node.js
2022
uses: actions/setup-node@v4
@@ -59,6 +61,22 @@ jobs:
5961
name: codexapp
6062
path: android/app/build/outputs/apk/debug/codexapp.apk
6163

64+
- name: Generate changelog
65+
if: github.ref == 'refs/heads/android'
66+
id: changelog
67+
run: |
68+
PREV_TAG=$(git tag --sort=-v:refname | head -1)
69+
if [ -n "$PREV_TAG" ]; then
70+
CHANGES=$(git log "$PREV_TAG"..HEAD --pretty=format:"- %s" --reverse)
71+
else
72+
CHANGES=$(git log --pretty=format:"- %s" --reverse -20)
73+
fi
74+
{
75+
echo "notes<<CHANGELOG_EOF"
76+
echo "$CHANGES"
77+
echo "CHANGELOG_EOF"
78+
} >> "$GITHUB_OUTPUT"
79+
6280
- name: Create Release
6381
if: github.ref == 'refs/heads/android'
6482
uses: softprops/action-gh-release@v2
@@ -68,9 +86,12 @@ jobs:
6886
body: |
6987
🔥 **Codex App** — OpenAI Codex on Android
7088
71-
📱 Install the APK on any ARM64 Android 7.0+ device.
72-
No root required.
89+
📱 Install the APK on any ARM64 Android 7.0+ device. No root required.
90+
91+
### Changes
92+
${{ steps.changelog.outputs.notes }}
7393
94+
---
7495
Built from commit ${{ github.sha }}
7596
files: android/app/build/outputs/apk/debug/codexapp.apk
7697
draft: false

android/app/src/main/java/com/codex/mobile/CodexServerManager.kt

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ class CodexServerManager(private val context: Context) {
7979

8080
// ── Install checks ─────────────────────────────────────────────────────
8181

82+
fun isProotInstalled(): Boolean {
83+
val paths = BootstrapInstaller.getPaths(context)
84+
return File(paths.prefixDir, "bin/proot").exists()
85+
}
86+
8287
fun isNodeInstalled(): Boolean {
8388
val paths = BootstrapInstaller.getPaths(context)
8489
return File(paths.prefixDir, "bin/node").exists()
@@ -177,6 +182,123 @@ WEOF
177182
return isNodeInstalled()
178183
}
179184

185+
/**
186+
* Install proot from the Termux repository. proot uses ptrace to
187+
* intercept filesystem syscalls and remap hardcoded Termux paths
188+
* (e.g. /data/data/com.termux/files/usr) to our actual prefix,
189+
* enabling dpkg, apt-get install, and other tools that have
190+
* compiled-in path references.
191+
*/
192+
fun installProot(onProgress: (String) -> Unit): Boolean {
193+
val paths = BootstrapInstaller.getPaths(context)
194+
val prefix = paths.prefixDir
195+
val termuxPrefix = "/data/data/com.termux/files/usr"
196+
197+
onProgress("Downloading proot…")
198+
199+
val downloadCmd = """
200+
cd $prefix/tmp &&
201+
apt-get update --allow-insecure-repositories 2>&1;
202+
apt-get download --allow-unauthenticated proot libtalloc 2>&1
203+
""".trimIndent()
204+
205+
val dlCode = runInPrefix(downloadCmd, onOutput = { onProgress(it) })
206+
if (dlCode != 0) {
207+
Log.e(TAG, "apt-get download proot failed with code $dlCode")
208+
return false
209+
}
210+
211+
onProgress("Extracting proot…")
212+
val extractCmd = """
213+
cd $prefix/tmp &&
214+
mkdir -p _proot_stage &&
215+
for deb in proot*.deb libtalloc*.deb; do
216+
[ -f "${'$'}deb" ] && dpkg-deb -x "${'$'}deb" _proot_stage/ 2>&1
217+
done &&
218+
if [ -d "_proot_stage$termuxPrefix" ]; then
219+
cp -a _proot_stage$termuxPrefix/* "$prefix/" 2>&1
220+
elif [ -d "_proot_stage/usr" ]; then
221+
cp -a _proot_stage/usr/* "$prefix/" 2>&1
222+
fi &&
223+
chmod 700 "$prefix/bin/proot" 2>/dev/null
224+
rm -rf _proot_stage proot*.deb libtalloc*.deb 2>/dev/null
225+
echo "proot installed"
226+
""".trimIndent()
227+
228+
val extractCode = runInPrefix(extractCmd, onOutput = { onProgress(it) })
229+
if (extractCode != 0) {
230+
Log.e(TAG, "proot extract failed with code $extractCode")
231+
return false
232+
}
233+
234+
return isProotInstalled()
235+
}
236+
237+
fun isPythonInstalled(): Boolean {
238+
val paths = BootstrapInstaller.getPaths(context)
239+
return File(paths.prefixDir, "bin/python3").exists() ||
240+
File(paths.prefixDir, "bin/python").exists()
241+
}
242+
243+
/**
244+
* Install Python using proot to handle dpkg's hardcoded Termux paths.
245+
* proot bind-mounts our prefix onto the compiled-in Termux prefix so
246+
* dpkg postinst scripts and shared library lookups resolve correctly.
247+
*/
248+
fun installPython(onProgress: (String) -> Unit): Boolean {
249+
val paths = BootstrapInstaller.getPaths(context)
250+
val prefix = paths.prefixDir
251+
val termuxPrefix = "/data/data/com.termux/files/usr"
252+
253+
onProgress("Downloading Python packages…")
254+
255+
val downloadCmd = """
256+
cd $prefix/tmp &&
257+
apt-get update --allow-insecure-repositories 2>&1;
258+
apt-get download --allow-unauthenticated python python-pip 2>&1
259+
""".trimIndent()
260+
261+
val dlCode = runInPrefix(downloadCmd, onOutput = { onProgress(it) })
262+
if (dlCode != 0) {
263+
Log.e(TAG, "apt-get download python failed with code $dlCode")
264+
}
265+
266+
onProgress("Extracting Python…")
267+
val extractCmd = """
268+
cd $prefix/tmp &&
269+
mkdir -p _python_stage &&
270+
for deb in python*.deb; do
271+
[ -f "${'$'}deb" ] && echo "Extracting ${'$'}deb..." && dpkg-deb -x "${'$'}deb" _python_stage/ 2>&1
272+
done &&
273+
if [ -d "_python_stage$termuxPrefix" ]; then
274+
cp -a _python_stage$termuxPrefix/* "$prefix/" 2>&1
275+
elif [ -d "_python_stage/usr" ]; then
276+
cp -a _python_stage/usr/* "$prefix/" 2>&1
277+
fi &&
278+
chmod 700 "$prefix/bin/python"* 2>/dev/null
279+
chmod 700 "$prefix/bin/pip"* 2>/dev/null
280+
rm -rf _python_stage python*.deb 2>/dev/null
281+
echo "Python installed"
282+
""".trimIndent()
283+
284+
val extractCode = runInPrefix(extractCmd, onOutput = { onProgress(it) })
285+
if (extractCode != 0) {
286+
Log.e(TAG, "Python extract failed with code $extractCode")
287+
return false
288+
}
289+
290+
// Create python3 wrapper to handle shebang issues
291+
val fixCmd = """
292+
if [ -f "$prefix/bin/python3" ] && [ ! -f "$prefix/bin/python" ]; then
293+
ln -sf python3 "$prefix/bin/python"
294+
fi
295+
echo "Python ready"
296+
""".trimIndent()
297+
runInPrefix(fixCmd, onOutput = { onProgress(it) })
298+
299+
return isPythonInstalled()
300+
}
301+
180302
fun installCodex(onProgress: (String) -> Unit): Boolean {
181303
val paths = BootstrapInstaller.getPaths(context)
182304
val prefix = paths.prefixDir

android/app/src/main/java/com/codex/mobile/MainActivity.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,16 @@ class MainActivity : AppCompatActivity() {
139139
}
140140
updateStatus("Environment ready")
141141

142+
// Step 1b: Install proot (needed for dpkg/apt-get path remapping)
143+
if (!serverManager.isProotInstalled()) {
144+
updateStatus("Installing proot…", "Needed for package management")
145+
val prootOk = serverManager.installProot { msg -> updateDetail(msg) }
146+
if (!prootOk) {
147+
throw RuntimeException("Failed to install proot")
148+
}
149+
}
150+
updateStatus("proot ready")
151+
142152
// Step 2: Install Node.js
143153
if (!serverManager.isNodeInstalled()) {
144154
updateStatus("Installing Node.js (first run)…", "This may take a few minutes")
@@ -149,6 +159,15 @@ class MainActivity : AppCompatActivity() {
149159
}
150160
updateStatus("Node.js ready")
151161

162+
// Step 2b: Install Python
163+
if (!serverManager.isPythonInstalled()) {
164+
updateStatus("Installing Python…")
165+
val pyOk = serverManager.installPython { msg -> updateDetail(msg) }
166+
if (!pyOk) {
167+
Log.w(TAG, "Python install failed — continuing without it")
168+
}
169+
}
170+
152171
// Step 3: Install Codex CLI
153172
if (!serverManager.isCodexInstalled()) {
154173
updateStatus("Installing Codex CLI…", "This may take a few minutes")

0 commit comments

Comments
 (0)