split-dex 0.1.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.
- split_dex/__init__.py +5 -0
- split_dex/class_sets.py +135 -0
- split_dex/cli.py +227 -0
- split_dex/partition.py +229 -0
- split_dex/py.typed +0 -0
- split_dex/registry.py +214 -0
- split_dex/splitter.py +584 -0
- split_dex/strategies/__init__.py +1 -0
- split_dex/strategies/original.py +59 -0
- split_dex/strategies/package_affinity.py +40 -0
- split_dex/strategies/package_order.py +56 -0
- split_dex/strategies/single.py +23 -0
- split_dex/strategy.py +233 -0
- split_dex-0.1.0.dist-info/METADATA +253 -0
- split_dex-0.1.0.dist-info/RECORD +18 -0
- split_dex-0.1.0.dist-info/WHEEL +4 -0
- split_dex-0.1.0.dist-info/entry_points.txt +2 -0
- split_dex-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""The `package-order` strategy: whole packages in name order, without code analysis."""
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
|
|
5
|
+
from split_dex import partition
|
|
6
|
+
from split_dex import strategy
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PackageOrder:
|
|
10
|
+
"""Put whole packages, sorted by name, into files of about `cap` classes; no code analysis."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, *, cap: int = 1000) -> None:
|
|
13
|
+
"""Create the strategy.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
cap: The target number of classes per DEX file.
|
|
17
|
+
|
|
18
|
+
Raises:
|
|
19
|
+
ValueError: If `cap` is below 1.
|
|
20
|
+
"""
|
|
21
|
+
if cap < 1:
|
|
22
|
+
raise ValueError("cap must be >= 1")
|
|
23
|
+
self._cap = cap
|
|
24
|
+
|
|
25
|
+
def plan(self, classes: strategy.Classes) -> list[strategy.Group]:
|
|
26
|
+
"""Sort the classes by package, then by name, and fill files with whole packages.
|
|
27
|
+
|
|
28
|
+
The classes are sorted by package first: sorted by descriptor alone, a package would be
|
|
29
|
+
interleaved with its subpackages (`Lcom/a/z;` sorts after `Lcom/a/m/X;`). Packages next
|
|
30
|
+
to each other in name order are usually parts of one library or feature, so this keeps
|
|
31
|
+
related code together without reading any code: it is fast, and a new version of the app
|
|
32
|
+
moves few classes between files. Each package is a block named after it, so a group that
|
|
33
|
+
goes over a 64K limit is cut at a package boundary.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
classes: The classes to group.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The groups.
|
|
40
|
+
"""
|
|
41
|
+
descriptors = classes.descriptors
|
|
42
|
+
ordered = sorted(
|
|
43
|
+
classes.ids,
|
|
44
|
+
key=lambda class_id: (
|
|
45
|
+
partition.package_of(descriptors[class_id]),
|
|
46
|
+
descriptors[class_id],
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
runs = itertools.groupby(
|
|
50
|
+
ordered, key=lambda class_id: partition.package_of(descriptors[class_id])
|
|
51
|
+
)
|
|
52
|
+
packages = [list(ids) for _, ids in runs]
|
|
53
|
+
return [
|
|
54
|
+
strategy.Group.of_packages(group, descriptors)
|
|
55
|
+
for group in partition.fill_groups(packages, self._cap)
|
|
56
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""The `single` strategy: all the classes in one DEX file."""
|
|
2
|
+
|
|
3
|
+
from split_dex import strategy
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Single:
|
|
7
|
+
"""Put all the classes in one DEX file, cut only where it goes over a 64K limit."""
|
|
8
|
+
|
|
9
|
+
def plan(self, classes: strategy.Classes) -> list[strategy.Group]:
|
|
10
|
+
"""Put every class in one group, in multidex order.
|
|
11
|
+
|
|
12
|
+
Each class is a block of its own, so a group that goes over a 64K limit is cut in the
|
|
13
|
+
middle, and the halves are cut again until each one fits.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
classes: The classes to group.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
One group, or none when the set is empty.
|
|
20
|
+
"""
|
|
21
|
+
if len(classes.ids) == 0:
|
|
22
|
+
return []
|
|
23
|
+
return [strategy.Group.of_classes(classes.ids)]
|
split_dex/strategy.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""The interface between the splitter and the strategies that group classes into DEX files.
|
|
2
|
+
|
|
3
|
+
The splitter sorts the classes of an APK into sets (see `split_dex.class_sets`). For each set, a
|
|
4
|
+
strategy decides how its classes are grouped: it returns groups, and each group becomes one DEX
|
|
5
|
+
file, or several when it goes over a 64K limit of the DEX format.
|
|
6
|
+
|
|
7
|
+
A strategy is a class whose keyword-only constructor parameters are its settings and whose
|
|
8
|
+
`plan` method returns the groups. Built-in strategies are registered in `split_dex.registry`; a
|
|
9
|
+
user's strategy is given on the command line as `FILE.py:CLASS` or `MODULE:CLASS`, or installed
|
|
10
|
+
as a plugin (see `split_dex.registry`).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import collections.abc
|
|
14
|
+
import dataclasses
|
|
15
|
+
import enum
|
|
16
|
+
import itertools
|
|
17
|
+
import typing
|
|
18
|
+
from typing import Self
|
|
19
|
+
|
|
20
|
+
import dexpp
|
|
21
|
+
|
|
22
|
+
import split_dex
|
|
23
|
+
from split_dex import partition
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class StrategyError(split_dex.Error):
|
|
27
|
+
"""Raised when a strategy cannot be created, or returns groups that break the rules."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@enum.unique
|
|
31
|
+
class LinkKind(enum.Enum):
|
|
32
|
+
"""The kinds of links between two classes."""
|
|
33
|
+
|
|
34
|
+
SUPERCLASS = "superclass"
|
|
35
|
+
"""One class is the superclass of the other."""
|
|
36
|
+
INTERFACE = "interface"
|
|
37
|
+
"""One class implements the other, an interface."""
|
|
38
|
+
NESTING = "nesting"
|
|
39
|
+
"""One class is nested in the other: `LFoo$Bar;` is linked to `LFoo;`."""
|
|
40
|
+
CALL = "call"
|
|
41
|
+
"""A method of one class calls a method of the other."""
|
|
42
|
+
FIELD = "field"
|
|
43
|
+
"""A method of one class reads or writes a field of the other."""
|
|
44
|
+
TYPE = "type"
|
|
45
|
+
"""A method of one class uses the other as a type: `new`, a cast, `instanceof`, ..."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclasses.dataclass(frozen=True)
|
|
49
|
+
class Block:
|
|
50
|
+
"""Classes that should stay in the same DEX file.
|
|
51
|
+
|
|
52
|
+
When a group goes over a 64K limit, the splitter cuts it at the block boundary nearest its
|
|
53
|
+
middle. Only a group of a single block is cut inside that block.
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
class_ids: The classes, in write order.
|
|
57
|
+
name: How the warning names the block when it has to be cut, such as
|
|
58
|
+
`package Lcom/a`. None for a block that can be cut without a warning.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
class_ids: tuple[int, ...]
|
|
62
|
+
name: str | None = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclasses.dataclass(frozen=True)
|
|
66
|
+
class Group:
|
|
67
|
+
"""The classes of one DEX file, in write order, split into blocks.
|
|
68
|
+
|
|
69
|
+
Attributes:
|
|
70
|
+
blocks: The blocks, in write order.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
blocks: tuple[Block, ...]
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def of_classes(cls, class_ids: collections.abc.Iterable[int]) -> Self:
|
|
77
|
+
"""A group with one block per class, so that a cut can fall anywhere.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
class_ids: The classes, in write order.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The group.
|
|
84
|
+
"""
|
|
85
|
+
return cls(tuple(Block((class_id,)) for class_id in class_ids))
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def of_packages(
|
|
89
|
+
cls, class_ids: collections.abc.Iterable[int], descriptors: collections.abc.Sequence[str]
|
|
90
|
+
) -> Self:
|
|
91
|
+
"""A group with one block per package, named after it.
|
|
92
|
+
|
|
93
|
+
A group that goes over a 64K limit is then cut at a package boundary, and a package that
|
|
94
|
+
has to be cut is named in the warning.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
class_ids: The classes, in write order. The classes of a package should be next to
|
|
98
|
+
each other: each run of one package's classes becomes a block.
|
|
99
|
+
descriptors: The descriptor of every class, by class id.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
The group.
|
|
103
|
+
"""
|
|
104
|
+
runs = itertools.groupby(
|
|
105
|
+
class_ids, key=lambda class_id: partition.package_of(descriptors[class_id])
|
|
106
|
+
)
|
|
107
|
+
return cls(tuple(Block(tuple(ids), f"package {package}") for package, ids in runs))
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def class_ids(self) -> list[int]:
|
|
111
|
+
"""Every class of the group, in write order."""
|
|
112
|
+
return [class_id for block in self.blocks for class_id in block.class_ids]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class App(typing.Protocol):
|
|
116
|
+
"""What the splitter knows about the APK. Every set shares it."""
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def descriptors(self) -> collections.abc.Sequence[str]:
|
|
120
|
+
"""The descriptor of every class of the APK, by class id."""
|
|
121
|
+
...
|
|
122
|
+
|
|
123
|
+
def source_dex(self, class_id: int) -> int:
|
|
124
|
+
"""The input DEX file a class comes from: 0 for `classes.dex`, 1 for `classes2.dex`, ..."""
|
|
125
|
+
...
|
|
126
|
+
|
|
127
|
+
def links(
|
|
128
|
+
self,
|
|
129
|
+
class_ids: collections.abc.Sequence[int],
|
|
130
|
+
weights: collections.abc.Mapping[LinkKind, int],
|
|
131
|
+
) -> partition.AffinityGraph:
|
|
132
|
+
"""The links among the given classes, each link counted with the weight of its kind."""
|
|
133
|
+
...
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def apk(self) -> dexpp.Apk:
|
|
137
|
+
"""The APK."""
|
|
138
|
+
...
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def analysis(self) -> dexpp.AnalysisContext:
|
|
142
|
+
"""The analysis of the APK's code."""
|
|
143
|
+
...
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class Classes:
|
|
147
|
+
"""A set of classes to group, and what a strategy can know about them.
|
|
148
|
+
|
|
149
|
+
Class ids count every class of the APK, in multidex order, so they index `descriptors`
|
|
150
|
+
directly. A class defined in more than one DEX file is counted once, for its first copy.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def __init__(self, ids: collections.abc.Sequence[int], app: App) -> None:
|
|
154
|
+
"""Create the view of a set of classes.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
ids: The classes of the set, in multidex order.
|
|
158
|
+
app: What the splitter knows about the APK.
|
|
159
|
+
"""
|
|
160
|
+
self._ids = ids
|
|
161
|
+
self._app = app
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def ids(self) -> collections.abc.Sequence[int]:
|
|
165
|
+
"""The classes of the set, in multidex order."""
|
|
166
|
+
return self._ids
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def descriptors(self) -> collections.abc.Sequence[str]:
|
|
170
|
+
"""The descriptor of every class of the APK, by class id."""
|
|
171
|
+
return self._app.descriptors
|
|
172
|
+
|
|
173
|
+
def source_dex(self, class_id: int) -> int:
|
|
174
|
+
"""The input DEX file a class comes from: 0 for `classes.dex`, 1 for `classes2.dex`, ...
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
class_id: The class.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
The position of its DEX file in multidex order.
|
|
181
|
+
"""
|
|
182
|
+
return self._app.source_dex(class_id)
|
|
183
|
+
|
|
184
|
+
def links(
|
|
185
|
+
self, weights: collections.abc.Mapping[LinkKind, int] | None = None
|
|
186
|
+
) -> partition.AffinityGraph:
|
|
187
|
+
"""The links among the classes of the set, counted per pair of classes.
|
|
188
|
+
|
|
189
|
+
The graph is built on each call, from the code of the classes of the set.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
weights: The weight of each kind of link. A kind left out weighs 1, and a kind that
|
|
193
|
+
weighs 0 is ignored. None weighs every link 1.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
The graph, over class ids. The weight of an edge is the sum of the weights of the
|
|
197
|
+
links between its two classes, in both directions.
|
|
198
|
+
"""
|
|
199
|
+
merged = dict.fromkeys(LinkKind, 1)
|
|
200
|
+
if weights is not None:
|
|
201
|
+
merged.update(weights)
|
|
202
|
+
return self._app.links(self._ids, merged)
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def apk(self) -> dexpp.Apk:
|
|
206
|
+
"""The APK, for its manifest, resources or entries."""
|
|
207
|
+
return self._app.apk
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def analysis(self) -> dexpp.AnalysisContext:
|
|
211
|
+
"""The analysis of the APK's code: classes, methods, call graph, class hierarchy, xrefs.
|
|
212
|
+
|
|
213
|
+
It covers every class of the APK, not only the set. It is made on first use and shared by
|
|
214
|
+
every set.
|
|
215
|
+
"""
|
|
216
|
+
return self._app.analysis
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@typing.runtime_checkable
|
|
220
|
+
class Strategy(typing.Protocol):
|
|
221
|
+
"""How the classes of a set are grouped into DEX files."""
|
|
222
|
+
|
|
223
|
+
def plan(self, classes: Classes) -> collections.abc.Sequence[Group]:
|
|
224
|
+
"""Group the classes of a set.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
classes: The classes to group.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
The groups, in the order their DEX files are written. Every class of the set must be
|
|
231
|
+
in exactly one group, and no group or block may be empty.
|
|
232
|
+
"""
|
|
233
|
+
...
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: split-dex
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Split the classes of an Android APK into grouped DEX files
|
|
5
|
+
Project-URL: Homepage, https://github.com/burogurama/split-dex
|
|
6
|
+
Project-URL: Issues, https://github.com/burogurama/split-dex/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/burogurama/split-dex/blob/main/CHANGELOG.md
|
|
8
|
+
Author: Mohamed Nasser
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: android,apk,dex,multidex,split
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.14
|
|
22
|
+
Requires-Dist: dexpp<0.3,>=0.2.0
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# split-dex
|
|
26
|
+
|
|
27
|
+
`split-dex` splits the classes of an Android APK into grouped DEX files.
|
|
28
|
+
|
|
29
|
+
It reads every `classes*.dex` in the APK, sorts the classes into sets, and
|
|
30
|
+
splits each set into DEX files with a strategy. Both are chosen on the command
|
|
31
|
+
line. Every class ends up in exactly one output DEX file, two sets never share a
|
|
32
|
+
file, and maps record where each class went.
|
|
33
|
+
|
|
34
|
+
It is written in Python, on the Python binding of
|
|
35
|
+
[dexpp](https://github.com/burogurama/dexpp), which reads, analyses and writes
|
|
36
|
+
the DEX files.
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
split-dex --apk app.apk --out out/
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
With no other option, every class is in one set, `all`, split by the
|
|
45
|
+
`package-affinity` strategy into files of about 1000 classes.
|
|
46
|
+
|
|
47
|
+
| Option | Meaning |
|
|
48
|
+
| ------ | ------- |
|
|
49
|
+
| `--apk FILE` | Input APK. All its `classes*.dex` entries are read. Required. |
|
|
50
|
+
| `--out DIR` | Output directory. Created if it does not exist. Required. |
|
|
51
|
+
| `--set NAME=STRATEGY[,KEY=VALUE...]` | A set of classes and the strategy that splits it, with the strategy's settings. `STRATEGY` is a built-in or plugin name (`--list-strategies` shows them), or your own class as `MODULE:CLASS` or `FILE.py:CLASS` (see [Writing a strategy](#writing-a-strategy)). Repeat it for more sets. The DEX files are written set by set, in this order. |
|
|
52
|
+
| `--assign PATTERN[,PATTERN...]=NAME` | Send the classes whose names match a pattern to the set `NAME`. Repeat it; each class goes to the first pattern it matches. |
|
|
53
|
+
| `--list-strategies` | Show the strategies and their settings. |
|
|
54
|
+
|
|
55
|
+
With a single `--set`, `--assign` is not needed: every class goes to that set.
|
|
56
|
+
With several, every class must match some pattern; end with `'**=NAME'` to
|
|
57
|
+
catch the rest.
|
|
58
|
+
|
|
59
|
+
A DEX file can reference at most 65,536 methods, 65,536 fields and 65,535
|
|
60
|
+
types. When a group of classes goes over one of these limits, it is cut in two,
|
|
61
|
+
and each half is written again (and cut again if needed). The cut falls nearest
|
|
62
|
+
the middle, between the units the strategy keeps together, such as packages.
|
|
63
|
+
Only a unit too big for one DEX file is cut inside, and `split-dex` prints a
|
|
64
|
+
warning that names it.
|
|
65
|
+
|
|
66
|
+
If the APK defines a class in more than one DEX file, only the first copy in
|
|
67
|
+
multidex order is kept. That is the copy Android loads.
|
|
68
|
+
|
|
69
|
+
### Patterns
|
|
70
|
+
|
|
71
|
+
Patterns match class names such as `com.myapp.Main` or `com.myapp.Outer$Inner`.
|
|
72
|
+
`*` matches any characters except a dot, and `**` matches any characters:
|
|
73
|
+
|
|
74
|
+
| Pattern | Matches |
|
|
75
|
+
| ------- | ------- |
|
|
76
|
+
| `com.myapp.**` | Every class under `com.myapp`, subpackages included. |
|
|
77
|
+
| `com.myapp.*` | The classes directly in `com.myapp`. |
|
|
78
|
+
| `com.myapp.Main` | One class. |
|
|
79
|
+
| `**` | Every class. |
|
|
80
|
+
| `@FILE` | The patterns listed in `FILE`, one per line. Blank lines and lines starting with `#` are skipped. |
|
|
81
|
+
|
|
82
|
+
### Examples
|
|
83
|
+
|
|
84
|
+
Smaller files:
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
split-dex --apk app.apk --out out/ --set all=package-affinity,cap=500
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Split the app's own classes, and put all the library classes in one DEX file
|
|
91
|
+
(cut in halves where it goes over the 64K limits):
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
split-dex --apk app.apk --out out/ \
|
|
95
|
+
--set app=package-affinity,cap=500 \
|
|
96
|
+
--set libraries=single \
|
|
97
|
+
--assign 'com.myapp.**=app' \
|
|
98
|
+
--assign '**=libraries'
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Split the app's own classes, but keep some of its packages with the libraries,
|
|
102
|
+
which stay in their original DEX files (at most 16):
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
split-dex --apk app.apk --out out/ \
|
|
106
|
+
--set app=package-affinity,cap=1000 \
|
|
107
|
+
--set libraries=original,max_files=16 \
|
|
108
|
+
--assign '@startup-packages.txt=libraries' \
|
|
109
|
+
--assign 'com.myapp.**=app' \
|
|
110
|
+
--assign '**=libraries'
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Output
|
|
114
|
+
|
|
115
|
+
In the output directory:
|
|
116
|
+
|
|
117
|
+
| File | Content |
|
|
118
|
+
| ----------- | ------------------------------------------------------- |
|
|
119
|
+
| `<k>.dex` | The DEX files: `0.dex`, `1.dex`, ..., set by set. |
|
|
120
|
+
| `index.csv` | `class,file`: every class descriptor and its DEX file. |
|
|
121
|
+
| `sets.csv` | `file,set`: the set of each DEX file. |
|
|
122
|
+
|
|
123
|
+
Each DEX file is a valid, standalone DEX.
|
|
124
|
+
|
|
125
|
+
Running again into the same directory replaces these files. If an earlier run
|
|
126
|
+
wrote more `<k>.dex` files, the extra ones are removed, so every DEX file in the
|
|
127
|
+
directory is listed in `index.csv`. Other files in the directory are not
|
|
128
|
+
touched.
|
|
129
|
+
|
|
130
|
+
At the end, `split-dex` prints a summary: how many classes were read, how many
|
|
131
|
+
files were written, how many groups had to be cut for the 64K limits, and the
|
|
132
|
+
classes and files of each set.
|
|
133
|
+
|
|
134
|
+
## Install
|
|
135
|
+
|
|
136
|
+
split-dex needs Python 3.14 or newer. Install the `split-dex` command from
|
|
137
|
+
PyPI with [uv](https://docs.astral.sh/uv/) or pip:
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
uv tool install split-dex
|
|
141
|
+
# or
|
|
142
|
+
pip install split-dex
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Its dependency dexpp has prebuilt wheels for Linux x86_64 (glibc 2.28+ and
|
|
146
|
+
musl). On other platforms, the installer builds dexpp from source, which needs
|
|
147
|
+
a C++23 compiler (GCC 13 or newer, or Clang 19 or newer) and CMake 3.18 or
|
|
148
|
+
newer.
|
|
149
|
+
|
|
150
|
+
From a checkout, `uv tool install .` installs the command.
|
|
151
|
+
|
|
152
|
+
## Writing a strategy
|
|
153
|
+
|
|
154
|
+
The command-line options cover the common setups. A strategy of your own is for
|
|
155
|
+
the cases they don't. A strategy is a Python class:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from split_dex import strategy
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class OnePerFile:
|
|
162
|
+
"""Each class in a DEX file of its own, up to `limit` files."""
|
|
163
|
+
|
|
164
|
+
def __init__(self, *, limit: int = 100) -> None:
|
|
165
|
+
self._limit = limit
|
|
166
|
+
|
|
167
|
+
def plan(self, classes: strategy.Classes) -> list[strategy.Group]:
|
|
168
|
+
ids = list(classes.ids)
|
|
169
|
+
groups = [strategy.Group.of_classes([class_id]) for class_id in ids[: self._limit - 1]]
|
|
170
|
+
return groups + [strategy.Group.of_classes(ids[self._limit - 1 :])]
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
- Its keyword-only constructor parameters are its settings. The command line
|
|
174
|
+
converts each `KEY=VALUE` to the parameter's type: int, float, bool, str or
|
|
175
|
+
Path, optionally `| None`.
|
|
176
|
+
- `plan(classes)` receives the classes of one set and returns the groups to
|
|
177
|
+
write, in order. Each group (`strategy.Group`) becomes one DEX file, and is
|
|
178
|
+
made of blocks (`strategy.Block`): classes that should stay in one file.
|
|
179
|
+
When a group goes over a 64K limit, the splitter cuts it at the block
|
|
180
|
+
boundary nearest its middle. `Group.of_classes(ids)` makes one block per
|
|
181
|
+
class.
|
|
182
|
+
- The splitter checks that every class of the set is in exactly one group;
|
|
183
|
+
otherwise the run fails with an error that names the strategy's mistake.
|
|
184
|
+
|
|
185
|
+
What `classes` (`strategy.Classes`) gives:
|
|
186
|
+
|
|
187
|
+
| Member | Meaning |
|
|
188
|
+
| ------ | ------- |
|
|
189
|
+
| `ids` | The class ids of the set, in multidex order. |
|
|
190
|
+
| `descriptors` | The descriptor of every class of the APK, indexed by class id. |
|
|
191
|
+
| `source_dex(id)` | The input DEX file of a class: 0 for `classes.dex`, 1 for `classes2.dex`, ... |
|
|
192
|
+
| `links(weights=None)` | The links among the classes of the set, as a `partition.AffinityGraph`. `weights` maps a `strategy.LinkKind` (superclass, interface, nesting, call, field, type) to its weight; a kind left out weighs 1, and 0 ignores it. |
|
|
193
|
+
| `analysis` | dexpp's `AnalysisContext` for the whole APK: classes, methods, call graph, class hierarchy, cross-references. |
|
|
194
|
+
| `apk` | dexpp's `Apk`: manifest, resources, entries. |
|
|
195
|
+
|
|
196
|
+
There are four ways to use it:
|
|
197
|
+
|
|
198
|
+
- From a file: `--set all=./my_strategy.py:OnePerFile,limit=8`.
|
|
199
|
+
- From a module Python can import: `--set all=my_package.strategies:OnePerFile`.
|
|
200
|
+
- As a plugin. An installed package declares the strategy under the
|
|
201
|
+
entry-point group `split_dex.strategies` in its `pyproject.toml`; it is then
|
|
202
|
+
used by name, and `--list-strategies` shows it:
|
|
203
|
+
|
|
204
|
+
```toml
|
|
205
|
+
[project.entry-points."split_dex.strategies"]
|
|
206
|
+
one-per-file = "my_package.strategies:OnePerFile"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
- Built into split-dex: put the module in `split_dex/strategies/` and add the
|
|
210
|
+
class to `STRATEGIES` in `split_dex/registry.py`.
|
|
211
|
+
|
|
212
|
+
A strategy is ordinary Python code that runs inside split-dex, so only use
|
|
213
|
+
files, modules and plugins you trust.
|
|
214
|
+
|
|
215
|
+
From Python, `split_dex.splitter.split()` takes `split_dex.splitter.Options`,
|
|
216
|
+
whose sets can use any object with a `plan` method.
|
|
217
|
+
|
|
218
|
+
## Development
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
uv sync # .venv with split-dex, dexpp and the dev tools
|
|
222
|
+
uv run pytest # the tests
|
|
223
|
+
uv run ruff format --check # formatting
|
|
224
|
+
uv run ruff check # lint
|
|
225
|
+
uv run mypy # types
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The tests build small APKs in memory. Their classes are copies of the template
|
|
229
|
+
classes in `tests/data/templates.dex`, renamed for each test. The templates are
|
|
230
|
+
compiled from `tests/data/Templates.java`; `tests/data/make_templates.sh`
|
|
231
|
+
rebuilds them (it needs `javac` and the Android SDK's `d8`).
|
|
232
|
+
|
|
233
|
+
To move to a newer dexpp, update its version range in `dependencies` in
|
|
234
|
+
`pyproject.toml` and run `uv lock`.
|
|
235
|
+
|
|
236
|
+
To release: set `version` in `pyproject.toml`, turn the `## Unreleased`
|
|
237
|
+
section of `CHANGELOG.md` into `## <version>`, and push a `v<version>` tag. The
|
|
238
|
+
release workflow builds the wheel and the sdist, checks that the wheel installs
|
|
239
|
+
and runs, creates the GitHub release with that changelog section as notes, and
|
|
240
|
+
uploads the same files to PyPI.
|
|
241
|
+
|
|
242
|
+
## Layout
|
|
243
|
+
|
|
244
|
+
- `split_dex/cli.py` — the command-line interface.
|
|
245
|
+
- `split_dex/splitter.py` — reads the APK, sorts the classes into sets, runs
|
|
246
|
+
each set's strategy, checks its groups, and writes the DEX files and maps.
|
|
247
|
+
- `split_dex/class_sets.py` — sets, patterns, and how classes are assigned.
|
|
248
|
+
- `split_dex/strategy.py` — the interface between the splitter and strategies.
|
|
249
|
+
- `split_dex/registry.py` — finds strategies (built-in, plugin, module or file)
|
|
250
|
+
and creates them from their settings.
|
|
251
|
+
- `split_dex/strategies/` — the built-in strategies.
|
|
252
|
+
- `split_dex/partition.py` — the package-affinity grouping algorithm.
|
|
253
|
+
- `tests/` — the unit tests (pytest).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
split_dex/__init__.py,sha256=yDE--EYxkoM-n19N4EXVitZtitbDlvpHBHu2dbVcdBs,139
|
|
2
|
+
split_dex/class_sets.py,sha256=Ud6qt8p4epXDeUYyoaWTN6AsNYa8Jyaoa1ft5DV3Y8o,4450
|
|
3
|
+
split_dex/cli.py,sha256=ZlvD4usAoGMMADdfV2wEeM6S6moP1Y2gyB1E9STJhog,8468
|
|
4
|
+
split_dex/partition.py,sha256=wlLfSS08Pu92uXgJuwCQfFPoSBWUdxtpyVugYL4mPr4,8677
|
|
5
|
+
split_dex/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
split_dex/registry.py,sha256=hDF5z50G4jjEJshC92OEeiAmk4mp8_MQwdIwDGQRJ5o,8195
|
|
7
|
+
split_dex/splitter.py,sha256=z6AjipuSbwZLuAuHtwZ7_IzxabC84m6hCa4S17kcPew,22462
|
|
8
|
+
split_dex/strategy.py,sha256=-QeGECv1G20QVQuDM_I08bncWyv4x4TcU3Gz-zAUC5I,7801
|
|
9
|
+
split_dex/strategies/__init__.py,sha256=Uo3u3Zt2TKBn_cwXSXPeHx4nDJAs9UUARdgJHPO7Q6w,84
|
|
10
|
+
split_dex/strategies/original.py,sha256=ND9DK9EbYDfuylnd0cs42RxS-n0hi85Bmi2757LOZCI,2252
|
|
11
|
+
split_dex/strategies/package_affinity.py,sha256=dWycvxMUwflDnTwD4043V06gxJa2j52d39mCaCSzDLI,1296
|
|
12
|
+
split_dex/strategies/package_order.py,sha256=KiI0cXbFfR1St846JTiDIqdITL5hRHDuVbuv1mrejZg,2022
|
|
13
|
+
split_dex/strategies/single.py,sha256=6Ih8Jn28BnpO-VcLd8o5Fy1yuNJbavf0fg36gIyln4w,745
|
|
14
|
+
split_dex-0.1.0.dist-info/METADATA,sha256=DFHwfYJCDUPeynDNoWbW04121mZVfq2ZSujMuRdwCXc,10556
|
|
15
|
+
split_dex-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
16
|
+
split_dex-0.1.0.dist-info/entry_points.txt,sha256=qiaGTLQ0TUu5ts-cKQBGIe7XZDyylMP6XsCmFNl6Zbo,49
|
|
17
|
+
split_dex-0.1.0.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
|
|
18
|
+
split_dex-0.1.0.dist-info/RECORD,,
|