#!/usr/bin/env python3
"""
FSI_FELON ANDROID TERMINAL COMPILER v2
No Android Studio. No wrapper download. Uses system Gradle + Android SDK.
"""
import os, sys, subprocess, shutil, zipfile, urllib.request, glob
from pathlib import Path
ANDROID_SDK_DIR = os.path.expanduser("~/android-sdk")
GRADLE_VERSION = "8.5"
class AndroidCompiler:
def __init__(self):
self.sdk_dir = ANDROID_SDK_DIR
self.project_dir = None
self.app_name = "FSIApp"
self.package = "com.fsi.app"
self._ensure_sdk()
def _sdk_manager_path(self):
p = os.path.join(self.sdk_dir, "cmdline-tools", "latest", "bin", "sdkmanager")
if os.path.exists(p):
return p
p2 = os.path.join(self.sdk_dir, "cmdline-tools", "bin", "sdkmanager")
return p2 if os.path.exists(p2) else None
def _ensure_sdk(self):
"""Download Android SDK command-line tools if not present."""
if self._sdk_manager_path():
installed = self._ensure_platform()
if installed:
print(" ✅ Android SDK ready")
return True
print(" 📥 Downloading Android SDK command-line tools...")
os.makedirs(self.sdk_dir, exist_ok=True)
sdk_url = "https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip"
zip_path = f"{self.sdk_dir}/cmdline-tools.zip"
try:
urllib.request.urlretrieve(sdk_url, zip_path)
print(" ✅ SDK tools downloaded")
# Extract
with zipfile.ZipFile(zip_path, 'r') as z:
z.extractall(self.sdk_dir)
# Move to correct structure
old = os.path.join(self.sdk_dir, "cmdline-tools")
new = os.path.join(self.sdk_dir, "cmdline-tools", "latest")
if os.path.exists(new):
shutil.rmtree(new)
os.makedirs(os.path.dirname(new), exist_ok=True)
# Move contents rather than directory to avoid nested structure
for item in os.listdir(old):
shutil.move(os.path.join(old, item), new)
os.rmdir(old)
os.remove(zip_path)
print(" ✅ SDK ready")
return self._ensure_platform()
except Exception as e:
print(f" ⚠️ SDK download failed: {e}")
print(" 💡 Manual install: https://developer.android.com/studio#command-tools")
return False
def _ensure_platform(self):
"""Ensure required Android platform and build tools are installed."""
sm = self._sdk_manager_path()
if not sm:
return False
try:
r = subprocess.run([sm, "--list", "--sdk_root=" + self.sdk_dir], capture_output=True, text=True, timeout=30)
if "platforms;android-34" not in r.stdout:
print(" 📥 Installing Android SDK platform 34...")
subprocess.run([sm, "--sdk_root=" + self.sdk_dir, "platforms;android-34"], capture_output=True, timeout=120)
if "build-tools;34.0.0" not in r.stdout:
print(" 📥 Installing build-tools 34.0.0...")
subprocess.run([sm, "--sdk_root=" + self.sdk_dir, "build-tools;34.0.0"], capture_output=True, timeout=120)
return True
except:
return False
def _find_aapt(self):
bt = os.path.join(self.sdk_dir, "build-tools")
if os.path.exists(bt):
for v in sorted(os.listdir(bt), reverse=True):
aapt = os.path.join(bt, v, "aapt")
if os.path.exists(aapt):
return aapt
return shutil.which("aapt")
def verify_apk(self, apk_path=None):
"""Verify APK using aapt dump badging."""
path = apk_path or self._find_apk()
if not path or not os.path.exists(path):
return {"valid": False, "error": "APK not found"}
aapt = self._find_aapt()
if not aapt:
return {"valid": False, "error": "aapt not found — install Android build-tools"}
try:
r = subprocess.run([aapt, "dump", "badging", path], capture_output=True, text=True, timeout=15)
if r.returncode == 0:
lines = r.stdout.split("\n")
info = {"valid": True, "path": path, "size": os.path.getsize(path)}
for l in lines:
if "package:" in l:
for p in l.split():
if "name=" in p: info["package"] = p.split("=")[1].strip("'")
if "versionCode=" in p: info["version_code"] = p.split("=")[1].strip("'")
if "versionName=" in p: info["version_name"] = p.split("=")[1].strip("'")
if "launchable-activity:" in l:
info["activity"] = l.split("name=")[1].split()[0].strip("'")
if "sdkVersion:" in l:
info["min_sdk"] = l.split(":")[1].strip()
if "targetSdkVersion:" in l:
info["target_sdk"] = l.split(":")[1].strip()
return info
else:
return {"valid": False, "error": r.stderr[:200]}
except Exception as e:
return {"valid": False, "error": str(e)}
def install_apk(self, apk_path=None, serial=None):
"""Install APK via adb."""
path = apk_path or self._find_apk()
if not path or not os.path.exists(path):
return {"success": False, "error": "APK not found"}
adb = shutil.which("adb")
if not adb:
return {"success": False, "error": "adb not found — install Android platform-tools"}
cmd = [adb]
if serial:
cmd += ["-s", serial]
cmd += ["install", "-r", path]
try:
r = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
if "Success" in r.stdout:
return {"success": True, "output": r.stdout.strip()}
else:
return {"success": False, "error": r.stdout[-200:] or r.stderr[-200:]}
except Exception as e:
return {"success": False, "error": str(e)}
def list_devices(self):
"""List connected ADB devices."""
adb = shutil.which("adb")
if not adb:
return []
try:
r = subprocess.run([adb, "devices"], capture_output=True, text=True, timeout=5)
lines = r.stdout.strip().split("\n")[1:]
devices = []
for l in lines:
if l.strip() and "device" in l and "offline" not in l:
devices.append(l.split()[0])
return devices
except:
return []
def _find_apk(self):
if self.project_dir:
apks = glob.glob(self.project_dir + "/**/*.apk", recursive=True)
if apks:
return apks[0]
standard = os.path.join(self.project_dir, "app/build/outputs/apk/debug/app-debug.apk")
if os.path.exists(standard):
return standard
return None
def _ensure_gradle(self):
"""Check if gradle is available."""
if shutil.which("gradle"):
return True
# Try to install via apt
try:
subprocess.run(["apt-get", "install", "-y", "gradle"],
capture_output=True, timeout=60)
return shutil.which("gradle") is not None
except:
return False
def generate_project(self, app_name="FSIApp", package="com.fsi.app", activity_name="MainActivity"):
"""Generate a complete Android project."""
self.app_name = app_name
self.package = package
self.project_dir = f"/tmp/fsi_felon/android_projects/{app_name.lower().replace(' ', '_')}"
print(f"\n 🔨 GENERATING: {app_name}")
print(f" 📦 Package: {package}")
if os.path.exists(self.project_dir):
shutil.rmtree(self.project_dir)
# Create structure
pkg_path = package.replace('.', '/')
dirs = [
f"app/src/main/java/{pkg_path}",
"app/src/main/res/layout",
"app/src/main/res/values",
"app/src/main/res/drawable",
"gradle/wrapper"
]
for d in dirs:
os.makedirs(os.path.join(self.project_dir, d), exist_ok=True)
# Write all files
self._write_build_gradle()
self._write_manifest(activity_name)
self._write_main_activity(activity_name, pkg_path)
self._write_layout()
self._write_resources()
self._write_gradle_props()
print(f" ✅ Generated: {len(dirs)} directories, 10+ files")
return self.project_dir
def _write_build_gradle(self):
content = f"""plugins {{
id 'com.android.application'
}}
android {{
namespace '{self.package}'
compileSdk 34
defaultConfig {{
applicationId "{self.package}"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"
}}
buildTypes {{
release {{
minifyEnabled false
}}
}}
}}
dependencies {{
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
}}
"""
with open(os.path.join(self.project_dir, "app", "build.gradle"), 'w') as f:
f.write(content)
def _write_manifest(self, activity_name):
content = f"""
"""
with open(os.path.join(self.project_dir, "app", "src", "main", "AndroidManifest.xml"), 'w') as f:
f.write(content)
def _write_main_activity(self, activity_name, pkg_path):
content = f"""package {self.package};
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
public class {activity_name} extends AppCompatActivity {{
@Override
protected void onCreate(Bundle savedInstanceState) {{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}}
}}
"""
path = os.path.join(self.project_dir, f"app/src/main/java/{pkg_path}/{activity_name}.java")
with open(path, 'w') as f:
f.write(content)
def _write_layout(self):
content = """
"""
with open(os.path.join(self.project_dir, "app/src/main/res/layout/activity_main.xml"), 'w') as f:
f.write(content)
def _write_resources(self):
with open(os.path.join(self.project_dir, "app/src/main/res/values/strings.xml"), 'w') as f:
f.write(f"""
{self.app_name}
""")
with open(os.path.join(self.project_dir, "app/src/main/res/values/themes.xml"), 'w') as f:
f.write("""
""")
def _write_gradle_props(self):
with open(os.path.join(self.project_dir, "gradle.properties"), 'w') as f:
f.write("""org.gradle.jvmargs=-Xmx2048m
android.useAndroidX=true
""")
with open(os.path.join(self.project_dir, "settings.gradle"), 'w') as f:
f.write(f"""pluginManagement {{
repositories {{ google(); mavenCentral(); gradlePluginPortal() }}
}}
dependencyResolutionManagement {{
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {{ google(); mavenCentral() }}
}}
rootProject.name = "{self.app_name}"
include ':app'
""")
def compile(self):
"""Compile using system gradle or gradlew."""
if not self.project_dir:
print(" ❌ No project. Generate first.")
return None
if not self._ensure_gradle():
print(" ❌ Gradle not found. Install: apt-get install gradle")
return None
print(f"\n 🔨 COMPILING...")
print(f" 📁 {self.project_dir}")
# Try gradle directly
gradle = shutil.which("gradle")
cmd = [gradle, "assembleDebug", "--no-daemon", "-p", self.project_dir]
print(f" ⚙️ {' '.join(cmd)}")
print(f" ⏳ First build takes 3-5 minutes (downloads dependencies)...")
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
if result.returncode == 0:
apk = os.path.join(self.project_dir, "app/build/outputs/apk/debug/app-debug.apk")
if os.path.exists(apk):
size = os.path.getsize(apk)
print(f"\n ✅ APK BUILT SUCCESSFULLY")
print(f" 📦 {apk}")
print(f" 📊 {size:,} bytes ({size/1024/1024:.2f} MB)")
print(f"\n 🚀 INSTALL: adb install {apk}")
print(f" 📤 SHARE: cp {apk} ~/Downloads/")
return apk
else:
# Find APK
apks = glob.glob(self.project_dir + "/**/*.apk", recursive=True)
if apks:
print(f" ✅ APK found: {apks[0]}")
return apks[0]
print(" ⚠️ Build succeeded but APK not found")
return None
else:
print(f" ❌ BUILD FAILED")
err = result.stderr[-500:] if result.stderr else result.stdout[-500:]
print(f" {err}")
return None
except subprocess.TimeoutExpired:
print(" ⏱️ Timeout. Try again with more memory.")
return None
except Exception as e:
print(f" ❌ Error: {e}")
return None
def main():
compiler = AndroidCompiler()
project = compiler.generate_project("FSI_Demo", "com.fsi.demo", "MainActivity")
apk = compiler.compile()
if apk:
print(f"\n 🎉 DONE. APK ready at: {apk}")
if __name__ == '__main__':
main()