| |
| """ |
| 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") |
| |
| |
| with zipfile.ZipFile(zip_path, 'r') as z: |
| z.extractall(self.sdk_dir) |
| |
| |
| 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) |
| |
| 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: |
| 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) |
| |
| |
| 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) |
| |
| |
| 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"""<?xml version="1.0" encoding="utf-8"?> |
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" |
| package="{self.package}"> |
| <application |
| android:label="@string/app_name" |
| android:theme="@style/Theme.FSI"> |
| <activity android:name=".{activity_name}" android:exported="true"> |
| <intent-filter> |
| <action android:name="android.intent.action.MAIN" /> |
| <category android:name="android.intent.category.LAUNCHER" /> |
| </intent-filter> |
| </activity> |
| </application> |
| </manifest> |
| """ |
| 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 = """<?xml version="1.0" encoding="utf-8"?> |
| <androidx.constraintlayout.widget.ConstraintLayout |
| xmlns:android="http://schemas.android.com/apk/res/android" |
| android:layout_width="match_parent" |
| android:layout_height="match_parent" |
| android:background="#0A0A0F"> |
| |
| <TextView |
| android:id="@+id/title" |
| android:layout_width="wrap_content" |
| android:layout_height="wrap_content" |
| android:text="FSI_FELON" |
| android:textColor="#00FF41" |
| android:textSize="32sp" |
| android:textStyle="bold" |
| android:fontFamily="monospace" /> |
| |
| </androidx.constraintlayout.widget.ConstraintLayout> |
| """ |
| 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"""<?xml version="1.0" encoding="utf-8"?> |
| <resources> |
| <string name="app_name">{self.app_name}</string> |
| </resources> |
| """) |
| |
| with open(os.path.join(self.project_dir, "app/src/main/res/values/themes.xml"), 'w') as f: |
| f.write("""<?xml version="1.0" encoding="utf-8"?> |
| <resources> |
| <style name="Theme.FSI" parent="Theme.Material3.Dark.NoActionBar"> |
| <item name="android:statusBarColor">#0A0A0F</item> |
| </style> |
| </resources> |
| """) |
| |
| 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}") |
| |
| |
| 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: |
| |
| 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() |
|
|