from flask import Flask, request, send_file, render_template_string import os import shutil import subprocess import tempfile import logging from markupsafe import escape import glob app = Flask(__name__) # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # HTML template with license checkbox HTML_TEMPLATE = """ Python to APK Converter

Python to Android APK Converter

Please enter a valid alphanumeric app name with no spaces.
You must accept the Android SDK License Agreement to proceed.
Converting...

Converting your code to APK...

{% if error %} {% endif %} {% if apk_path %} {% endif %} {% if logs %}
Build Logs
{{ logs | safe }}
{% endif %}
""" @app.route("/", methods=["GET"]) def index(): return render_template_string(HTML_TEMPLATE) @app.route("/convert", methods=["POST"]) def convert(): app_name = request.form.get("app_name").replace(" ", "_") code = request.form.get("code") requirements = request.form.get("requirements") license_agreement = request.form.get("license_agreement") logs = [] # Validate license agreement if license_agreement != "on": logs.append("Validation error: You must accept the Android SDK License Agreement.") return render_template_string(HTML_TEMPLATE, error="You must accept the Android SDK License Agreement to proceed.", logs="\n".join(logs)) # Validate app name if not app_name.isalnum(): logs.append("Validation error: App name must be alphanumeric with no spaces.") return render_template_string(HTML_TEMPLATE, error="App name must be alphanumeric with no spaces.", logs="\n".join(logs)) # Log Python and pip versions for debugging try: python_version = subprocess.run( ["python3", "--version"], capture_output=True, text=True, check=True ).stdout.strip() logs.append(f"Python version: {python_version}") pip_version = subprocess.run( ["python3", "-m", "pip", "--version"], capture_output=True, text=True, check=True ).stdout.strip() logs.append(f"pip version: {pip_version}") except subprocess.CalledProcessError as e: logs.append("Error: Failed to verify Python or pip:") logs.append(e.stderr) return render_template_string(HTML_TEMPLATE, error="Failed to verify Python/pip environment.", logs="\n".join(logs)) # Create a temporary directory for the project with tempfile.TemporaryDirectory() as temp_dir: project_dir = os.path.join(temp_dir, app_name) logs.append(f"Created temporary project directory: {project_dir}") os.makedirs(project_dir, exist_ok=True) # Create the Python app structure src_dir = os.path.join(project_dir, "src", app_name) os.makedirs(src_dir, exist_ok=True) logs.append(f"Created source directory: {src_dir}") # Write the user-provided code to app.py app_file = os.path.join(src_dir, "app.py") with open(app_file, "w") as f: f.write(code) logs.append(f"Wrote user code to: {app_file}") # Write requirements.txt if provided if requirements and requirements.strip(): req_file = os.path.join(project_dir, "requirements.txt") with open(req_file, "w") as f: f.write(requirements) logs.append(f"Wrote requirements to: {req_file}") try: # Install dependencies as the current user result = subprocess.run( ["python3", "-m", "pip", "install", "--user", "--no-cache-dir", "-r", req_file], check=True, capture_output=True, text=True, env={"HOME": "/home/appuser", "PATH": os.environ.get("PATH")} ) logs.append("Installed dependencies successfully:") logs.append(result.stdout) if result.stderr: logs.append("Dependency installation warnings:") logs.append(result.stderr) except subprocess.CalledProcessError as e: logs.append("Failed to install dependencies:") logs.append(e.stderr) return render_template_string(HTML_TEMPLATE, error=f"Failed to install dependencies: {escape(e.stderr)}", logs="\n".join(logs)) # Create a minimal pyproject.toml for BeeWare pyproject_content = f""" [tool.briefcase] project_name = "{app_name}" bundle = "com.example" version = "0.0.1" description = "A Python app converted to APK" license = "MIT" [tool.briefcase.app.{app_name}] formal_name = "{app_name}" icon = "" sources = ["src/{app_name}"] [tool.briefcase.app.{app_name}.android] build_tool = "gradle" """ if requirements and requirements.strip(): pyproject_content += f""" requirements = [{', '.join(f'"{line.strip()}"' for line in requirements.splitlines() if line.strip())}] """ pyproject_file = os.path.join(project_dir, "pyproject.toml") with open(pyproject_file, "w") as f: f.write(pyproject_content) logs.append(f"Wrote pyproject.toml to: {pyproject_file}") try: # Change to project directory os.chdir(project_dir) logs.append(f"Changed working directory to: {project_dir}") # Verify Briefcase installation briefcase_version = subprocess.run( ["briefcase", "--version"], capture_output=True, text=True, check=True ).stdout.strip() logs.append(f"Briefcase version: {briefcase_version}") # Log environment variables for debugging logs.append(f"Environment PATH: {os.environ.get('PATH')}") logs.append(f"Android SDK Home: {os.environ.get('ANDROID_HOME')}") logs.append(f"Android SDK Root: {os.environ.get('ANDROID_SDK_ROOT')}") logs.append(f"Java Home: {os.environ.get('JAVA_HOME', 'Not set')}") # Verify Android SDK tools try: sdkmanager_version = subprocess.run( ["sdkmanager", "--version"], capture_output=True, text=True, check=True ).stdout.strip() logs.append(f"sdkmanager version: {sdkmanager_version}") except subprocess.CalledProcessError as e: logs.append("Failed to verify sdkmanager version:") logs.append(e.stderr) return render_template_string(HTML_TEMPLATE, error="Failed to verify Android SDK tools.", logs="\n".join(logs)) # Verify Java installation try: java_version = subprocess.run( ["java", "-version"], capture_output=True, text=True, check=True ).stderr.strip() # java -version outputs to stderr logs.append(f"Java version: {java_version}") except subprocess.CalledProcessError as e: logs.append("Failed to verify Java version:") logs.append(e.stderr) return render_template_string(HTML_TEMPLATE, error="Failed to verify Java installation.", logs="\n".join(logs)) # Ensure Android SDK licenses are accepted try: logs.append("Ensuring Android SDK licenses are accepted...") result_licenses = subprocess.run( ["sdkmanager", "--licenses"], input="y\n" * 10, # Accept all licenses capture_output=True, text=True, check=True, env={ "HOME": "/home/appuser", "PATH": os.environ.get("PATH"), "ANDROID_HOME": "/opt/android-sdk", "ANDROID_SDK_ROOT": "/opt/android-sdk", "JAVA_HOME": "/usr/lib/jvm/java-17-openjdk-amd64" } ) logs.append("License acceptance output:") logs.append(result_licenses.stdout) if result_licenses.stderr: logs.append("License acceptance warnings/errors:") logs.append(result_licenses.stderr) except subprocess.CalledProcessError as e: logs.append("Failed to accept Android SDK licenses:") logs.append(f"Return code: {e.returncode}") logs.append(f"Stdout: {e.stdout}") logs.append(f"Stderr: {e.stderr}") return render_template_string(HTML_TEMPLATE, error="Failed to accept Android SDK licenses.", logs="\n".join(logs)) # Run Briefcase create with verbose output and debug logging logs.append("Running Briefcase create command...") env = { "HOME": "/home/appuser", "PATH": os.environ.get("PATH"), "ANDROID_HOME": "/opt/android-sdk", "ANDROID_SDK_ROOT": "/opt/android-sdk", "JAVA_HOME": "/usr/lib/jvm/java-17-openjdk-amd64" } try: result_create = subprocess.run( ["briefcase", "create", "android", "-v", "--no-input"], check=True, capture_output=True, text=True, env=env ) logs.append("Briefcase create output:") logs.append(result_create.stdout) if result_create.stderr: logs.append("Briefcase create warnings/errors:") logs.append(result_create.stderr) except subprocess.CalledProcessError as e: logs.append("Briefcase create failed with error:") logs.append(f"Return code: {e.returncode}") logs.append(f"Stdout: {e.stdout}") logs.append(f"Stderr: {e.stderr}") return render_template_string(HTML_TEMPLATE, error=f"Briefcase create failed: {escape(e.stderr or e.stdout)}", logs="\n".join(logs)) except Exception as e: logs.append(f"Unexpected error during Briefcase create: {str(e)}") return render_template_string(HTML_TEMPLATE, error=f"Unexpected error: {str(e)}", logs="\n".join(logs)) # Run Briefcase build logs.append("Running Briefcase build command...") try: result_build = subprocess.run( ["briefcase", "build", "android", "-v", "--no-input"], check=True, capture_output=True, text=True, env=env ) logs.append("Briefcase build output:") logs.append(result_build.stdout) if result_build.stderr: logs.append("Briefcase build warnings/errors:") logs.append(result_build.stderr) except subprocess.CalledProcessError as e: logs.append("Briefcase build failed with error:") logs.append(f"Return code: {e.returncode}") logs.append(f"Stdout: {e.stdout}") logs.append(f"Stderr: {e.stderr}") return render_template_string(HTML_TEMPLATE, error=f"Briefcase build failed: {escape(e.stderr or e.stdout)}", logs="\n".join(logs)) # Locate the generated APK apk_file = "app-debug.apk" # Try the default path apk_dir = os.path.join(project_dir, "build", app_name, "android", "gradle", "app", "build", "outputs", "apk", "debug") apk_path = os.path.join(apk_dir, apk_file) logs.append(f"Checking for APK at: {apk_path}") # If not found, search for app-debug.apk in the project directory if not os.path.exists(apk_path): logs.append(f"APK not found at: {apk_path}") apk_files = glob.glob(os.path.join(project_dir, "**", "app-debug.apk"), recursive=True) if apk_files: apk_path = apk_files[0] logs.append(f"Found APK at alternative path: {apk_path}") else: logs.append("Error: APK not found in project directory.") return render_template_string(HTML_TEMPLATE, error="APK not found after build.", logs="\n".join(logs)) # Move APK to persistent directory persistent_dir = "/persistent_storage" os.makedirs(persistent_dir, exist_ok=True) final_apk_path = os.path.join(persistent_dir, f"{app_name}.apk") shutil.copy(apk_path, final_apk_path) logs.append(f"Copied APK to: {final_apk_path}") return render_template_string(HTML_TEMPLATE, apk_path=f"{app_name}.apk", logs="\n".join(logs)) except subprocess.CalledProcessError as e: logs.append("Build failed with the following error:") logs.append(e.stderr) return render_template_string(HTML_TEMPLATE, error=f"Build failed: {escape(e.stderr)}", logs="\n".join(logs)) @app.route("/download/") def download(filename): try: file_path = os.path.join("/persistent_storage", filename) return send_file(file_path, as_attachment=True) except FileNotFoundError: return render_template_string(HTML_TEMPLATE, error="APK file not found for download.") if __name__ == "__main__": app.run(host="0.0.0.0", port=7860)