py2apk-compiler 0.1.0__tar.gz
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.
- py2apk_compiler-0.1.0/PKG-INFO +144 -0
- py2apk_compiler-0.1.0/Py2APK/__init__.py +0 -0
- py2apk_compiler-0.1.0/Py2APK/classfile.py +191 -0
- py2apk_compiler-0.1.0/Py2APK/cli.py +159 -0
- py2apk_compiler-0.1.0/Py2APK/cli_build.py +273 -0
- py2apk_compiler-0.1.0/Py2APK/cli_compile.py +93 -0
- py2apk_compiler-0.1.0/Py2APK/cli_fetch_jars.py +158 -0
- py2apk_compiler-0.1.0/Py2APK/cli_fetch_platform.py +115 -0
- py2apk_compiler-0.1.0/Py2APK/cli_init.py +137 -0
- py2apk_compiler-0.1.0/Py2APK/cli_setup.py +208 -0
- py2apk_compiler-0.1.0/Py2APK/compiler_j.py +895 -0
- py2apk_compiler-0.1.0/Py2APK/downloader.py +95 -0
- py2apk_compiler-0.1.0/Py2APK/jar_index.py +361 -0
- py2apk_compiler-0.1.0/Py2APK/maven_resolve.py +316 -0
- py2apk_compiler-0.1.0/README.md +134 -0
- py2apk_compiler-0.1.0/py2apk_compiler.egg-info/PKG-INFO +144 -0
- py2apk_compiler-0.1.0/py2apk_compiler.egg-info/SOURCES.txt +48 -0
- py2apk_compiler-0.1.0/py2apk_compiler.egg-info/dependency_links.txt +1 -0
- py2apk_compiler-0.1.0/py2apk_compiler.egg-info/entry_points.txt +2 -0
- py2apk_compiler-0.1.0/py2apk_compiler.egg-info/requires.txt +2 -0
- py2apk_compiler-0.1.0/py2apk_compiler.egg-info/top_level.txt +1 -0
- py2apk_compiler-0.1.0/pyproject.toml +23 -0
- py2apk_compiler-0.1.0/setup.cfg +4 -0
- py2apk_compiler-0.1.0/setup.py +35 -0
- py2apk_compiler-0.1.0/templates/add2/_template.keep +1 -0
- py2apk_compiler-0.1.0/templates/blank/_template.keep +1 -0
- py2apk_compiler-0.1.0/templates/chart/deps.txt +2 -0
- py2apk_compiler-0.1.0/templates/chart/src/__PACKAGE__/MainActivity.py +33 -0
- py2apk_compiler-0.1.0/templates/chart/target/AndroidManifest.xml +25 -0
- py2apk_compiler-0.1.0/templates/chart/target/apktool.yml +20 -0
- py2apk_compiler-0.1.0/templates/chart/target/res/values/strings.xml +4 -0
- py2apk_compiler-0.1.0/templates/helloworld/src/com/example/helloworld/DialogClickListener.py +18 -0
- py2apk_compiler-0.1.0/templates/helloworld/src/com/example/helloworld/GreetingTextWatcher.py +37 -0
- py2apk_compiler-0.1.0/templates/helloworld/src/com/example/helloworld/MainActivity.py +54 -0
- py2apk_compiler-0.1.0/templates/helloworld/target/AndroidManifest.xml +25 -0
- py2apk_compiler-0.1.0/templates/helloworld/target/apktool.yml +20 -0
- py2apk_compiler-0.1.0/templates/helloworld/target/res/values/strings.xml +4 -0
- py2apk_compiler-0.1.0/templates/weather/deps.txt +2 -0
- py2apk_compiler-0.1.0/templates/weather/src/__PACKAGE__/MainActivity.py +73 -0
- py2apk_compiler-0.1.0/templates/weather/src/__PACKAGE__/RefreshClickListener.py +18 -0
- py2apk_compiler-0.1.0/templates/weather/src/__PACKAGE__/WeatherCallback.py +27 -0
- py2apk_compiler-0.1.0/templates/weather/src/__PACKAGE__/WeatherLocationListener.py +18 -0
- py2apk_compiler-0.1.0/templates/weather/target/AndroidManifest.xml +28 -0
- py2apk_compiler-0.1.0/templates/weather/target/apktool.yml +20 -0
- py2apk_compiler-0.1.0/templates/weather/target/res/values/strings.xml +4 -0
- py2apk_compiler-0.1.0/tests/test_cli.py +164 -0
- py2apk_compiler-0.1.0/tests/test_cli_build.py +181 -0
- py2apk_compiler-0.1.0/tests/test_cli_compile.py +145 -0
- py2apk_compiler-0.1.0/tests/test_cli_setup.py +117 -0
- py2apk_compiler-0.1.0/tests/test_jars.py +548 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: py2apk-compiler
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A CLI tool for building Android APKs from Python source trees
|
|
5
|
+
Project-URL: Homepage, https://github.com/muayyad-alsadi/py2apk
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: PyYAML>=6.0
|
|
9
|
+
Requires-Dist: tqdm>=4.66
|
|
10
|
+
|
|
11
|
+
# Py2APK - A cli tool to create Android Apps APK from python source tree
|
|
12
|
+
|
|
13
|
+
I hate heavy bloated IDEs, I hate Android Studio even more.
|
|
14
|
+
|
|
15
|
+
This is a cli tool that let you write your app in python-like syntax
|
|
16
|
+
Then build your apk directly from that code.
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
pip install py2apk-compiler
|
|
20
|
+
py2apk setup
|
|
21
|
+
py2apk init --template weather --pkg com.example.myweather --name "My First Weather App" myweather
|
|
22
|
+
cd myweather
|
|
23
|
+
py2apk build
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
* `py2apk setup` - set up the stable toolchain JAR files.
|
|
27
|
+
* `py2apk init` - create a new project by copying a packaged template.
|
|
28
|
+
`--icon` is optional. `__PACKAGE__` and `__NAME__` in a template are replaced
|
|
29
|
+
with `--pkg` and `--name`.
|
|
30
|
+
* `py2apk build` - from the project directory, fetch jars when `jars.db` is
|
|
31
|
+
missing, compile changed sources, assemble the APK, and sign it.
|
|
32
|
+
|
|
33
|
+
`fetch_jars`, `fetch_platform`, and `compile` are the same stages `build`
|
|
34
|
+
runs. Use them when you want that stage by itself.
|
|
35
|
+
|
|
36
|
+
* `py2apk fetch_jars` - read `-r` (default `deps.txt`) and download those Maven
|
|
37
|
+
jars plus their runtime transitives. Google Maven and Central are always
|
|
38
|
+
searched. A `deps.txt` line `--extra-index-url URL` adds another repository
|
|
39
|
+
before those two, the same way `requirements.txt` adds an index. `-R URL`
|
|
40
|
+
does that from the command line. The `chart` template uses JitPack that way
|
|
41
|
+
for MPAndroidChart. An `.aar` dependency contributes its `classes.jar`. The
|
|
42
|
+
command also downloads the SDK platform stub for `targetSdkVersion`.
|
|
43
|
+
* `py2apk fetch_platform` - download only that platform stub.
|
|
44
|
+
* `py2apk compile` - find missing or stale Jasmin outputs for Python-like sources.
|
|
45
|
+
|
|
46
|
+
`fetch_jars` caches Maven files under `~/.cache/py2apk/maven/` and the platform
|
|
47
|
+
jar under `~/.cache/py2apk/platforms/<api>/`. Each cached jar gets a sibling
|
|
48
|
+
SQLite file of its public and protected members, in the same directory as the
|
|
49
|
+
Maven `.pom` when the artifact has one. That scan is reused until the jar
|
|
50
|
+
changes. The project `jars/` directory receives dependency symlinks only.
|
|
51
|
+
The platform stub stays in the cache. One merged `jars.db` sits at the project
|
|
52
|
+
root. `jars.kind` is
|
|
53
|
+
`direct`, `transitive`, or `platform`. Compile reads every kind. D8 dexes
|
|
54
|
+
`direct` and `transitive` jars. The platform stub is passed to D8 with
|
|
55
|
+
`--lib` so type lookup succeeds, and it is not written into the dex. The
|
|
56
|
+
toolchain jars from `setup` are not dexed. Downloads use `aiohttp` with five concurrent
|
|
57
|
+
transfers when that package is already installed, and `urllib.request`
|
|
58
|
+
otherwise. `aiohttp` is not a required dependency.
|
|
59
|
+
|
|
60
|
+
`py2apk setup` caches Jasmin and R8 through the Maven cache at
|
|
61
|
+
`~/.cache/py2apk/maven/`. Apktool 3.0.3 and uber-apk-signer 1.3.0 are not
|
|
62
|
+
published to Maven Central, so those two are cached from their GitHub releases
|
|
63
|
+
under `~/.cache/py2apk/toolchain/`. The command symlinks all four into the
|
|
64
|
+
toolchain `jars/` directory. It also creates
|
|
65
|
+
`~/.local/share/py2apk/default.keystore` (alias `py2apk`, RSA 2048, PKCS12,
|
|
66
|
+
10000 days) and writes the password to `default.keystore.pass` beside it.
|
|
67
|
+
`XDG_DATA_HOME` overrides the directory. An existing keystore is left in place.
|
|
68
|
+
|
|
69
|
+
The command interface is available after installation as `py2apk`, or directly
|
|
70
|
+
from the repository root as `python3 -m Py2APK.cli`. `setup` retrieves
|
|
71
|
+
apktool, Jasmin, R8/D8, and uber-apk-signer. Without `--dst`, it finds a writable
|
|
72
|
+
Py2APK asset directory containing `templates/` and creates the sibling `jars/`
|
|
73
|
+
directory. Pass `-d JARS_DIR` or `--dst JARS_DIR` to choose it explicitly.
|
|
74
|
+
`compile` performs incremental source discovery and emits Jasmin for the
|
|
75
|
+
currently supported typed Python-like subset. `build [DIRECTORY]` does the
|
|
76
|
+
full project build: `fetch_jars` when `jars.db` is missing, then compile,
|
|
77
|
+
Jasmin, D8, and apktool, then signs that APK with
|
|
78
|
+
`~/.local/share/py2apk/default.keystore`. The installable file is
|
|
79
|
+
`target/dist/<name>.signed.apk`, for example `weather.signed.apk`. `init`
|
|
80
|
+
requires a destination that does not exist and recursively copies the
|
|
81
|
+
selected template into it. Templates are package data, so
|
|
82
|
+
this also works after `pip install --user py2apk-compiler`. For templates with
|
|
83
|
+
apktool build inputs, `init` also copies `target/AndroidManifest.xml` and
|
|
84
|
+
`target/apktool.yml` to the project root while preserving their modes and
|
|
85
|
+
modification times. Root files and `src/` are authored inputs; `target/` is
|
|
86
|
+
reproducible build output and may be deleted.
|
|
87
|
+
|
|
88
|
+
## How does it work
|
|
89
|
+
|
|
90
|
+
`Py2APK/cli.py` owns argument parsing and dispatch. Each non-trivial command is
|
|
91
|
+
implemented in `Py2APK/cli_<COMMAND>.py`, such as `cli_setup.py`,
|
|
92
|
+
`cli_init.py`, `cli_fetch_jars.py`, `cli_compile.py`, and `cli_build.py`.
|
|
93
|
+
|
|
94
|
+
The command `py2apk compile` scans `src/` for missing or stale Jasmin targets and
|
|
95
|
+
uses Python's built-in AST to emit `.j` files under `target/j/`. Method and
|
|
96
|
+
field signatures come from the project's `jars.db`. The compiler supports the
|
|
97
|
+
small, fully typed language subset used by the bundled templates.
|
|
98
|
+
|
|
99
|
+
`py2apk build [DIRECTORY]` fetches jars when `jars.db` is missing, runs that
|
|
100
|
+
compiler, synchronizes newer authored `AndroidManifest.xml` and `apktool.yml`
|
|
101
|
+
files into `target/`, assembles the Jasmin into JVM classes, converts those
|
|
102
|
+
classes to `classes.dex` with D8, asks apktool for the unsigned APK, and signs
|
|
103
|
+
it with uber-apk-signer. The file to install is `target/dist/<name>.signed.apk`.
|
|
104
|
+
It selects the first existing JAR
|
|
105
|
+
directory from `$PY2APK_JARS`, the project's `jars/`, and the `jars/` sibling of
|
|
106
|
+
source or installed `templates/` assets. A selected directory is expected to
|
|
107
|
+
contain the complete toolchain, including `uber-apk-signer-*.jar`. If it does
|
|
108
|
+
not, the error prints the exact `py2apk setup -d JARS_DIR` recovery command. Set
|
|
109
|
+
`PY2APK_JARS=/path/to/jars` when the JARs live elsewhere.
|
|
110
|
+
|
|
111
|
+
See the [internal documentation](docs/interals.md) for the generated directory
|
|
112
|
+
layout and individual build stages.
|
|
113
|
+
|
|
114
|
+
## Signing development APKs
|
|
115
|
+
|
|
116
|
+
Apktool assembles an APK but does not sign it, and Android will not install an
|
|
117
|
+
unsigned APK. `py2apk build` runs [`uber-apk-signer`](https://github.com/patrickfav/uber-apk-signer)
|
|
118
|
+
on that APK with `~/.local/share/py2apk/default.keystore` and writes
|
|
119
|
+
`target/dist/<name>.signed.apk`. Install that file:
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
adb install -r target/dist/weather.signed.apk
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The same signer invocation, if you need to run it by hand, is:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
java -jar /path/to/uber-apk-signer-1.3.0.jar \
|
|
129
|
+
-a target/dist/helloworld.apk \
|
|
130
|
+
--ks ~/.local/share/py2apk/default.keystore \
|
|
131
|
+
--ksAlias py2apk \
|
|
132
|
+
--ksPass "$(cat ~/.local/share/py2apk/default.keystore.pass)" \
|
|
133
|
+
--ksKeyPass "$(cat ~/.local/share/py2apk/default.keystore.pass)"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
This keystore is a local release-style key, not a store you should publish
|
|
137
|
+
with. Do not commit the keystore or its password file.
|
|
138
|
+
|
|
139
|
+
Android Studio is not required for either building or signing Py2APK projects.
|
|
140
|
+
If a release process uses Google's official tools, install only the Android SDK
|
|
141
|
+
command-line tools and Build Tools. `zipalign` is a platform-specific native
|
|
142
|
+
executable, not a JAR; `apksigner` is Java-based but is normally distributed as
|
|
143
|
+
part of the SDK Build Tools. For lightweight development without an SDK install,
|
|
144
|
+
`uber-apk-signer` bundles the alignment and signing workflow behind one command.
|
|
File without changes
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Parse JVM class files into the members the Jasmin compiler needs.
|
|
2
|
+
|
|
3
|
+
The reader uses only ``struct``. It walks the constant pool and member tables
|
|
4
|
+
and skips attributes by length, so method bodies are never decoded.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import struct
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
ACC_PUBLIC = 0x0001
|
|
14
|
+
ACC_PRIVATE = 0x0002
|
|
15
|
+
ACC_PROTECTED = 0x0004
|
|
16
|
+
ACC_STATIC = 0x0008
|
|
17
|
+
ACC_INTERFACE = 0x0200
|
|
18
|
+
ACC_SYNTHETIC = 0x1000
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class ClassMember:
|
|
23
|
+
owner: str
|
|
24
|
+
name: str
|
|
25
|
+
descriptor: str
|
|
26
|
+
is_static: bool
|
|
27
|
+
opcode: str = ""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class ParsedClass:
|
|
32
|
+
internal: str
|
|
33
|
+
super_internal: str | None
|
|
34
|
+
is_interface: bool
|
|
35
|
+
interfaces: tuple[str, ...]
|
|
36
|
+
methods: tuple[ClassMember, ...]
|
|
37
|
+
fields: tuple[ClassMember, ...]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ClassFileError(ValueError):
|
|
41
|
+
"""Raised when a class file is not readable."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _Reader:
|
|
45
|
+
def __init__(self, data: bytes) -> None:
|
|
46
|
+
self.data = data
|
|
47
|
+
self.offset = 0
|
|
48
|
+
|
|
49
|
+
def u1(self) -> int:
|
|
50
|
+
value = self.data[self.offset]
|
|
51
|
+
self.offset += 1
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
def u2(self) -> int:
|
|
55
|
+
value = struct.unpack_from(">H", self.data, self.offset)[0]
|
|
56
|
+
self.offset += 2
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
def u4(self) -> int:
|
|
60
|
+
value = struct.unpack_from(">I", self.data, self.offset)[0]
|
|
61
|
+
self.offset += 4
|
|
62
|
+
return value
|
|
63
|
+
|
|
64
|
+
def skip(self, count: int) -> None:
|
|
65
|
+
self.offset += count
|
|
66
|
+
|
|
67
|
+
def bytes(self, count: int) -> bytes:
|
|
68
|
+
chunk = self.data[self.offset : self.offset + count]
|
|
69
|
+
self.offset += count
|
|
70
|
+
return chunk
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def parameter_descriptors(params: str) -> tuple[str, ...]:
|
|
74
|
+
index = 0
|
|
75
|
+
found: list[str] = []
|
|
76
|
+
while index < len(params):
|
|
77
|
+
start = index
|
|
78
|
+
index = _skip_field(params, index)
|
|
79
|
+
found.append(params[start:index])
|
|
80
|
+
return tuple(found)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def split_method_descriptor(descriptor: str) -> tuple[str, str]:
|
|
84
|
+
"""Return the concatenated parameter descriptors and the result descriptor."""
|
|
85
|
+
if not descriptor.startswith("(") or ")" not in descriptor:
|
|
86
|
+
raise ClassFileError(f"bad method descriptor: {descriptor}")
|
|
87
|
+
index = 1
|
|
88
|
+
while descriptor[index] != ")":
|
|
89
|
+
index = _skip_field(descriptor, index)
|
|
90
|
+
return descriptor[1:index], descriptor[index + 1 :]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def parse_class(data: bytes) -> ParsedClass:
|
|
94
|
+
reader = _Reader(data)
|
|
95
|
+
if len(data) < 8 or reader.u4() != 0xCAFEBABE:
|
|
96
|
+
raise ClassFileError("class file does not start with 0xCAFEBABE")
|
|
97
|
+
_ = reader.u2()
|
|
98
|
+
_ = reader.u2()
|
|
99
|
+
pool = _constant_pool(reader)
|
|
100
|
+
access = reader.u2()
|
|
101
|
+
internal = _class_name(pool, reader.u2())
|
|
102
|
+
super_index = reader.u2()
|
|
103
|
+
super_internal = _class_name(pool, super_index) if super_index else None
|
|
104
|
+
interfaces = tuple(_class_name(pool, reader.u2()) for _ in range(reader.u2()))
|
|
105
|
+
is_interface = bool(access & ACC_INTERFACE)
|
|
106
|
+
fields = tuple(_members(reader, pool, internal, is_interface, methods=False))
|
|
107
|
+
methods = tuple(_members(reader, pool, internal, is_interface, methods=True))
|
|
108
|
+
return ParsedClass(internal, super_internal, is_interface, interfaces, methods, fields)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _constant_pool(reader: _Reader) -> list[object]:
|
|
112
|
+
count = reader.u2()
|
|
113
|
+
pool: list[object] = [None] * count
|
|
114
|
+
index = 1
|
|
115
|
+
while index < count:
|
|
116
|
+
tag = reader.u1()
|
|
117
|
+
if tag == 1:
|
|
118
|
+
pool[index] = reader.bytes(reader.u2()).decode("utf-8", "surrogateescape")
|
|
119
|
+
elif tag in {7, 8, 16, 19, 20}:
|
|
120
|
+
pool[index] = ("class", reader.u2()) if tag == 7 else reader.u2()
|
|
121
|
+
elif tag in {3, 4, 9, 10, 11, 12, 17, 18}:
|
|
122
|
+
reader.skip(4)
|
|
123
|
+
elif tag in {5, 6}:
|
|
124
|
+
reader.skip(8)
|
|
125
|
+
index += 1
|
|
126
|
+
elif tag == 15:
|
|
127
|
+
reader.skip(3)
|
|
128
|
+
else:
|
|
129
|
+
raise ClassFileError(f"unknown constant-pool tag {tag}")
|
|
130
|
+
index += 1
|
|
131
|
+
return pool
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _class_name(pool: list[object], index: int) -> str:
|
|
135
|
+
entry = pool[index]
|
|
136
|
+
if not isinstance(entry, tuple) or entry[0] != "class":
|
|
137
|
+
raise ClassFileError(f"constant pool {index} is not a class")
|
|
138
|
+
name = pool[entry[1]]
|
|
139
|
+
if not isinstance(name, str):
|
|
140
|
+
raise ClassFileError(f"class name {entry[1]} is not Utf8")
|
|
141
|
+
return name
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _utf8(pool: list[object], index: int) -> str:
|
|
145
|
+
value = pool[index]
|
|
146
|
+
if not isinstance(value, str):
|
|
147
|
+
raise ClassFileError(f"constant pool {index} is not Utf8")
|
|
148
|
+
return value
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _members(
|
|
152
|
+
reader: _Reader,
|
|
153
|
+
pool: list[object],
|
|
154
|
+
owner: str,
|
|
155
|
+
is_interface: bool,
|
|
156
|
+
*,
|
|
157
|
+
methods: bool,
|
|
158
|
+
) -> list[ClassMember]:
|
|
159
|
+
found: list[ClassMember] = []
|
|
160
|
+
for _ in range(reader.u2()):
|
|
161
|
+
access = reader.u2()
|
|
162
|
+
name = _utf8(pool, reader.u2())
|
|
163
|
+
descriptor = _utf8(pool, reader.u2())
|
|
164
|
+
for _attribute in range(reader.u2()):
|
|
165
|
+
_ = reader.u2()
|
|
166
|
+
reader.skip(reader.u4())
|
|
167
|
+
visible = bool(access & (ACC_PUBLIC | ACC_PROTECTED))
|
|
168
|
+
if not visible or access & ACC_SYNTHETIC or access & ACC_PRIVATE:
|
|
169
|
+
continue
|
|
170
|
+
is_static = bool(access & ACC_STATIC)
|
|
171
|
+
opcode = ""
|
|
172
|
+
if methods:
|
|
173
|
+
if is_static:
|
|
174
|
+
opcode = "invokestatic"
|
|
175
|
+
elif name == "<init>":
|
|
176
|
+
opcode = "invokespecial"
|
|
177
|
+
elif is_interface:
|
|
178
|
+
opcode = "invokeinterface"
|
|
179
|
+
else:
|
|
180
|
+
opcode = "invokevirtual"
|
|
181
|
+
found.append(ClassMember(owner, name, descriptor, is_static, opcode))
|
|
182
|
+
return found
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _skip_field(descriptor: str, index: int) -> int:
|
|
186
|
+
while descriptor[index] == "[":
|
|
187
|
+
index += 1
|
|
188
|
+
if descriptor[index] == "L":
|
|
189
|
+
end = descriptor.index(";", index)
|
|
190
|
+
return end + 1
|
|
191
|
+
return index + 1
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Command-line interface for Py2APK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import cast
|
|
9
|
+
|
|
10
|
+
from Py2APK.cli_build import build_project
|
|
11
|
+
from Py2APK.cli_compile import compile_project
|
|
12
|
+
from Py2APK.cli_fetch_jars import fetch_jars
|
|
13
|
+
from Py2APK.cli_fetch_platform import main as fetch_platform_main
|
|
14
|
+
from Py2APK.cli_setup import download_jars
|
|
15
|
+
from Py2APK.cli_init import init_project
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _setup(args: argparse.Namespace) -> int:
|
|
19
|
+
destination_arg = cast(str | None, args.destination)
|
|
20
|
+
destination = None if destination_arg is None else Path(destination_arg)
|
|
21
|
+
return download_jars(destination)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _init(args: argparse.Namespace) -> int:
|
|
25
|
+
icon = None if args.icon is None else str(args.icon)
|
|
26
|
+
return init_project(
|
|
27
|
+
template_name=str(args.template),
|
|
28
|
+
icon=icon,
|
|
29
|
+
package=str(args.package),
|
|
30
|
+
name=str(args.name),
|
|
31
|
+
destination=Path(str(args.directory)),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _fetch_jars(args: argparse.Namespace) -> int:
|
|
36
|
+
repositories = list(args.repositories or [])
|
|
37
|
+
dependency_file = None if args.dependencies is None else Path(args.dependencies)
|
|
38
|
+
return fetch_jars(Path(args.directory).expanduser(), dependency_file, repositories)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _fetch_platform(args: argparse.Namespace) -> int:
|
|
42
|
+
return fetch_platform_main(Path(args.directory).expanduser())
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _compile(_args: argparse.Namespace) -> int:
|
|
46
|
+
return compile_project()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _build(args: argparse.Namespace) -> int:
|
|
50
|
+
return build_project(Path(args.directory).expanduser())
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
54
|
+
parser = argparse.ArgumentParser(
|
|
55
|
+
prog="py2apk",
|
|
56
|
+
description="Build Android APKs from Python source trees.",
|
|
57
|
+
)
|
|
58
|
+
subparsers = parser.add_subparsers(dest="command", metavar="COMMAND")
|
|
59
|
+
|
|
60
|
+
setup_parser = subparsers.add_parser(
|
|
61
|
+
"setup",
|
|
62
|
+
help="set up the required toolchain JAR files",
|
|
63
|
+
)
|
|
64
|
+
_ = setup_parser.add_argument(
|
|
65
|
+
"-d",
|
|
66
|
+
"--dst",
|
|
67
|
+
dest="destination",
|
|
68
|
+
help="destination jars directory (default: detected package assets)",
|
|
69
|
+
)
|
|
70
|
+
setup_parser.set_defaults(handler=_setup)
|
|
71
|
+
|
|
72
|
+
init_parser = subparsers.add_parser(
|
|
73
|
+
"init",
|
|
74
|
+
help="initialize a project from a template",
|
|
75
|
+
)
|
|
76
|
+
init_parser.add_argument(
|
|
77
|
+
"--template",
|
|
78
|
+
default="blank",
|
|
79
|
+
help="project template (default: blank)",
|
|
80
|
+
)
|
|
81
|
+
init_parser.add_argument("--icon", help="path to the app icon")
|
|
82
|
+
init_parser.add_argument(
|
|
83
|
+
"--pkg",
|
|
84
|
+
dest="package",
|
|
85
|
+
required=True,
|
|
86
|
+
help="Android package name",
|
|
87
|
+
)
|
|
88
|
+
init_parser.add_argument("--name", required=True, help="display name of the app")
|
|
89
|
+
init_parser.add_argument("directory", help="directory to create")
|
|
90
|
+
init_parser.set_defaults(handler=_init)
|
|
91
|
+
|
|
92
|
+
compile_parser = subparsers.add_parser(
|
|
93
|
+
"compile",
|
|
94
|
+
help="find Python sources that need Jasmin output",
|
|
95
|
+
)
|
|
96
|
+
compile_parser.set_defaults(handler=_compile)
|
|
97
|
+
|
|
98
|
+
build_parser = subparsers.add_parser(
|
|
99
|
+
"build",
|
|
100
|
+
help="build a project into an APK",
|
|
101
|
+
)
|
|
102
|
+
build_parser.add_argument(
|
|
103
|
+
"directory",
|
|
104
|
+
nargs="?",
|
|
105
|
+
default=".",
|
|
106
|
+
help="project directory (default: current directory)",
|
|
107
|
+
)
|
|
108
|
+
build_parser.set_defaults(handler=_build)
|
|
109
|
+
|
|
110
|
+
fetch_jars_parser = subparsers.add_parser(
|
|
111
|
+
"fetch_jars",
|
|
112
|
+
help="fetch dependency jars and the Android platform jar",
|
|
113
|
+
)
|
|
114
|
+
fetch_jars_parser.add_argument(
|
|
115
|
+
"-r",
|
|
116
|
+
dest="dependencies",
|
|
117
|
+
help="dependency file (default: deps.txt)",
|
|
118
|
+
)
|
|
119
|
+
fetch_jars_parser.add_argument(
|
|
120
|
+
"-R",
|
|
121
|
+
dest="repositories",
|
|
122
|
+
action="append",
|
|
123
|
+
help="extra Maven repository URL, tried before Central (repeatable)",
|
|
124
|
+
)
|
|
125
|
+
fetch_jars_parser.add_argument(
|
|
126
|
+
"directory",
|
|
127
|
+
nargs="?",
|
|
128
|
+
default=".",
|
|
129
|
+
help="project directory (default: current directory)",
|
|
130
|
+
)
|
|
131
|
+
fetch_jars_parser.set_defaults(handler=_fetch_jars)
|
|
132
|
+
|
|
133
|
+
fetch_platform_parser = subparsers.add_parser(
|
|
134
|
+
"fetch_platform",
|
|
135
|
+
help="fetch the Android platform stub jar for targetSdkVersion",
|
|
136
|
+
)
|
|
137
|
+
fetch_platform_parser.add_argument(
|
|
138
|
+
"directory",
|
|
139
|
+
nargs="?",
|
|
140
|
+
default=".",
|
|
141
|
+
help="project directory (default: current directory)",
|
|
142
|
+
)
|
|
143
|
+
fetch_platform_parser.set_defaults(handler=_fetch_platform)
|
|
144
|
+
|
|
145
|
+
return parser
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
149
|
+
parser = create_parser()
|
|
150
|
+
args = parser.parse_args(argv)
|
|
151
|
+
handler = getattr(args, "handler", None)
|
|
152
|
+
if handler is None:
|
|
153
|
+
parser.print_help()
|
|
154
|
+
return 0
|
|
155
|
+
return handler(args)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
raise SystemExit(main())
|