py2apk 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.4
2
+ Name: py2apk
3
+ Version: 0.3.0
4
+ Summary: Turn a Python script into an Android APK with a GUI - no Docker, no compiling on your machine
5
+ Author: TwoPlasmaStudios
6
+ License-Expression: LicenseRef-Proprietary
7
+ Project-URL: Homepage, https://github.com/TwoPlasmaStudios/py2apk
8
+ Project-URL: Issues, https://github.com/TwoPlasmaStudios/py2apk/issues
9
+ Keywords: android,apk,python,packaging,flask,webview,gui
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: X11 Applications
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: Microsoft :: Windows
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Software Development :: Build Tools
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Provides-Extra: icon
22
+ Requires-Dist: pillow; extra == "icon"
23
+ Dynamic: license-file
24
+
25
+ # py2apk
26
+
27
+ [Türkçe](README.tr.md)
28
+
29
+ Turn a Python script into an Android APK, from a simple desktop GUI.
30
+ No Docker, no WSL, no compiler on your machine, and it runs on Windows, macOS and Linux.
31
+
32
+ ```
33
+ pip install py2apk
34
+ py2apk
35
+ ```
36
+
37
+ Pick your `.py` file, choose an app type, press **Build APK**. That's it.
38
+
39
+ ## How it works
40
+
41
+ py2apk is split in two parts so that you never have to compile anything:
42
+
43
+ 1. **The factory** (`factory.py` + a GitHub Actions workflow) runs once on GitHub.
44
+ It cross-compiles CPython for Android (arm64-v8a and x86_64), builds the Android
45
+ wheels for a set of popular libraries (Flask, NumPy, pandas, Pillow, ...), and
46
+ publishes everything as a GitHub Release.
47
+ 2. **The tool** (`py2apk.py`, the GUI you run) downloads those ready-made pieces,
48
+ scans your imports, bundles your code and the libraries it needs, writes the
49
+ Android manifest, and packages and signs the APK.
50
+
51
+ The only things downloaded on your machine are a JDK 17 and the Android build-tools
52
+ (if you don't already have them), kept in `~/.py2apk`.
53
+
54
+ ## Install
55
+
56
+ From PyPI (once published):
57
+
58
+ ```
59
+ pip install py2apk
60
+ ```
61
+
62
+ Straight from GitHub (works today):
63
+
64
+ ```
65
+ pip install git+https://github.com/TwoPlasmaStudios/pyapk.git
66
+ ```
67
+
68
+ Requirements: Python 3.10+ with tkinter. The Windows and macOS installers from
69
+ python.org include tkinter. On Debian/Ubuntu: `sudo apt install python3-tk`.
70
+ Optional: `pip install py2apk[icon]` (Pillow) to use JPG/WebP logos and get a sharp preview.
71
+
72
+ ## Use it
73
+
74
+ ```
75
+ py2apk # opens the GUI
76
+ py2apk path/to/app.py # opens the GUI with a file preselected
77
+ ```
78
+
79
+ 1. **Python file**: your entry script. Other `.py` files and packages next to it are
80
+ included automatically, so keep the project in its own folder.
81
+ 2. **App**: name, package name (e.g. `com.me.myapp`), version and an optional logo.
82
+ 3. **App type**: see below.
83
+ 4. **Libraries**: detected from your imports. Each one shows whether a ready-made
84
+ Android build exists. Add extra packages in the *Extra packages* field.
85
+ 5. Press **Build APK**. The first build downloads the JDK and Android tools (several
86
+ hundred MB); later builds are fast. The APK is written next to your script.
87
+
88
+ Install it with `adb install MyApp-1.0.apk` or copy it to the phone and open it
89
+ (allow installs from unknown sources). The APK is signed with a debug key, which is
90
+ fine for testing and sideloading but not for Google Play.
91
+
92
+ ### App types
93
+
94
+ | Type | Status | What you get |
95
+ |---|---|---|
96
+ | **Console** | works | `print()` output is shown on screen. For scripts, bots, calculations. No `input()`. |
97
+ | **Web UI** | works | Run Flask/FastAPI/etc. in your script. The app opens it full screen in a WebView. |
98
+ | **Pygame / Games** | not yet | Needs an SDL2 runtime. |
99
+ | **Kivy** | not yet | Needs an SDL2 runtime. |
100
+
101
+ Tkinter does not exist on Android. Use the **Web UI** type for graphical apps.
102
+
103
+ #### Web UI example
104
+
105
+ ```python
106
+ import os
107
+ from flask import Flask
108
+
109
+ app = Flask(__name__)
110
+
111
+ @app.get("/")
112
+ def index():
113
+ return "<h1>Hello from Android!</h1>"
114
+
115
+ app.run(host="127.0.0.1", port=int(os.environ.get("PORT", 5000)), debug=False)
116
+ ```
117
+
118
+ Keep `debug=False` (the reloader starts a second process, which Android won't allow) and
119
+ listen on `127.0.0.1`. The port is set in the GUI and passed to your code as `PORT`.
120
+ If the server fails to start, the app shows the Python traceback instead of a blank page.
121
+
122
+ ## Libraries
123
+
124
+ The factory builds Android wheels for a fixed list of popular packages
125
+ (see `LIBS` in `factory.py`), plus all of their dependencies. Pure-Python packages
126
+ work as they are. Packages with C extensions only work if an Android build exists.
127
+
128
+ * Packages in the Release are used first.
129
+ * Anything else is tried from PyPI (pure-Python or Android wheels) with `pip`.
130
+ * If nothing is found you get a warning in the log and the package is skipped.
131
+
132
+ Which libraries are available changes as the factory improves: open the
133
+ `index.json` asset of the Release (or look at the status column in the GUI).
134
+ Some heavy packages (SciPy, scikit-learn, OpenCV, anything needing Rust or Fortran
135
+ toolchains) are the hardest to cross-compile and may be marked as failed.
136
+
137
+ ## Run your own factory
138
+
139
+ Fork the repository, enable Actions, give the workflow write access
140
+ (*Settings -> Actions -> General -> Workflow permissions -> Read and write*), then run
141
+ **Actions -> factory -> Run workflow**. When it finishes, a release tagged `cp313` appears.
142
+ Point the tool at your fork:
143
+
144
+ ```
145
+ # Windows (PowerShell)
146
+ $env:PY2APK_REPO = "you/your-fork"
147
+ # macOS / Linux
148
+ export PY2APK_REPO=you/your-fork
149
+ ```
150
+
151
+ Or use *Advanced -> GitHub repository* inside the GUI.
152
+
153
+ ## Environment variables
154
+
155
+ | Variable | Meaning |
156
+ |---|---|
157
+ | `PY2APK_REPO` | GitHub `owner/repo` that hosts the Release (default `TwoPlasmaStudios/pyapk`) |
158
+ | `PY2APK_HOME` | Cache folder (default `~/.py2apk`) |
159
+ | `PY2APK_PY` | Release tag, i.e. the Python version (default `cp313`) |
160
+
161
+ ## Troubleshooting
162
+
163
+ * **"Library index could not be fetched"**: the repository must be public and the factory
164
+ must have finished at least once.
165
+ * **App crashes or shows a traceback**: run `adb logcat -s pyapk` while opening it.
166
+ * **Import error for a native library**: that package has no Android build yet. Check its
167
+ status in the GUI.
168
+ * **Build fails while downloading tools**: check your connection; downloads resume from the cache.
169
+
170
+ ## Limitations
171
+
172
+ * Python 3.13 runtime, arm64-v8a and x86_64 only, minimum Android 7.0 (API 24).
173
+ * No tkinter, pygame or Kivy yet.
174
+ * Console apps have no interactive input.
175
+ * Debug-signed APKs only.
176
+
177
+ ## Publishing to PyPI (maintainers)
178
+
179
+ Releases are published by `.github/workflows/publish.yml` with PyPI Trusted Publishing
180
+ (no API token needed). It only runs for tags that start with `v`, so the `cp313` release
181
+ created by the factory never triggers it.
182
+
183
+ One-time setup:
184
+
185
+ 1. Check that the name is free: https://pypi.org/project/py2apk/ should return 404.
186
+ 2. On pypi.org: *Your projects -> Publishing -> Add a new pending publisher* with
187
+ project `py2apk`, owner `TwoPlasmaStudios`, repository `pyapk`,
188
+ workflow `publish.yml`, environment `pypi`.
189
+ 3. On GitHub: *Settings -> Environments -> New environment* named `pypi`.
190
+
191
+ Each release: bump `version` in `pyproject.toml`, then
192
+
193
+ ```
194
+ git tag v0.3.0
195
+ git push origin v0.3.0
196
+ ```
197
+
198
+ Manual alternative: `pip install build twine`, `python -m build`, `twine upload dist/*`
199
+ (use an API token, username `__token__`).
200
+
201
+ ## License
202
+
203
+ MIT. Libraries in the Release keep their own licenses; CPython is under the PSF license.
@@ -0,0 +1,7 @@
1
+ py2apk.py,sha256=Fh0aK5SHqfqnB2ZDUci93lndIg9CxzSBSxtz834ZZTk,36263
2
+ py2apk-0.3.0.dist-info/licenses/LICENSE,sha256=9TtYJ2FgdCTJPDmIX_SEGjQ4PKGmoclPeDvD6wLFzZk,1188
3
+ py2apk-0.3.0.dist-info/METADATA,sha256=rQ3OLnaQeDvs9YEtQ43oWDFsn0XTFuICyTrXQfe0JfE,7459
4
+ py2apk-0.3.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
5
+ py2apk-0.3.0.dist-info/entry_points.txt,sha256=4YAmgE4UsTNVQ2CijKHCoAnQBAW_g-Bwgl_BUWr_PbI,35
6
+ py2apk-0.3.0.dist-info/top_level.txt,sha256=qDhc3xcs25ljfpUnNwZ5JsUEmgHOg2Q6gsyFrtZvnMY,7
7
+ py2apk-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [gui_scripts]
2
+ py2apk = py2apk:main
@@ -0,0 +1,12 @@
1
+ Copyright (c) 2026 Two Plasma Studios. All rights reserved.
2
+
3
+ CONFIDENTIALITY AND PROPRIETARY NOTICE
4
+
5
+ This software and all associated documentation files (the "Software") are the intellectual property of Two Plasma Studios.
6
+
7
+ Strictly Prohibition on Copying and Distribution:
8
+ 1. No part of this Software may be copied, reproduced, modified, reverse engineered, decompiled, disassembled, or distributed in any form or by any means, electronic or mechanical, without the prior written permission of the copyright owner.
9
+ 2. The Software is provided solely for personal running/execution via official package managers (e.g., pip).
10
+ 3. Commercial use, reselling, or hosting the Software as a service (SaaS) is strictly prohibited.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1 @@
1
+ py2apk
py2apk.py ADDED
@@ -0,0 +1,698 @@
1
+ #!/usr/bin/env python3
2
+ """py2apk - GUI ile Python -> APK (Windows / macOS / Linux).
3
+
4
+ Kullanıcı tarafında DERLEME YOK: CPython runtime'ı ve kütüphane wheel'leri
5
+ GitHub Releases'e önceden derlenip konmuştur (bkz. factory.py). Bu araç yalnızca
6
+ eksik araçları (JDK, Android build-tools) indirir, hazır parçaları çeker ve APK'yı paketler.
7
+
8
+ pip install py2apk -> py2apk
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import ast, json, os, platform, queue, re, shutil, struct, subprocess, sys, tarfile
13
+ import threading, traceback, urllib.parse, urllib.request, zipfile, zlib
14
+ from pathlib import Path
15
+ from xml.sax.saxutils import quoteattr
16
+
17
+ _old, _new = Path.home() / ".pyapk", Path.home() / ".py2apk" # eski önbelleği (JDK/SDK) yeniden indirme
18
+ HOME = Path(os.environ.get("PY2APK_HOME") or (_old if _old.exists() and not _new.exists() else _new))
19
+ REPO = os.environ.get("PY2APK_REPO") or os.environ.get("PYAPK_REPO") or "TwoPlasmaStudios/pyapk"
20
+ PY_TAG = os.environ.get("PY2APK_PY", "cp313") # release etiketi
21
+ ABIS = ("arm64-v8a", "x86_64")
22
+ BT_VER, PLATFORM, TARGET_SDK, MIN_SDK = "34.0.0", "android-34", 34, 24
23
+ CMDLINE = "https://dl.google.com/android/repository/commandlinetools-{os}-11076708_latest.zip"
24
+ SYS = platform.system()
25
+ WIN = SYS == "Windows"
26
+ ARCH = "aarch64" if platform.machine().lower() in ("arm64", "aarch64") else "x86_64"
27
+ EXE, BAT = (".exe", ".bat") if WIN else ("", "")
28
+
29
+ PIPMAP = {"PIL": "pillow", "cv2": "opencv-python-headless", "yaml": "pyyaml", "bs4": "beautifulsoup4",
30
+ "sklearn": "scikit-learn", "dateutil": "python-dateutil", "dotenv": "python-dotenv",
31
+ "OpenSSL": "pyopenssl", "Crypto": "pycryptodome", "serial": "pyserial", "jwt": "pyjwt",
32
+ "attr": "attrs", "skimage": "scikit-image", "git": "gitpython", "magic": "python-magic"}
33
+ UNSUPPORTED = {"tkinter": "Android'de Tk yok (Web arayüzü türünü kullanın)",
34
+ "pygame": "pygame için SDL altyapısı henüz yok", "kivy": "kivy için SDL altyapısı henüz yok"}
35
+ WEB_LIBS = {"flask", "fastapi", "bottle", "django", "quart", "starlette", "uvicorn", "waitress"}
36
+ MODES = [ # (anahtar, başlık, açıklama, destekleniyor)
37
+ ("console", "Konsol", "print() çıktısı ekranda görünür. Betikler, botlar, hesaplamalar.", True),
38
+ ("webview", "Web arayüzü", "Flask / FastAPI + HTML. Uygulama yerel sunucu açar, arayüz tam ekran WebView'da görünür.", True),
39
+ ("pygame", "Pygame / Oyun", "SDL2 altyapısı gerekir. Henüz desteklenmiyor.", False),
40
+ ("kivy", "Kivy", "SDL2 altyapısı gerekir. Henüz desteklenmiyor.", False),
41
+ ]
42
+
43
+
44
+ # --------------------------------------------------------------------------- günlük / hata
45
+ class Fail(Exception):
46
+ pass
47
+
48
+
49
+ _log = print
50
+ def set_log(fn): global _log; _log = fn
51
+ def log(m): _log(m)
52
+ def die(m): raise Fail(m)
53
+ def norm(n): return re.sub(r"[-_.]+", "-", n).lower()
54
+
55
+
56
+ def run(cmd, input=None, **kw):
57
+ cmd = [str(c) for c in cmd]
58
+ log("$ " + " ".join(cmd)[:200])
59
+ if WIN:
60
+ kw["creationflags"] = 0x08000000 # konsol penceresi açma
61
+ p = subprocess.Popen(cmd, stdin=subprocess.PIPE if input else None, stdout=subprocess.PIPE,
62
+ stderr=subprocess.STDOUT, text=True, errors="replace", **kw)
63
+ if input:
64
+ p.stdin.write(input); p.stdin.close()
65
+ for line in p.stdout:
66
+ log(line.rstrip())
67
+ if p.wait():
68
+ die(f"komut başarısız ({p.returncode}): {Path(cmd[0]).name}")
69
+
70
+
71
+ def download(url, dest):
72
+ dest = Path(dest)
73
+ if dest.exists():
74
+ return dest
75
+ dest.parent.mkdir(parents=True, exist_ok=True)
76
+ log(f"indiriliyor: {url}")
77
+ tmp = dest.with_name(dest.name + ".part")
78
+ try:
79
+ with urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": "py2apk"}), timeout=60) as r, \
80
+ open(tmp, "wb") as f:
81
+ total, got, mark = int(r.headers.get("Content-Length") or 0), 0, 0
82
+ while chunk := r.read(1 << 20):
83
+ f.write(chunk); got += len(chunk)
84
+ if total and got - mark >= (16 << 20):
85
+ mark = got; log(f" {got >> 20}/{total >> 20} MB")
86
+ except Exception as e:
87
+ tmp.unlink(missing_ok=True); die(f"indirilemedi: {url} ({e})")
88
+ tmp.rename(dest)
89
+ return dest
90
+
91
+
92
+ def extract(archive, dest):
93
+ dest = Path(dest); dest.mkdir(parents=True, exist_ok=True)
94
+ if str(archive).endswith(".zip"):
95
+ with zipfile.ZipFile(archive) as z:
96
+ for i in z.infolist():
97
+ z.extract(i, dest)
98
+ mode = (i.external_attr >> 16) & 0o7777
99
+ if mode and not WIN:
100
+ os.chmod(dest / i.filename, mode)
101
+ else:
102
+ with tarfile.open(archive) as t:
103
+ t.extractall(dest, filter="tar") if hasattr(tarfile, "data_filter") else t.extractall(dest)
104
+
105
+
106
+ def jenv(jdk):
107
+ e = dict(os.environ)
108
+ e["JAVA_HOME"] = str(jdk)
109
+ e["PATH"] = str(Path(jdk) / "bin") + os.pathsep + e.get("PATH", "")
110
+ return e
111
+
112
+
113
+ # --------------------------------------------------------------------------- araç kurulumu (yalnızca eksikse)
114
+ def ensure_jdk() -> Path:
115
+ def major(javac):
116
+ try:
117
+ p = subprocess.run([str(javac), "-version"], capture_output=True, text=True)
118
+ return int(re.search(r"(\d+)", p.stdout + p.stderr).group(1))
119
+ except Exception:
120
+ return 0
121
+ cands = []
122
+ if os.environ.get("JAVA_HOME"):
123
+ cands.append(Path(os.environ["JAVA_HOME"]) / "bin" / f"javac{EXE}")
124
+ if shutil.which("javac"):
125
+ cands.append(Path(shutil.which("javac")).resolve())
126
+ for c in cands:
127
+ if c.exists() and major(c) >= 17:
128
+ return c.parent.parent
129
+ d = HOME / "jdk"
130
+ found = list(d.glob(f"**/bin/javac{EXE}"))
131
+ if not found:
132
+ osn = {"Windows": "windows", "Darwin": "mac"}.get(SYS, "linux")
133
+ a = "aarch64" if ARCH == "aarch64" else "x64"
134
+ ext = "zip" if WIN else "tar.gz"
135
+ f = download(f"https://api.adoptium.net/v3/binary/latest/17/ga/{osn}/{a}/jdk/hotspot/normal/eclipse",
136
+ HOME / "dl" / f"jdk17-{osn}-{a}.{ext}")
137
+ extract(f, d)
138
+ found = list(d.glob(f"**/bin/javac{EXE}"))
139
+ return found[0].parent.parent
140
+
141
+
142
+ def ensure_sdk(jdk) -> Path:
143
+ sdk = HOME / "sdk"
144
+ sm = sdk / "cmdline-tools" / "latest" / "bin" / f"sdkmanager{BAT}"
145
+ if not sm.exists():
146
+ osn = {"Windows": "win", "Darwin": "mac"}.get(SYS, "linux")
147
+ z = download(CMDLINE.format(os=osn), HOME / "dl" / f"cmdline-tools-{osn}.zip")
148
+ tmp = HOME / "tmp_cmd"; shutil.rmtree(tmp, ignore_errors=True); extract(z, tmp)
149
+ (sdk / "cmdline-tools").mkdir(parents=True, exist_ok=True)
150
+ shutil.move(str(tmp / "cmdline-tools"), str(sm.parent.parent))
151
+ want = []
152
+ if not (sdk / "build-tools" / BT_VER / f"aapt2{EXE}").exists(): want.append(f"build-tools;{BT_VER}")
153
+ if not (sdk / "platforms" / PLATFORM / "android.jar").exists(): want.append(f"platforms;{PLATFORM}")
154
+ if want:
155
+ log("Android SDK lisansları otomatik kabul ediliyor: https://developer.android.com/studio/terms")
156
+ run([sm, f"--sdk_root={sdk}", *want], input="y\n" * 60, env=jenv(jdk))
157
+ return sdk
158
+
159
+
160
+ # --------------------------------------------------------------------------- hazır parçalar (GitHub Releases)
161
+ def asset_url(repo, tag, name):
162
+ return f"https://github.com/{repo}/releases/download/{tag}/{urllib.parse.quote(name)}"
163
+
164
+
165
+ def fetch_index(repo):
166
+ try:
167
+ with urllib.request.urlopen(asset_url(repo, PY_TAG, "index.json"), timeout=30) as r:
168
+ return json.load(r)
169
+ except Exception as e:
170
+ die(f"Kütüphane dizini alınamadı ({repo}, etiket {PY_TAG}): {e}\n"
171
+ "GitHub deposunda 'factory' iş akışının çalışıp release oluşturduğundan emin olun.")
172
+
173
+
174
+ def lib_status(idx, lib):
175
+ if idx is None:
176
+ return "…"
177
+ e = idx["libs"].get(lib)
178
+ if e:
179
+ return "✓ hazır (derlenmiş)" if e["native"] else "✓ hazır (saf Python)"
180
+ if lib in idx.get("failed", {}):
181
+ return "✗ " + idx["failed"][lib][:60]
182
+ return "dizinde yok → pip denenecek"
183
+
184
+
185
+ # --------------------------------------------------------------------------- uygulama içeriği
186
+ def collect(main: Path, extra=()):
187
+ items = []
188
+ for p in sorted(main.parent.iterdir()):
189
+ if p.name.startswith((".", "_pyapk")):
190
+ continue
191
+ if p.is_file() and p.suffix == ".py":
192
+ items.append(p)
193
+ elif p.is_dir() and (p / "__init__.py").exists() and p.name not in {"venv", "env", "build", "dist"}:
194
+ items.append(p)
195
+ return items + [Path(e).resolve() for e in extra]
196
+
197
+
198
+ def detect_libs(items):
199
+ local = {p.stem if p.is_file() else p.name for p in items}
200
+ found = set()
201
+ for p in items:
202
+ for f in ([p] if p.is_file() else p.rglob("*.py")):
203
+ try:
204
+ tree = ast.parse(f.read_text(encoding="utf-8", errors="replace"))
205
+ except (SyntaxError, OSError):
206
+ continue
207
+ for n in ast.walk(tree):
208
+ if isinstance(n, ast.Import):
209
+ found |= {a.name.split(".")[0] for a in n.names}
210
+ elif isinstance(n, ast.ImportFrom) and n.level == 0 and n.module:
211
+ found.add(n.module.split(".")[0])
212
+ for m, why in UNSUPPORTED.items():
213
+ if m in found:
214
+ log(f"UYARI: '{m}' kullanılıyor ama {why}")
215
+ return sorted({norm(PIPMAP.get(m, m)) for m in found - set(sys.stdlib_module_names) - local})
216
+
217
+
218
+ def pip_fallback(libs, site, abi, py):
219
+ cmd = [sys.executable, "-m", "pip", "install", "--target", site, "--only-binary=:all:", "--no-compile",
220
+ "--disable-pip-version-check", "--implementation", "cp", "--python-version", py,
221
+ "--abi", "cp" + py.replace(".", "")]
222
+ api_tag = abi.replace("-", "_")
223
+ for api in range(21, MIN_SDK + 1):
224
+ cmd += ["--platform", f"android_{api}_{api_tag}"]
225
+ for lib in libs: # tek tek: biri olmazsa diğerleri yine gelsin
226
+ try:
227
+ run(cmd + [lib])
228
+ except Fail:
229
+ log(f"UYARI: '{lib}' için Android/saf-Python wheel bulunamadı, atlandı")
230
+
231
+
232
+ def make_icon(icon, dest: Path):
233
+ dest.parent.mkdir(parents=True, exist_ok=True)
234
+ if icon:
235
+ try:
236
+ from PIL import Image
237
+ Image.open(icon).convert("RGBA").resize((192, 192)).save(dest); return
238
+ except ImportError:
239
+ if Path(icon).suffix.lower() != ".png":
240
+ die("PNG olmayan logo için Pillow gerekir: pip install pillow")
241
+ shutil.copy(icon, dest); return
242
+ n = 192
243
+ raw = (b"\x00" + bytes([0x30, 0x6E, 0xB8, 255]) * n) * n
244
+ ch = lambda t, d: struct.pack(">I", len(d)) + t + d + struct.pack(">I", zlib.crc32(t + d))
245
+ dest.write_bytes(b"\x89PNG\r\n\x1a\n" + ch(b"IHDR", struct.pack(">IIBBBBB", n, n, 8, 6, 0, 0, 0))
246
+ + ch(b"IDAT", zlib.compress(raw)) + ch(b"IEND", b""))
247
+
248
+
249
+ def manifest_xml(pkg, name, vname, vcode, mode="console", port=5000):
250
+ return f'''<?xml version="1.0" encoding="utf-8"?>
251
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package={quoteattr(pkg)}
252
+ android:versionCode="{vcode}" android:versionName={quoteattr(vname)}>
253
+ <uses-permission android:name="android.permission.INTERNET"/>
254
+ <application android:label={quoteattr(name)} android:icon="@mipmap/ic_launcher" android:allowBackup="false"
255
+ android:extractNativeLibs="true" android:usesCleartextTraffic="true"
256
+ android:theme="@android:style/Theme.Material.Light.NoActionBar">
257
+ <meta-data android:name="py2apk.mode" android:value={quoteattr(mode)}/>
258
+ <meta-data android:name="py2apk.port" android:value={quoteattr(str(port))}/>
259
+ <activity android:name="pyapk.runtime.MainActivity" android:exported="true"
260
+ android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize|uiMode">
261
+ <intent-filter>
262
+ <action android:name="android.intent.action.MAIN"/>
263
+ <category android:name="android.intent.category.LAUNCHER"/>
264
+ </intent-filter>
265
+ </activity>
266
+ </application>
267
+ </manifest>
268
+ '''
269
+
270
+
271
+ BOOT_PY = r'''import sys, os, runpy, traceback
272
+ sys.stdout = open(1, "w", buffering=1, encoding="utf-8", errors="replace", closefd=False)
273
+ sys.stderr = open(2, "w", buffering=1, encoding="utf-8", errors="replace", closefd=False)
274
+ try:
275
+ import certifi
276
+ os.environ.setdefault("SSL_CERT_FILE", certifi.where())
277
+ except Exception:
278
+ pass
279
+ os.environ["PORT"] = "@PORT@"
280
+ _main = os.path.join(os.path.dirname(os.path.abspath(__file__)), @MAIN@)
281
+ sys.argv = [_main]
282
+ try:
283
+ runpy.run_path(_main, run_name="__main__")
284
+ except SystemExit:
285
+ pass
286
+ except BaseException:
287
+ traceback.print_exc()
288
+ print("\n[program sona erdi]")
289
+ '''
290
+
291
+
292
+ # --------------------------------------------------------------------------- APK üretimi
293
+ def build_apk(c) -> Path:
294
+ """c: script, name, package, icon, version, vcode, abis, libs(list), repo, add(list)"""
295
+ if c.get("mode", "console") not in ("console", "webview"):
296
+ die("Bu uygulama türü henüz desteklenmiyor")
297
+ idx = fetch_index(c["repo"])
298
+ py, tag, bid = idx["py"], idx["tag"], idx["build"]
299
+ for abi in c["abis"]:
300
+ if abi not in idx["abis"]:
301
+ die(f"Release'te {abi} runtime'ı yok")
302
+ src = Path(c["script"]).resolve()
303
+ items = collect(src, c.get("add", []))
304
+ libs = list(dict.fromkeys([*c["libs"], "certifi"]))
305
+ log(f"uygulama: {c['name']} ({c['package']}) ABI: {c['abis']} kütüphaneler: {libs}")
306
+
307
+ jdk = ensure_jdk(); sdk = ensure_sdk(jdk); je = jenv(jdk)
308
+ bt, android_jar = sdk / "build-tools" / BT_VER, sdk / "platforms" / PLATFORM / "android.jar"
309
+ work = HOME / "work" / c["package"]; shutil.rmtree(work, ignore_errors=True); work.mkdir(parents=True)
310
+
311
+ make_icon(c.get("icon"), work / "res" / "mipmap-xxxhdpi" / "ic_launcher.png")
312
+ (work / "AndroidManifest.xml").write_text(manifest_xml(c["package"], c["name"], c["version"], c["vcode"],
313
+ c.get("mode", "console"), c.get("port", 5000)),
314
+ encoding="utf-8")
315
+ run([bt / f"aapt2{EXE}", "compile", "--dir", work / "res", "-o", work / "res.zip"])
316
+ base = work / "base.apk"
317
+ run([bt / f"aapt2{EXE}", "link", "-o", base, "-I", android_jar, "--manifest", work / "AndroidManifest.xml",
318
+ "--min-sdk-version", MIN_SDK, "--target-sdk-version", TARGET_SDK, "--version-code", c["vcode"],
319
+ "--version-name", c["version"], work / "res.zip"])
320
+
321
+ dl = HOME / "dl" / bid
322
+ dex = download(asset_url(c["repo"], tag, f"classes-{tag}.dex"), dl / "classes.dex")
323
+ parts = {}
324
+ for abi in c["abis"]:
325
+ rt = HOME / "runtime" / bid / abi
326
+ if not rt.exists():
327
+ extract(download(asset_url(c["repo"], tag, f"runtime-{tag}-{abi}.zip"), dl / f"runtime-{abi}.zip"), rt)
328
+ root = work / f"home-{abi}"
329
+ shutil.copytree(rt / "home", root)
330
+ site = root / "lib" / f"python{py}" / "site-packages"; site.mkdir(parents=True, exist_ok=True)
331
+ missing, done = [], set()
332
+ for lib in libs:
333
+ ent = idx["libs"].get(lib)
334
+ if not ent or abi not in ent["files"]:
335
+ missing.append(lib); continue
336
+ for fn in ent["files"][abi]:
337
+ if fn not in done:
338
+ done.add(fn)
339
+ with zipfile.ZipFile(download(asset_url(c["repo"], tag, fn), dl / fn)) as z:
340
+ z.extractall(site)
341
+ if missing:
342
+ log(f"dizinde olmayanlar pip ile deneniyor: {missing}")
343
+ pip_fallback(missing, site, abi, py)
344
+ app = root / "app"; app.mkdir()
345
+ for it in items:
346
+ if it.is_dir():
347
+ shutil.copytree(it, app / it.name, ignore=shutil.ignore_patterns("__pycache__"))
348
+ else:
349
+ shutil.copy2(it, app / it.name)
350
+ boot = BOOT_PY.replace("@MAIN@", repr(src.name)).replace("@PORT@", str(c.get("port", 5000)))
351
+ (app / "_pyapk_boot.py").write_text(boot, encoding="utf-8")
352
+ z = work / f"pyhome-{abi}.zip"
353
+ with zipfile.ZipFile(z, "w", zipfile.ZIP_DEFLATED) as zf:
354
+ for p in sorted(root.rglob("*")):
355
+ if p.is_file():
356
+ zf.write(p, p.relative_to(root).as_posix())
357
+ parts[abi] = (sorted((rt / "libs").glob("*.so")), z)
358
+
359
+ with zipfile.ZipFile(base, "a") as z:
360
+ z.write(dex, "classes.dex", zipfile.ZIP_DEFLATED)
361
+ for abi, (sos, home_zip) in parts.items():
362
+ for so in sos:
363
+ z.write(so, f"lib/{abi}/{so.name}", zipfile.ZIP_STORED)
364
+ z.write(home_zip, f"assets/pyhome-{abi}.zip", zipfile.ZIP_STORED)
365
+
366
+ ks = HOME / "debug.keystore"
367
+ if not ks.exists():
368
+ run([jdk / "bin" / f"keytool{EXE}", "-genkeypair", "-keystore", ks, "-storepass", "android",
369
+ "-keypass", "android", "-alias", "pyapk", "-keyalg", "RSA", "-keysize", "2048", "-validity", "10000",
370
+ "-dname", "CN=py2apk"])
371
+ aligned = work / "aligned.apk"
372
+ run([bt / f"zipalign{EXE}", "-f", "-p", "4", base, aligned])
373
+ out = Path(c.get("out") or src.parent / f"{re.sub(r'[^A-Za-z0-9_-]', '_', c['name'])}-{c['version']}.apk")
374
+ run([bt / f"apksigner{BAT}", "sign", "--ks", ks, "--ks-pass", "pass:android", "--key-pass", "pass:android",
375
+ "--out", out, aligned], env=je)
376
+ log(f"HAZIR: {out} ({out.stat().st_size >> 20} MB)")
377
+ return out
378
+
379
+
380
+ # --------------------------------------------------------------------------- GUI
381
+ BG, HEAD, CARD, CARD2 = "#0b1220", "#0f1830", "#141c2f", "#18233b"
382
+ FIELD, LINE, FG, MUTED = "#0c1426", "#26334f", "#e6edf7", "#8b9ab3"
383
+ ACC, OK, WARN, BAD = "#5b8cff", "#34d399", "#fbbf24", "#f87171"
384
+ UI = {"Windows": "Segoe UI", "Darwin": "Helvetica Neue"}.get(SYS, "DejaVu Sans")
385
+ MONO = {"Windows": "Consolas", "Darwin": "Menlo"}.get(SYS, "DejaVu Sans Mono")
386
+
387
+
388
+ def open_folder(p):
389
+ if WIN:
390
+ os.startfile(p) # noqa
391
+ else:
392
+ subprocess.run(["open" if SYS == "Darwin" else "xdg-open", str(p)])
393
+
394
+
395
+ def main(argv=None):
396
+ try:
397
+ import tkinter as tk
398
+ from tkinter import filedialog, messagebox, ttk
399
+ except ImportError:
400
+ sys.exit("tkinter bulunamadı. Linux'ta: sudo apt install python3-tk")
401
+ if WIN:
402
+ try:
403
+ import ctypes
404
+ ctypes.windll.shcore.SetProcessDpiAwareness(1) # net yazı
405
+ except Exception:
406
+ pass
407
+ argv = sys.argv[1:] if argv is None else argv
408
+
409
+ class App(tk.Tk):
410
+ def __init__(self):
411
+ super().__init__()
412
+ self.title("py2apk"); self.geometry("1100x790"); self.minsize(1000, 720); self.configure(bg=BG)
413
+ self.q, self.idx, self.detected, self.photo = queue.Queue(), None, [], None
414
+ self.script, self.name, self.pkg, self.icon = (tk.StringVar() for _ in range(4))
415
+ self.version, self.extra, self.repo = tk.StringVar(value="1.0"), tk.StringVar(), tk.StringVar(value=REPO)
416
+ self.port, self.mode, self.status = tk.StringVar(value="5000"), tk.StringVar(value="console"), \
417
+ tk.StringVar(value="Hazır")
418
+ self.abi = {a: tk.BooleanVar(value=(a == "arm64-v8a")) for a in ABIS}
419
+ self.cards = {}
420
+ self.style_setup(); self.build_ui(); self.set_mode("console")
421
+ self.extra.trace_add("write", lambda *_: self.refresh())
422
+ set_log(lambda m: self.q.put(("log", m)))
423
+ self.after(100, self.pump)
424
+ threading.Thread(target=self.load_index, daemon=True).start()
425
+ if argv and Path(argv[0]).is_file():
426
+ self.script.set(argv[0]); self.on_script()
427
+
428
+ # ---- görünüm
429
+ def style_setup(self):
430
+ s = ttk.Style(self); s.theme_use("clam")
431
+ s.configure(".", background=BG, foreground=FG, font=(UI, 10))
432
+ s.configure("T.TEntry", fieldbackground=FIELD, foreground=FG, bordercolor=LINE, lightcolor=LINE,
433
+ darkcolor=LINE, insertcolor=FG, padding=7)
434
+ s.map("T.TEntry", bordercolor=[("focus", ACC)], lightcolor=[("focus", ACC)], darkcolor=[("focus", ACC)])
435
+ s.configure("T.TButton", background=LINE, foreground=FG, borderwidth=0, focusthickness=0, padding=(14, 7))
436
+ s.map("T.TButton", background=[("active", "#2f3d5e")])
437
+ s.configure("Accent.TButton", background=ACC, foreground="#ffffff", font=(UI, 12, "bold"),
438
+ padding=(18, 12), borderwidth=0, focusthickness=0)
439
+ s.map("Accent.TButton", background=[("disabled", "#26324d"), ("active", "#7aa2ff")],
440
+ foreground=[("disabled", "#7c8aa5")])
441
+ s.configure("T.Horizontal.TProgressbar", troughcolor=FIELD, background=ACC, bordercolor=BG,
442
+ lightcolor=ACC, darkcolor=ACC, thickness=8)
443
+ s.configure("T.Treeview", background=FIELD, fieldbackground=FIELD, foreground=FG, rowheight=27,
444
+ borderwidth=0, font=(UI, 10))
445
+ s.configure("T.Treeview.Heading", background=CARD2, foreground=MUTED, borderwidth=0, relief="flat",
446
+ font=(UI, 9, "bold"))
447
+ s.map("T.Treeview", background=[("selected", "#22304f")])
448
+ s.map("T.Treeview.Heading", background=[("active", CARD2)])
449
+ s.configure("T.TCheckbutton", background=CARD, foreground=FG, font=(UI, 10), focusthickness=0)
450
+ s.map("T.TCheckbutton", background=[("active", CARD)])
451
+ s.configure("Vertical.TScrollbar", background=LINE, troughcolor=FIELD, bordercolor=FIELD,
452
+ arrowcolor=MUTED, relief="flat")
453
+
454
+ def card(self, parent, step, title):
455
+ outer = tk.Frame(parent, bg=CARD, highlightthickness=1, highlightbackground=LINE)
456
+ head = tk.Frame(outer, bg=CARD); head.pack(fill="x", padx=16, pady=(14, 8))
457
+ tk.Label(head, text=str(step), bg=ACC, fg="#fff", font=(UI, 9, "bold"), width=2).pack(side="left")
458
+ tk.Label(head, text=title, bg=CARD, fg=FG, font=(UI, 11, "bold")).pack(side="left", padx=10)
459
+ body = tk.Frame(outer, bg=CARD); body.pack(fill="both", expand=True, padx=16, pady=(0, 14))
460
+ return outer, body
461
+
462
+ def label(self, parent, text, **kw):
463
+ return tk.Label(parent, text=text, bg=kw.pop("bg", CARD), fg=kw.pop("fg", MUTED),
464
+ font=kw.pop("font", (UI, 9)), anchor="w", **kw)
465
+
466
+ def field(self, body, row, text, var, browse=None, width=None):
467
+ self.label(body, text).grid(row=row, column=0, sticky="w", pady=4, padx=(0, 10))
468
+ ttk.Entry(body, textvariable=var, style="T.TEntry", width=width or 30).grid(row=row, column=1,
469
+ sticky="ew", pady=4)
470
+ if browse:
471
+ ttk.Button(body, text="Seç…", style="T.TButton", command=browse).grid(row=row, column=2,
472
+ padx=(8, 0))
473
+
474
+ def build_ui(self):
475
+ hd = tk.Frame(self, bg=HEAD); hd.pack(fill="x")
476
+ inner = tk.Frame(hd, bg=HEAD); inner.pack(fill="x", padx=24, pady=16)
477
+ tk.Label(inner, text="py2", bg=HEAD, fg=ACC, font=(UI, 22, "bold")).pack(side="left")
478
+ tk.Label(inner, text="apk", bg=HEAD, fg=FG, font=(UI, 22, "bold")).pack(side="left")
479
+ tk.Label(inner, text="Python'u Android uygulamasına çevir", bg=HEAD, fg=MUTED,
480
+ font=(UI, 11)).pack(side="left", padx=16, pady=(8, 0))
481
+ tk.Frame(self, bg=LINE, height=1).pack(fill="x")
482
+
483
+ foot = tk.Frame(self, bg=BG); foot.pack(side="bottom", fill="x", padx=20, pady=(6, 16))
484
+ self.btn = ttk.Button(foot, text="APK OLUŞTUR", style="Accent.TButton", command=self.start)
485
+ self.btn.pack(side="left")
486
+ right = tk.Frame(foot, bg=BG); right.pack(side="left", fill="x", expand=True, padx=18)
487
+ tk.Label(right, textvariable=self.status, bg=BG, fg=MUTED, font=(UI, 9), anchor="w").pack(fill="x")
488
+ self.pb = ttk.Progressbar(right, mode="indeterminate", style="T.Horizontal.TProgressbar")
489
+ self.pb.pack(fill="x", pady=(4, 0))
490
+
491
+ body = tk.Frame(self, bg=BG); body.pack(fill="both", expand=True, padx=20, pady=16)
492
+ body.columnconfigure(0, weight=11, uniform="c"); body.columnconfigure(1, weight=9, uniform="c")
493
+ body.rowconfigure(0, weight=1)
494
+ left = tk.Frame(body, bg=BG); left.grid(row=0, column=0, sticky="nsew", padx=(0, 10))
495
+ rt = tk.Frame(body, bg=BG); rt.grid(row=0, column=1, sticky="nsew", padx=(10, 0))
496
+
497
+ # 1 kaynak
498
+ c, b = self.card(left, 1, "Python dosyası"); c.pack(fill="x", pady=(0, 12)); b.columnconfigure(1, weight=1)
499
+ self.field(b, 0, "Giriş dosyası", self.script, self.pick_script)
500
+
501
+ # 2 uygulama bilgisi
502
+ c, b = self.card(left, 2, "Uygulama"); c.pack(fill="x", pady=(0, 12)); b.columnconfigure(1, weight=1)
503
+ self.field(b, 0, "Ad", self.name)
504
+ self.field(b, 1, "Paket adı", self.pkg)
505
+ self.field(b, 2, "Sürüm", self.version)
506
+ self.field(b, 3, "Logo", self.icon, self.pick_icon)
507
+ self.preview = tk.Label(b, text="logo", bg=FIELD, fg=MUTED, width=8, height=4, font=(UI, 8))
508
+ self.preview.grid(row=0, column=3, rowspan=3, padx=(14, 0), sticky="ne")
509
+
510
+ # 3 uygulama türü
511
+ c, b = self.card(left, 3, "Uygulama türü"); c.pack(fill="both", expand=True)
512
+ grid = tk.Frame(b, bg=CARD); grid.pack(fill="x")
513
+ for i in range(2):
514
+ grid.columnconfigure(i, weight=1, uniform="m")
515
+ for n, (key, title, desc, ok) in enumerate(MODES):
516
+ self.make_card(grid, key, title, desc, ok).grid(row=n // 2, column=n % 2, sticky="nsew",
517
+ padx=4, pady=4)
518
+ self.mode_box = tk.Frame(b, bg=CARD); self.mode_box.pack(fill="x", pady=(8, 0))
519
+ self.mode_hint = tk.Label(self.mode_box, bg=CARD, fg=MUTED, font=(UI, 9), justify="left",
520
+ anchor="w", wraplength=470)
521
+ self.mode_hint.pack(fill="x")
522
+ self.port_row = tk.Frame(self.mode_box, bg=CARD)
523
+ self.label(self.port_row, "Sunucu portu").pack(side="left")
524
+ ttk.Entry(self.port_row, textvariable=self.port, style="T.TEntry", width=7).pack(side="left", padx=8)
525
+ self.code_hint = tk.Label(self.mode_box, bg=FIELD, fg="#b9c7e3", font=(MONO, 9), justify="left",
526
+ anchor="w", padx=10, pady=8,
527
+ text="import os\napp.run(host=\"127.0.0.1\",\n port=int(os.environ.get(\"PORT\", 5000)),\n debug=False)")
528
+
529
+ # 4 kütüphaneler
530
+ c, b = self.card(rt, 4, "Kütüphaneler"); c.pack(fill="x", pady=(0, 12))
531
+ self.tree = ttk.Treeview(b, columns=("lib", "durum"), show="headings", height=5, style="T.Treeview")
532
+ self.tree.heading("lib", text="Algılanan", anchor="w"); self.tree.heading("durum", text="Durum", anchor="w")
533
+ self.tree.column("lib", width=150); self.tree.column("durum", width=260)
534
+ for t, col in (("ok", OK), ("warn", WARN), ("bad", BAD)):
535
+ self.tree.tag_configure(t, foreground=col)
536
+ self.tree.pack(fill="x")
537
+ r = tk.Frame(b, bg=CARD); r.pack(fill="x", pady=(10, 0)); r.columnconfigure(1, weight=1)
538
+ self.field(r, 0, "Ek paketler", self.extra, width=20)
539
+ ab = tk.Frame(b, bg=CARD); ab.pack(fill="x", pady=(8, 0))
540
+ for a, v in self.abi.items():
541
+ ttk.Checkbutton(ab, text=a + (" · telefon" if a == "arm64-v8a" else " · emülatör"), variable=v,
542
+ style="T.TCheckbutton").pack(side="left", padx=(0, 16))
543
+ adv = tk.Label(b, text="Gelişmiş ▸", bg=CARD, fg=MUTED, font=(UI, 9), cursor="hand2")
544
+ adv.pack(anchor="w", pady=(8, 0))
545
+ self.adv_row = tk.Frame(b, bg=CARD); self.adv_row.columnconfigure(1, weight=1)
546
+ self.field(self.adv_row, 0, "GitHub deposu", self.repo, width=20)
547
+ adv.bind("<Button-1>", lambda e: (self.adv_row.pack_forget() if self.adv_row.winfo_ismapped()
548
+ else self.adv_row.pack(fill="x")))
549
+
550
+ # günlük
551
+ c, b = self.card(rt, "›", "Günlük"); c.pack(fill="both", expand=True)
552
+ wrap = tk.Frame(b, bg="#070c16"); wrap.pack(fill="both", expand=True)
553
+ self.logw = tk.Text(wrap, height=8, state="disabled", bg="#070c16", fg="#c9d4e8", relief="flat",
554
+ font=(MONO, 9), wrap="word", padx=10, pady=8, insertbackground=FG,
555
+ highlightthickness=0)
556
+ sb = ttk.Scrollbar(wrap, command=self.logw.yview)
557
+ self.logw["yscrollcommand"] = sb.set
558
+ sb.pack(side="right", fill="y"); self.logw.pack(side="left", fill="both", expand=True)
559
+ self.logw.tag_configure("cmd", foreground="#6b7a99"); self.logw.tag_configure("warn", foreground=WARN)
560
+ self.logw.tag_configure("good", foreground=OK, font=(MONO, 9, "bold"))
561
+ self.logw.tag_configure("bad", foreground=BAD)
562
+
563
+ def make_card(self, parent, key, title, desc, ok):
564
+ fr = tk.Frame(parent, bg=CARD2, highlightthickness=2, highlightbackground=LINE,
565
+ cursor="hand2" if ok else "arrow")
566
+ top = tk.Frame(fr, bg=CARD2); top.pack(fill="x", padx=10, pady=(10, 2))
567
+ t = tk.Label(top, text=title, bg=CARD2, fg=FG if ok else MUTED, font=(UI, 11, "bold"), anchor="w")
568
+ t.pack(side="left")
569
+ if not ok:
570
+ tk.Label(top, text="YAKINDA", bg=WARN, fg="#1a1300", font=(UI, 7, "bold"), padx=5).pack(side="right")
571
+ d = tk.Label(fr, text=desc, bg=CARD2, fg=MUTED, font=(UI, 9), justify="left", anchor="nw",
572
+ wraplength=210, height=4)
573
+ d.pack(fill="x", padx=10, pady=(0, 10))
574
+ if ok:
575
+ for w in (fr, top, t, d):
576
+ w.bind("<Button-1>", lambda e, k=key: self.set_mode(k))
577
+ self.cards[key] = fr
578
+ return fr
579
+
580
+ def set_mode(self, key):
581
+ self.mode.set(key)
582
+ for k, fr in self.cards.items():
583
+ fr.configure(highlightbackground=ACC if k == key else LINE)
584
+ if key == "webview":
585
+ self.mode_hint.configure(text="Kodunuz bir web sunucusu başlatmalı (debug kapalı). "
586
+ "Hazır olunca arayüz uygulamada tam ekran açılır.")
587
+ self.port_row.pack(anchor="w", pady=(8, 0)); self.code_hint.pack(fill="x", pady=(8, 0))
588
+ else:
589
+ self.mode_hint.configure(text="Çıktı ekranda metin olarak görünür. Etkileşimli girdi (input) yok.")
590
+ self.port_row.pack_forget(); self.code_hint.pack_forget()
591
+
592
+ # ---- olaylar
593
+ def pick_script(self):
594
+ p = filedialog.askopenfilename(title="Python dosyası", filetypes=[("Python", "*.py")])
595
+ if p:
596
+ self.script.set(p); self.on_script()
597
+
598
+ def pick_icon(self):
599
+ p = filedialog.askopenfilename(title="Logo", filetypes=[("Resim", "*.png *.jpg *.jpeg *.webp")])
600
+ if not p:
601
+ return
602
+ self.icon.set(p)
603
+ try:
604
+ try:
605
+ from PIL import Image, ImageTk
606
+ self.photo = ImageTk.PhotoImage(Image.open(p).convert("RGBA").resize((64, 64)))
607
+ except ImportError:
608
+ img = tk.PhotoImage(file=p)
609
+ self.photo = img.subsample(max(1, img.width() // 64), max(1, img.height() // 64))
610
+ self.preview.configure(image=self.photo, text="", width=64, height=64)
611
+ except Exception:
612
+ self.preview.configure(image="", text="logo")
613
+
614
+ def on_script(self):
615
+ p = Path(self.script.get())
616
+ stem = re.sub(r"[^a-z0-9_]", "_", p.stem.lower()) or "app"
617
+ self.name.set(p.stem.replace("_", " ").title()); self.pkg.set(f"com.py2apk.{stem}")
618
+ try:
619
+ self.detected = detect_libs(collect(p))
620
+ except Exception as e:
621
+ self.detected = []; log(f"tarama hatası: {e}")
622
+ if WEB_LIBS & set(self.detected):
623
+ self.set_mode("webview")
624
+ log("Web çatısı algılandı → uygulama türü 'Web arayüzü' seçildi")
625
+ self.refresh()
626
+
627
+ def load_index(self):
628
+ try:
629
+ self.idx = fetch_index(self.repo.get())
630
+ except Fail as e:
631
+ self.q.put(("log", f"UYARI: {e}"))
632
+ self.q.put(("refresh", None))
633
+
634
+ def libs(self):
635
+ extra = [norm(x) for x in re.split(r"[,\s]+", self.extra.get()) if x]
636
+ return list(dict.fromkeys([*self.detected, *extra]))
637
+
638
+ def refresh(self):
639
+ self.tree.delete(*self.tree.get_children())
640
+ for lib in self.libs():
641
+ st = lib_status(self.idx, lib)
642
+ self.tree.insert("", "end", values=(lib, st),
643
+ tags=("ok" if st.startswith("✓") else "bad" if st.startswith("✗") else "warn",))
644
+
645
+ def start(self):
646
+ abis = [a for a, v in self.abi.items() if v.get()]
647
+ if not Path(self.script.get()).is_file():
648
+ return messagebox.showerror("py2apk", "Önce bir .py dosyası seçin")
649
+ if not re.fullmatch(r"[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+", self.pkg.get()):
650
+ return messagebox.showerror("py2apk", "Geçersiz paket adı (örn. com.ben.uygulama)")
651
+ if not abis:
652
+ return messagebox.showerror("py2apk", "En az bir mimari seçin")
653
+ if not self.port.get().isdigit() or not (1024 <= int(self.port.get()) <= 65535):
654
+ return messagebox.showerror("py2apk", "Port 1024-65535 arasında olmalı")
655
+ cfg = dict(script=self.script.get(), name=self.name.get() or "App", package=self.pkg.get(),
656
+ icon=self.icon.get() or None, version=self.version.get() or "1.0", vcode=1, abis=abis,
657
+ libs=self.libs(), repo=self.repo.get(), mode=self.mode.get(), port=int(self.port.get()))
658
+ self.btn.state(["disabled"]); self.pb.start(12); self.status.set("Başladı…")
659
+
660
+ def work():
661
+ try:
662
+ self.q.put(("done", str(build_apk(cfg))))
663
+ except Fail as e:
664
+ self.q.put(("err", str(e)))
665
+ except Exception:
666
+ self.q.put(("err", traceback.format_exc()))
667
+ threading.Thread(target=work, daemon=True).start()
668
+
669
+ def pump(self):
670
+ try:
671
+ while True:
672
+ kind, val = self.q.get_nowait()
673
+ if kind == "log":
674
+ tag = ("cmd" if val.startswith("$ ") else "warn" if val.startswith("UYARI")
675
+ else "good" if val.startswith("HAZIR") else "")
676
+ self.logw.configure(state="normal"); self.logw.insert("end", val + "\n", tag)
677
+ self.logw.see("end"); self.logw.configure(state="disabled")
678
+ if val.strip():
679
+ self.status.set(val.strip()[:100])
680
+ elif kind == "refresh":
681
+ self.refresh()
682
+ else:
683
+ self.pb.stop(); self.btn.state(["!disabled"])
684
+ if kind == "err":
685
+ self.status.set("Hata"); messagebox.showerror("py2apk - hata", val[-1500:])
686
+ else:
687
+ self.status.set("Tamamlandı ✓")
688
+ if messagebox.askyesno("py2apk", f"APK hazır:\n{val}\n\nKlasörü açayım mı?"):
689
+ open_folder(Path(val).parent)
690
+ except queue.Empty:
691
+ pass
692
+ self.after(100, self.pump)
693
+
694
+ App().mainloop()
695
+
696
+
697
+ if __name__ == "__main__":
698
+ main()