py2apk / app.py
broadfield-dev's picture
Update app.py
44d3cab verified
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 = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Python to APK Converter</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background-color: #f8f9fa;
}
.container {
max-width: 800px;
margin-top: 2rem;
}
.card {
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.form-label {
font-weight: 500;
}
.btn-primary {
background-color: #007bff;
border: none;
padding: 0.75rem 1.5rem;
}
.btn-primary:hover {
background-color: #0056b3;
}
.alert {
margin-top: 1rem;
}
#progress-container {
display: none;
margin-top: 1rem;
}
.download-link {
font-size: 1.1rem;
margin-top: 1rem;
}
</style>
</head>
<body>
<div class="container">
<div class="card p-4">
<h1 class="text-center mb-4">Python to Android APK Converter</h1>
<form id="convert-form" method="POST" action="/convert">
<div class="mb-3">
<label for="app_name" class="form-label">App Name (alphanumeric, no spaces)</label>
<input type="text" class="form-control" id="app_name" name="app_name" required pattern="[A-Za-z0-9]+">
<div class="invalid-feedback">Please enter a valid alphanumeric app name with no spaces.</div>
</div>
<div class="mb-3">
<label for="code" class="form-label">Python Code (Toga-compatible, e.g., app.py)</label>
<textarea class="form-control" id="code" name="code" rows="10" required></textarea>
</div>
<div class="mb-3">
<label for="requirements" class="form-label">Requirements.txt (optional, list dependencies like requests==2.28.1)</label>
<textarea class="form-control" id="requirements" name="requirements" rows="5"></textarea>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="license_agreement" name="license_agreement" required>
<label class="form-check-label" for="license_agreement">
I accept the <a href="https://developer.android.com/studio/terms" target="_blank">Android SDK License Agreement</a>
</label>
<div class="invalid-feedback">You must accept the Android SDK License Agreement to proceed.</div>
</div>
<button type="submit" class="btn btn-primary w-100" id="convert-btn" disabled>Convert to APK</button>
</form>
<div id="progress-container" class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Converting...</span>
</div>
<p class="mt-2">Converting your code to APK...</p>
</div>
{% if error %}
<div class="alert alert-danger mt-3" role="alert">
<strong>Error:</strong> {{ error | safe }}
</div>
{% endif %}
{% if apk_path %}
<div class="alert alert-success mt-3" role="alert">
<strong>Success!</strong> APK generated!
<a href="/download/{{ apk_path }}" class="download-link">Download APK</a>
</div>
{% endif %}
{% if logs %}
<div class="mt-3">
<h5>Build Logs</h5>
<pre class="border p-3 bg-light" style="max-height: 300px; overflow-y: auto;">{{ logs | safe }}</pre>
</div>
{% endif %}
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Enable submit button only when license is accepted
const licenseCheckbox = document.getElementById('license_agreement');
const convertBtn = document.getElementById('convert-btn');
licenseCheckbox.addEventListener('change', function() {
convertBtn.disabled = !this.checked;
});
// Form submission with progress indicator
document.getElementById('convert-form').addEventListener('submit', function() {
convertBtn.disabled = true;
document.getElementById('progress-container').style.display = 'block';
});
// Client-side validation for app name
const appNameInput = document.getElementById('app_name');
appNameInput.addEventListener('input', function() {
if (appNameInput.validity.patternMismatch) {
appNameInput.classList.add('is-invalid');
} else {
appNameInput.classList.remove('is-invalid');
}
});
</script>
</body>
</html>
"""
@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/<filename>")
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)