fluxgram-plugin-devkit 1.0.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.
- fluxgram_devkit/__init__.py +6 -0
- fluxgram_devkit/artifacts/api.json +10 -0
- fluxgram_devkit/artifacts/fluxgram-java-api-1.19.jar +0 -0
- fluxgram_devkit/cli.py +703 -0
- fluxgram_devkit/templates/python-java/fluxgram-plugin.toml +22 -0
- fluxgram_devkit/templates/python-java/java/src/example/plugin/Module.java +26 -0
- fluxgram_devkit/templates/python-java/plugin/main.py +11 -0
- fluxgram_plugin_devkit-1.0.0.dist-info/METADATA +18 -0
- fluxgram_plugin_devkit-1.0.0.dist-info/RECORD +12 -0
- fluxgram_plugin_devkit-1.0.0.dist-info/WHEEL +5 -0
- fluxgram_plugin_devkit-1.0.0.dist-info/entry_points.txt +2 -0
- fluxgram_plugin_devkit-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"api_version": 1,
|
|
3
|
+
"api_minor": 19,
|
|
4
|
+
"min_sdk": 24,
|
|
5
|
+
"compile_sdk": 36,
|
|
6
|
+
"sha256": "a2856a6ffa4ce33fb7617224fd911e411032ad68200d25779ab1d809176bde62",
|
|
7
|
+
"source_sha256": "163945fc56726f8b35b1f83951bcf13b454a9adb41850ff09e8a6224fe5a6258",
|
|
8
|
+
"host_validator_sha256": "5eb46b9c16c0b306d9ab20154dd01cc3037b18f5c71725e9bda7ce4f45dff066",
|
|
9
|
+
"source_commit": "8147531c0"
|
|
10
|
+
}
|
|
Binary file
|
fluxgram_devkit/cli.py
ADDED
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import hashlib
|
|
3
|
+
import io
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
import stat
|
|
10
|
+
import struct
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
import tomllib
|
|
15
|
+
import zipfile
|
|
16
|
+
import zlib
|
|
17
|
+
|
|
18
|
+
from . import JAVA_API_MINOR, JAVA_API_VERSION, SDK_MINOR, SDK_VERSION
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
MAX_ARCHIVE_BYTES = 8 * 1024 * 1024
|
|
22
|
+
MAX_UNPACKED_BYTES = 32 * 1024 * 1024
|
|
23
|
+
MAX_FILE_BYTES = 8 * 1024 * 1024
|
|
24
|
+
MAX_MANIFEST_BYTES = 64 * 1024
|
|
25
|
+
MAX_ENTRIES = 512
|
|
26
|
+
DEX_MAGICS = {b"dex\n035\0", b"dex\n037\0", b"dex\n038\0", b"dex\n039\0"}
|
|
27
|
+
PATH_PART = re.compile(r"[A-Za-z0-9_.-]+")
|
|
28
|
+
PLUGIN_ID = re.compile(r"[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+")
|
|
29
|
+
PLUGIN_VERSION = re.compile(r"(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)")
|
|
30
|
+
PYTHON_ENTRYPOINT = re.compile(r"[A-Za-z_][A-Za-z0-9_]*:PluginEntry")
|
|
31
|
+
JAVA_ENTRYPOINT = re.compile(r"[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_$]*)+")
|
|
32
|
+
HASH = re.compile(r"[a-f0-9]{64}")
|
|
33
|
+
FIXED_TIME = (2020, 1, 1, 0, 0, 0)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DevkitError(Exception):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def require(condition, message):
|
|
41
|
+
if not condition:
|
|
42
|
+
raise DevkitError(message)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def strict_object(pairs):
|
|
46
|
+
value = {}
|
|
47
|
+
|
|
48
|
+
for key, item in pairs:
|
|
49
|
+
require(key not in value, f"Duplicate JSON field: {key}")
|
|
50
|
+
value[key] = item
|
|
51
|
+
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def read_json(data):
|
|
56
|
+
try:
|
|
57
|
+
return json.loads(data.decode("utf-8"), object_pairs_hook=strict_object)
|
|
58
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
59
|
+
raise DevkitError("Invalid manifest JSON") from error
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def validate_string(value, name, limit):
|
|
63
|
+
require(isinstance(value, str), f"{name} must be a string")
|
|
64
|
+
require(value.strip() and len(value) <= limit, f"Invalid {name}")
|
|
65
|
+
|
|
66
|
+
for character in value:
|
|
67
|
+
code = ord(character)
|
|
68
|
+
require(character in "\n\t" or code >= 32 and code != 127, f"Invalid control character in {name}")
|
|
69
|
+
require(not 0x202A <= code <= 0x202E and not 0x2066 <= code <= 0x2069,
|
|
70
|
+
f"Bidirectional controls are not allowed in {name}")
|
|
71
|
+
|
|
72
|
+
return value
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def validate_integer(value, name):
|
|
76
|
+
require(type(value) is int and 0 <= value <= 2_147_483_647,
|
|
77
|
+
f"{name} must be a non-negative 32-bit integer")
|
|
78
|
+
return value
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def validate_path(name, directory=False):
|
|
82
|
+
require(isinstance(name, str) and 0 < len(name) <= 240, "Invalid ZIP path length")
|
|
83
|
+
require("\\" not in name and not name.startswith("/"), f"Unsafe ZIP path: {name}")
|
|
84
|
+
path = name[:-1] if directory and name.endswith("/") else name
|
|
85
|
+
parts = path.split("/")
|
|
86
|
+
require(path and len(parts) <= 16, f"Unsafe ZIP path: {name}")
|
|
87
|
+
|
|
88
|
+
for part in parts:
|
|
89
|
+
require(PATH_PART.fullmatch(part) is not None and part not in {".", ".."}
|
|
90
|
+
and not part.endswith("."), f"Unsafe ZIP path: {name}")
|
|
91
|
+
|
|
92
|
+
require(parts[0].lower() not in {"fluxgram", "fluxgram.py", ".fluxgram-source.fgplugin"},
|
|
93
|
+
f"Reserved package path: {name}")
|
|
94
|
+
return path
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def validate_manifest(manifest):
|
|
98
|
+
require(isinstance(manifest, dict), "Manifest must be an object")
|
|
99
|
+
base = {
|
|
100
|
+
"schema_version", "sdk_version", "min_sdk_minor", "id", "name", "version",
|
|
101
|
+
"description", "author", "entrypoint", "access", "declared_capabilities",
|
|
102
|
+
}
|
|
103
|
+
optional = {"icon", "java_module"}
|
|
104
|
+
require(set(manifest).issubset(base | optional), "Unknown manifest field")
|
|
105
|
+
require(base.issubset(manifest), "Missing manifest field")
|
|
106
|
+
schema = validate_integer(manifest["schema_version"], "schema_version")
|
|
107
|
+
require(schema in {2, 3}, "Only manifest schema 2 and 3 are supported")
|
|
108
|
+
require(validate_integer(manifest["sdk_version"], "sdk_version") == SDK_VERSION,
|
|
109
|
+
f"SDK major must be {SDK_VERSION}")
|
|
110
|
+
require(validate_integer(manifest["min_sdk_minor"], "min_sdk_minor") <= SDK_MINOR,
|
|
111
|
+
f"Plugin requires a newer SDK than {SDK_VERSION}.{SDK_MINOR}")
|
|
112
|
+
plugin_id = validate_string(manifest["id"], "id", 100)
|
|
113
|
+
require(PLUGIN_ID.fullmatch(plugin_id) is not None and not plugin_id.startswith("fluxgram."),
|
|
114
|
+
"Invalid or reserved plugin id")
|
|
115
|
+
validate_string(manifest["name"], "name", 80)
|
|
116
|
+
version = validate_string(manifest["version"], "version", 64)
|
|
117
|
+
require(PLUGIN_VERSION.fullmatch(version) is not None, "Version must have the form major.minor.patch")
|
|
118
|
+
validate_string(manifest["description"], "description", 2000)
|
|
119
|
+
validate_string(manifest["author"], "author", 100)
|
|
120
|
+
entrypoint = validate_string(manifest["entrypoint"], "entrypoint", 128)
|
|
121
|
+
require(PYTHON_ENTRYPOINT.fullmatch(entrypoint) is not None
|
|
122
|
+
and entrypoint != "fluxgram:PluginEntry", "Invalid Python entrypoint")
|
|
123
|
+
require(manifest["access"] == "full", "Only full access is supported")
|
|
124
|
+
capabilities = manifest["declared_capabilities"]
|
|
125
|
+
require(isinstance(capabilities, list) and len(capabilities) <= 32,
|
|
126
|
+
"declared_capabilities must be an array of at most 32 strings")
|
|
127
|
+
seen = set()
|
|
128
|
+
|
|
129
|
+
for capability in capabilities:
|
|
130
|
+
value = validate_string(capability, "declared capability", 160)
|
|
131
|
+
require(value not in seen, "Duplicate declared capability")
|
|
132
|
+
seen.add(value)
|
|
133
|
+
|
|
134
|
+
if "icon" in manifest:
|
|
135
|
+
icon = validate_path(validate_string(manifest["icon"], "icon", 240))
|
|
136
|
+
require(icon.lower().endswith((".png", ".webp")), "Icon must be PNG or WebP")
|
|
137
|
+
|
|
138
|
+
java = manifest.get("java_module")
|
|
139
|
+
require((schema == 3) == (java is not None), "java_module is required only for schema 3")
|
|
140
|
+
|
|
141
|
+
if java is not None:
|
|
142
|
+
require(isinstance(java, dict), "java_module must be an object")
|
|
143
|
+
fields = {"path", "entrypoint", "sha256", "api_version", "min_api_minor"}
|
|
144
|
+
require(set(java) == fields, "Incomplete or unknown java_module field")
|
|
145
|
+
dex_path = validate_path(validate_string(java["path"], "Java module path", 240))
|
|
146
|
+
require(dex_path.endswith(".dex"), "Java module must be a DEX file")
|
|
147
|
+
java_entrypoint = validate_string(java["entrypoint"], "Java entrypoint", 240)
|
|
148
|
+
require(JAVA_ENTRYPOINT.fullmatch(java_entrypoint) is not None, "Invalid Java entrypoint")
|
|
149
|
+
require(HASH.fullmatch(validate_string(java["sha256"], "DEX SHA-256", 64)) is not None,
|
|
150
|
+
"Invalid DEX SHA-256")
|
|
151
|
+
require(validate_integer(java["api_version"], "Java API version") == JAVA_API_VERSION,
|
|
152
|
+
f"Java API major must be {JAVA_API_VERSION}")
|
|
153
|
+
require(validate_integer(java["min_api_minor"], "Java API minor") <= JAVA_API_MINOR,
|
|
154
|
+
f"Plugin requires a newer Java API than {JAVA_API_VERSION}.{JAVA_API_MINOR}")
|
|
155
|
+
|
|
156
|
+
return manifest
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def validate_dex(data, java):
|
|
160
|
+
require(len(data) >= 112 and data[:8] in DEX_MAGICS, "Unsupported or truncated DEX")
|
|
161
|
+
require(hashlib.sha256(data).hexdigest() == java["sha256"], "DEX SHA-256 mismatch")
|
|
162
|
+
file_size, header_size, endian = struct.unpack_from("<III", data, 32)
|
|
163
|
+
require(file_size == len(data) and header_size == 112 and endian == 0x12345678,
|
|
164
|
+
"Invalid DEX header")
|
|
165
|
+
strings, strings_offset = struct.unpack_from("<II", data, 56)
|
|
166
|
+
types, types_offset = struct.unpack_from("<II", data, 64)
|
|
167
|
+
classes, classes_offset = struct.unpack_from("<II", data, 96)
|
|
168
|
+
|
|
169
|
+
def table(offset, count, width):
|
|
170
|
+
require(offset >= 112 and count > 0 and offset + count * width <= len(data),
|
|
171
|
+
"Invalid DEX table")
|
|
172
|
+
|
|
173
|
+
table(strings_offset, strings, 4)
|
|
174
|
+
table(types_offset, types, 4)
|
|
175
|
+
table(classes_offset, classes, 32)
|
|
176
|
+
expected = "L" + java["entrypoint"].replace(".", "/") + ";"
|
|
177
|
+
found = False
|
|
178
|
+
|
|
179
|
+
for index in range(classes):
|
|
180
|
+
type_index = struct.unpack_from("<I", data, classes_offset + index * 32)[0]
|
|
181
|
+
require(type_index < types, "Invalid DEX class type")
|
|
182
|
+
string_index = struct.unpack_from("<I", data, types_offset + type_index * 4)[0]
|
|
183
|
+
require(string_index < strings, "Invalid DEX type descriptor")
|
|
184
|
+
offset = struct.unpack_from("<I", data, strings_offset + string_index * 4)[0]
|
|
185
|
+
require(112 <= offset < len(data), "Invalid DEX string offset")
|
|
186
|
+
|
|
187
|
+
for _ in range(5):
|
|
188
|
+
require(offset < len(data), "Invalid DEX string length")
|
|
189
|
+
value = data[offset]
|
|
190
|
+
offset += 1
|
|
191
|
+
|
|
192
|
+
if value & 128 == 0:
|
|
193
|
+
break
|
|
194
|
+
else:
|
|
195
|
+
raise DevkitError("Invalid DEX string length")
|
|
196
|
+
|
|
197
|
+
end = data.find(b"\0", offset, min(len(data), offset + 1025))
|
|
198
|
+
require(end >= 0, "Unterminated or oversized DEX descriptor")
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
descriptor = data[offset:end].decode("utf-8")
|
|
202
|
+
except UnicodeDecodeError as error:
|
|
203
|
+
raise DevkitError("Invalid DEX class descriptor") from error
|
|
204
|
+
|
|
205
|
+
require(descriptor.startswith("L") and descriptor.endswith(";"),
|
|
206
|
+
"Invalid DEX class descriptor")
|
|
207
|
+
forbidden = ("Lorg/telegram/", "Landroid/", "Ljava/", "Ljavax/", "Ldalvik/", "Lcom/chaquo/")
|
|
208
|
+
require(not descriptor.startswith(forbidden), "DEX bundles host/platform classes")
|
|
209
|
+
found |= descriptor == expected
|
|
210
|
+
|
|
211
|
+
require(found, "Java entrypoint is not defined in DEX")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def read_number(data, offset, length, byteorder):
|
|
215
|
+
require(0 <= offset and offset + length <= len(data), "Truncated image data")
|
|
216
|
+
return int.from_bytes(data[offset:offset + length], byteorder)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def validate_icon(data, path):
|
|
220
|
+
require(20 <= len(data) <= 512 * 1024, "Invalid icon size")
|
|
221
|
+
width = 0
|
|
222
|
+
height = 0
|
|
223
|
+
|
|
224
|
+
if path.lower().endswith(".png"):
|
|
225
|
+
require(data[:8] == b"\x89PNG\r\n\x1a\n", "Invalid PNG icon")
|
|
226
|
+
offset = 8
|
|
227
|
+
end = False
|
|
228
|
+
pixels = False
|
|
229
|
+
|
|
230
|
+
while offset + 12 <= len(data):
|
|
231
|
+
length = read_number(data, offset, 4, "big")
|
|
232
|
+
require(length <= len(data) - offset - 12, "Truncated PNG icon")
|
|
233
|
+
kind = data[offset + 4:offset + 8]
|
|
234
|
+
checksum = read_number(data, offset + 8 + length, 4, "big")
|
|
235
|
+
require(zlib.crc32(data[offset + 4:offset + 8 + length]) == checksum,
|
|
236
|
+
"Invalid PNG checksum")
|
|
237
|
+
require(kind not in {b"acTL", b"fcTL", b"fdAT"}, "Animated icons are not supported")
|
|
238
|
+
|
|
239
|
+
if offset == 8:
|
|
240
|
+
require(kind == b"IHDR" and length == 13, "Invalid PNG header")
|
|
241
|
+
width = read_number(data, offset + 8, 4, "big")
|
|
242
|
+
height = read_number(data, offset + 12, 4, "big")
|
|
243
|
+
else:
|
|
244
|
+
require(kind != b"IHDR", "Duplicate PNG header")
|
|
245
|
+
|
|
246
|
+
pixels |= kind == b"IDAT"
|
|
247
|
+
offset += length + 12
|
|
248
|
+
|
|
249
|
+
if kind == b"IEND":
|
|
250
|
+
require(length == 0 and offset == len(data), "Invalid PNG end")
|
|
251
|
+
end = True
|
|
252
|
+
break
|
|
253
|
+
|
|
254
|
+
require(end and pixels, "Incomplete PNG icon")
|
|
255
|
+
else:
|
|
256
|
+
require(data[:4] == b"RIFF" and data[8:12] == b"WEBP", "Invalid WebP icon")
|
|
257
|
+
require(read_number(data, 4, 4, "little") == len(data) - 8, "Invalid WebP length")
|
|
258
|
+
offset = 12
|
|
259
|
+
pixels = False
|
|
260
|
+
|
|
261
|
+
while offset + 8 <= len(data):
|
|
262
|
+
kind = data[offset:offset + 4]
|
|
263
|
+
length = read_number(data, offset + 4, 4, "little")
|
|
264
|
+
require(length <= len(data) - offset - 8, "Truncated WebP icon")
|
|
265
|
+
start = offset + 8
|
|
266
|
+
require(kind not in {b"ANIM", b"ANMF"}, "Animated icons are not supported")
|
|
267
|
+
|
|
268
|
+
if kind == b"VP8X":
|
|
269
|
+
require(length == 10 and data[start] & 2 == 0, "Animated or invalid WebP icon")
|
|
270
|
+
check_icon_dimensions(read_number(data, start + 4, 3, "little") + 1,
|
|
271
|
+
read_number(data, start + 7, 3, "little") + 1)
|
|
272
|
+
elif kind == b"VP8 ":
|
|
273
|
+
require(not pixels and length >= 10 and data[start + 3:start + 6] == b"\x9d\x01\x2a",
|
|
274
|
+
"Invalid WebP frame")
|
|
275
|
+
width = read_number(data, start + 6, 2, "little") & 0x3FFF
|
|
276
|
+
height = read_number(data, start + 8, 2, "little") & 0x3FFF
|
|
277
|
+
pixels = True
|
|
278
|
+
elif kind == b"VP8L":
|
|
279
|
+
require(not pixels and length >= 5 and data[start] == 0x2F, "Invalid WebP frame")
|
|
280
|
+
bits = read_number(data, start + 1, 4, "little")
|
|
281
|
+
width = (bits & 0x3FFF) + 1
|
|
282
|
+
height = ((bits >> 14) & 0x3FFF) + 1
|
|
283
|
+
pixels = True
|
|
284
|
+
|
|
285
|
+
offset += 8 + length + (length & 1)
|
|
286
|
+
|
|
287
|
+
require(pixels and offset == len(data), "Incomplete WebP icon")
|
|
288
|
+
|
|
289
|
+
check_icon_dimensions(width, height)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def check_icon_dimensions(width, height):
|
|
293
|
+
require(0 < width <= 1024 and 0 < height <= 1024,
|
|
294
|
+
"Icon dimensions must be between 1 and 1024 pixels")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def inspect_archive(path):
|
|
298
|
+
path = Path(path)
|
|
299
|
+
require(path.is_file(), f"Package does not exist: {path}")
|
|
300
|
+
require(path.stat().st_size <= MAX_ARCHIVE_BYTES, "Archive exceeds 8 MiB")
|
|
301
|
+
spellings = {}
|
|
302
|
+
kinds = {}
|
|
303
|
+
contents = {}
|
|
304
|
+
total = 0
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
archive = zipfile.ZipFile(path)
|
|
308
|
+
except (OSError, zipfile.BadZipFile) as error:
|
|
309
|
+
raise DevkitError("Invalid ZIP archive") from error
|
|
310
|
+
|
|
311
|
+
with archive:
|
|
312
|
+
entries = archive.infolist()
|
|
313
|
+
require(len(entries) <= MAX_ENTRIES, "Too many ZIP entries")
|
|
314
|
+
|
|
315
|
+
for entry in entries:
|
|
316
|
+
require(entry.flag_bits & 1 == 0, "Encrypted ZIP entries are not supported")
|
|
317
|
+
require(entry.compress_type in {zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED},
|
|
318
|
+
"Unsupported ZIP compression")
|
|
319
|
+
directory = entry.is_dir()
|
|
320
|
+
name = validate_path(entry.filename, directory)
|
|
321
|
+
require(name not in kinds, f"Duplicate ZIP path: {name}")
|
|
322
|
+
kinds[name] = directory
|
|
323
|
+
prefix = ""
|
|
324
|
+
|
|
325
|
+
for part in name.split("/"):
|
|
326
|
+
prefix = part if not prefix else prefix + "/" + part
|
|
327
|
+
key = prefix.lower()
|
|
328
|
+
require(key not in spellings or spellings[key] == prefix, "Case-conflicting ZIP path")
|
|
329
|
+
spellings[key] = prefix
|
|
330
|
+
|
|
331
|
+
mode = entry.external_attr >> 16
|
|
332
|
+
|
|
333
|
+
if mode:
|
|
334
|
+
require(not stat.S_ISLNK(mode), "Symbolic links are not supported")
|
|
335
|
+
require(directory or stat.S_ISREG(mode), "Special files are not supported")
|
|
336
|
+
|
|
337
|
+
limit = MAX_MANIFEST_BYTES if name == "manifest.json" else MAX_FILE_BYTES
|
|
338
|
+
require(0 <= entry.file_size <= limit, f"Oversized file: {name}")
|
|
339
|
+
require(not directory or entry.file_size == 0, "Directory contains file data")
|
|
340
|
+
data = archive.read(entry)
|
|
341
|
+
require(len(data) == entry.file_size, f"Corrupt ZIP entry: {name}")
|
|
342
|
+
total += len(data)
|
|
343
|
+
require(total <= MAX_UNPACKED_BYTES, "Unpacked size exceeds 32 MiB")
|
|
344
|
+
|
|
345
|
+
if not directory:
|
|
346
|
+
contents[name] = data
|
|
347
|
+
|
|
348
|
+
for name in kinds:
|
|
349
|
+
parts = name.split("/")
|
|
350
|
+
|
|
351
|
+
for index in range(1, len(parts)):
|
|
352
|
+
require(kinds.get("/".join(parts[:index])) is not False, "File used as a directory")
|
|
353
|
+
|
|
354
|
+
require("manifest.json" in contents, "Missing root manifest.json")
|
|
355
|
+
manifest = validate_manifest(read_json(contents["manifest.json"]))
|
|
356
|
+
python_file = manifest["entrypoint"].split(":", 1)[0] + ".py"
|
|
357
|
+
require(python_file in contents, "Missing Python entrypoint file")
|
|
358
|
+
|
|
359
|
+
if manifest.get("java_module") is not None:
|
|
360
|
+
java = manifest["java_module"]
|
|
361
|
+
require(java["path"] in contents, "Missing Java module")
|
|
362
|
+
validate_dex(contents[java["path"]], java)
|
|
363
|
+
|
|
364
|
+
if "icon" in manifest:
|
|
365
|
+
require(manifest["icon"] in contents, "Missing plugin icon")
|
|
366
|
+
validate_icon(contents[manifest["icon"]], manifest["icon"])
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
"id": manifest["id"],
|
|
370
|
+
"version": manifest["version"],
|
|
371
|
+
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
|
|
372
|
+
"files": len(contents),
|
|
373
|
+
"bytes": total,
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def load_project(project_dir):
|
|
378
|
+
project_dir = Path(project_dir).resolve()
|
|
379
|
+
config_path = project_dir / "fluxgram-plugin.toml"
|
|
380
|
+
require(config_path.is_file(), f"Missing project file: {config_path}")
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
config = tomllib.loads(config_path.read_text("utf-8"))
|
|
384
|
+
except (OSError, tomllib.TOMLDecodeError) as error:
|
|
385
|
+
raise DevkitError("Invalid fluxgram-plugin.toml") from error
|
|
386
|
+
|
|
387
|
+
require(set(config).issubset({"plugin", "python", "java", "assets"}), "Unknown project section")
|
|
388
|
+
require("plugin" in config and "python" in config, "Project requires plugin and python sections")
|
|
389
|
+
plugin = config["plugin"]
|
|
390
|
+
python = config["python"]
|
|
391
|
+
require(isinstance(plugin, dict) and isinstance(python, dict), "Invalid project section")
|
|
392
|
+
plugin_fields = {
|
|
393
|
+
"id", "name", "version", "description", "author", "min_sdk_minor",
|
|
394
|
+
"declared_capabilities", "icon",
|
|
395
|
+
}
|
|
396
|
+
require(set(plugin).issubset(plugin_fields), "Unknown plugin project field")
|
|
397
|
+
required_plugin = plugin_fields - {"icon"}
|
|
398
|
+
require(required_plugin.issubset(plugin), "Missing plugin project field")
|
|
399
|
+
require(set(python) == {"root", "entrypoint", "files"}, "Invalid python project fields")
|
|
400
|
+
java = config.get("java")
|
|
401
|
+
assets = config.get("assets", {"root": "assets", "files": []})
|
|
402
|
+
require(isinstance(assets, dict) and set(assets) == {"root", "files"}, "Invalid assets section")
|
|
403
|
+
|
|
404
|
+
if java is not None:
|
|
405
|
+
require(isinstance(java, dict)
|
|
406
|
+
and set(java) == {"sources", "entrypoint", "min_api_minor"},
|
|
407
|
+
"Invalid java project fields")
|
|
408
|
+
|
|
409
|
+
manifest = {
|
|
410
|
+
"schema_version": 3 if java is not None else 2,
|
|
411
|
+
"sdk_version": SDK_VERSION,
|
|
412
|
+
"min_sdk_minor": plugin["min_sdk_minor"],
|
|
413
|
+
"id": plugin["id"],
|
|
414
|
+
"name": plugin["name"],
|
|
415
|
+
"version": plugin["version"],
|
|
416
|
+
"description": plugin["description"],
|
|
417
|
+
"author": plugin["author"],
|
|
418
|
+
"entrypoint": python["entrypoint"],
|
|
419
|
+
"access": "full",
|
|
420
|
+
"declared_capabilities": plugin["declared_capabilities"],
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if "icon" in plugin:
|
|
424
|
+
manifest["icon"] = plugin["icon"]
|
|
425
|
+
|
|
426
|
+
if java is not None:
|
|
427
|
+
manifest["java_module"] = {
|
|
428
|
+
"path": "java/module.dex",
|
|
429
|
+
"entrypoint": java["entrypoint"],
|
|
430
|
+
"sha256": "0" * 64,
|
|
431
|
+
"api_version": JAVA_API_VERSION,
|
|
432
|
+
"min_api_minor": java["min_api_minor"],
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
validate_manifest(manifest)
|
|
436
|
+
manifest.pop("java_module", None)
|
|
437
|
+
return project_dir, config, manifest
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def list_files(project_dir, root_name, names):
|
|
441
|
+
require(isinstance(root_name, str), "Project root must be a string")
|
|
442
|
+
require(isinstance(names, list) and all(isinstance(name, str) for name in names),
|
|
443
|
+
"Project files must be an array of strings")
|
|
444
|
+
root = (project_dir / root_name).resolve()
|
|
445
|
+
require(root == project_dir or project_dir in root.parents, "Project root escapes the project")
|
|
446
|
+
result = []
|
|
447
|
+
|
|
448
|
+
for name in names:
|
|
449
|
+
destination = validate_path(name)
|
|
450
|
+
source = (root / name).resolve()
|
|
451
|
+
require(root in source.parents and source.is_file(), f"Missing project file: {source}")
|
|
452
|
+
require(source.stat().st_size <= MAX_FILE_BYTES, f"Project file exceeds 8 MiB: {source}")
|
|
453
|
+
result.append((destination, source.read_bytes()))
|
|
454
|
+
|
|
455
|
+
return result
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def find_java(java_home):
|
|
459
|
+
homes = []
|
|
460
|
+
|
|
461
|
+
if java_home:
|
|
462
|
+
homes.append(Path(java_home))
|
|
463
|
+
|
|
464
|
+
if os.environ.get("JAVA_HOME"):
|
|
465
|
+
homes.append(Path(os.environ["JAVA_HOME"]))
|
|
466
|
+
|
|
467
|
+
for home in homes:
|
|
468
|
+
executable = home / "bin" / ("javac.exe" if os.name == "nt" else "javac")
|
|
469
|
+
|
|
470
|
+
if executable.is_file():
|
|
471
|
+
return executable
|
|
472
|
+
|
|
473
|
+
executable = shutil.which("javac")
|
|
474
|
+
require(executable is not None, "javac was not found; pass --java-home or set JAVA_HOME")
|
|
475
|
+
return Path(executable)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def find_android(android_sdk, build_tools):
|
|
479
|
+
candidates = []
|
|
480
|
+
|
|
481
|
+
if android_sdk:
|
|
482
|
+
candidates.append(Path(android_sdk))
|
|
483
|
+
|
|
484
|
+
for key in ("ANDROID_SDK_ROOT", "ANDROID_HOME"):
|
|
485
|
+
if os.environ.get(key):
|
|
486
|
+
candidates.append(Path(os.environ[key]))
|
|
487
|
+
|
|
488
|
+
candidates.extend([
|
|
489
|
+
Path.home() / "Library/Android/sdk",
|
|
490
|
+
Path.home() / "Android/Sdk",
|
|
491
|
+
Path(os.environ.get("LOCALAPPDATA", "")) / "Android/Sdk",
|
|
492
|
+
])
|
|
493
|
+
sdk = next((item for item in candidates if (item / "platforms/android-36/android.jar").is_file()), None)
|
|
494
|
+
require(sdk is not None, "Android SDK platform 36 was not found; pass --android-sdk")
|
|
495
|
+
android_jar = sdk / "platforms/android-36/android.jar"
|
|
496
|
+
|
|
497
|
+
if build_tools:
|
|
498
|
+
tools = sdk / "build-tools" / build_tools
|
|
499
|
+
require(tools.is_dir(), f"Android build-tools {build_tools} were not found")
|
|
500
|
+
else:
|
|
501
|
+
roots = [item for item in (sdk / "build-tools").iterdir()
|
|
502
|
+
if item.is_dir() and all(part.isdigit() for part in item.name.split("."))
|
|
503
|
+
and item.name.split(".", 1)[0] in {"35", "36"}]
|
|
504
|
+
require(roots, "Android build-tools 35 or 36 were not found")
|
|
505
|
+
tools = sorted(roots, key=lambda item: tuple(int(part) for part in item.name.split(".")))[-1]
|
|
506
|
+
|
|
507
|
+
d8 = tools / ("d8.bat" if os.name == "nt" else "d8")
|
|
508
|
+
require(d8.is_file(), f"D8 was not found in {tools}")
|
|
509
|
+
return sdk, android_jar, tools, d8
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def run(command, label):
|
|
513
|
+
process = subprocess.run([str(item) for item in command], text=True,
|
|
514
|
+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
|
515
|
+
|
|
516
|
+
if process.returncode != 0:
|
|
517
|
+
raise DevkitError(f"{label} failed:\n{process.stdout.strip()}")
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def api_artifact():
|
|
521
|
+
root = Path(__file__).with_name("artifacts")
|
|
522
|
+
artifact = root / f"fluxgram-java-api-{JAVA_API_VERSION}.{JAVA_API_MINOR}.jar"
|
|
523
|
+
metadata_path = root / "api.json"
|
|
524
|
+
require(artifact.is_file() and metadata_path.is_file(), "Bundled Java API artifact is missing")
|
|
525
|
+
|
|
526
|
+
try:
|
|
527
|
+
metadata = json.loads(metadata_path.read_text("utf-8"))
|
|
528
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
529
|
+
raise DevkitError("Invalid Java API metadata") from error
|
|
530
|
+
|
|
531
|
+
fields = {
|
|
532
|
+
"api_version", "api_minor", "min_sdk", "compile_sdk", "sha256",
|
|
533
|
+
"source_sha256", "host_validator_sha256", "source_commit",
|
|
534
|
+
}
|
|
535
|
+
require(isinstance(metadata, dict) and set(metadata) == fields, "Invalid Java API metadata")
|
|
536
|
+
require(metadata["api_version"] == JAVA_API_VERSION and metadata["api_minor"] == JAVA_API_MINOR,
|
|
537
|
+
"Java API metadata version mismatch")
|
|
538
|
+
require(metadata["min_sdk"] == 24 and metadata["compile_sdk"] == 36,
|
|
539
|
+
"Java API Android metadata mismatch")
|
|
540
|
+
require(HASH.fullmatch(metadata["sha256"]) is not None
|
|
541
|
+
and HASH.fullmatch(metadata["source_sha256"]) is not None
|
|
542
|
+
and HASH.fullmatch(metadata["host_validator_sha256"]) is not None
|
|
543
|
+
and isinstance(metadata["source_commit"], str) and metadata["source_commit"],
|
|
544
|
+
"Invalid Java API metadata values")
|
|
545
|
+
require(hashlib.sha256(artifact.read_bytes()).hexdigest() == metadata["sha256"],
|
|
546
|
+
"Java API artifact digest mismatch")
|
|
547
|
+
return artifact, metadata
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def zip_bytes(files):
|
|
551
|
+
output = io.BytesIO()
|
|
552
|
+
|
|
553
|
+
with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
|
|
554
|
+
for name, data in sorted(files):
|
|
555
|
+
info = zipfile.ZipInfo(name, FIXED_TIME)
|
|
556
|
+
info.create_system = 3
|
|
557
|
+
info.external_attr = (stat.S_IFREG | 0o644) << 16
|
|
558
|
+
info.compress_type = zipfile.ZIP_DEFLATED
|
|
559
|
+
archive.writestr(info, data)
|
|
560
|
+
|
|
561
|
+
output.seek(0)
|
|
562
|
+
return output.read()
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def compile_java(project_dir, java_config, java_home, android_sdk, build_tools):
|
|
566
|
+
javac = find_java(java_home)
|
|
567
|
+
sdk, android_jar, tools, d8 = find_android(android_sdk, build_tools)
|
|
568
|
+
source_roots = java_config["sources"]
|
|
569
|
+
require(isinstance(source_roots, list) and source_roots, "java.sources must not be empty")
|
|
570
|
+
sources = []
|
|
571
|
+
|
|
572
|
+
for root_name in source_roots:
|
|
573
|
+
require(isinstance(root_name, str), "Java source root must be a string")
|
|
574
|
+
root = (project_dir / root_name).resolve()
|
|
575
|
+
require(project_dir in root.parents and root.is_dir(), f"Missing Java source root: {root}")
|
|
576
|
+
sources.extend(sorted(root.rglob("*.java")))
|
|
577
|
+
|
|
578
|
+
require(sources, "No Java sources were found")
|
|
579
|
+
api_jar, _ = api_artifact()
|
|
580
|
+
|
|
581
|
+
with tempfile.TemporaryDirectory(prefix="fluxgram-plugin-") as temporary:
|
|
582
|
+
temporary = Path(temporary)
|
|
583
|
+
classes = temporary / "classes"
|
|
584
|
+
classes.mkdir()
|
|
585
|
+
run([javac, "--release", "8", "-cp", os.pathsep.join([str(android_jar), str(api_jar)]),
|
|
586
|
+
"-d", classes, *sources], "javac")
|
|
587
|
+
class_files = [(str(path.relative_to(classes)).replace(os.sep, "/"), path.read_bytes())
|
|
588
|
+
for path in classes.rglob("*.class")]
|
|
589
|
+
require(class_files, "javac produced no classes")
|
|
590
|
+
input_jar = temporary / "plugin.jar"
|
|
591
|
+
input_jar.write_bytes(zip_bytes(class_files))
|
|
592
|
+
dex_dir = temporary / "dex"
|
|
593
|
+
dex_dir.mkdir()
|
|
594
|
+
run([d8, "--min-api", "24", "--lib", android_jar, "--classpath", api_jar,
|
|
595
|
+
"--output", dex_dir, input_jar], "D8")
|
|
596
|
+
dex_files = sorted(dex_dir.glob("*.dex"))
|
|
597
|
+
require([item.name for item in dex_files] == ["classes.dex"], "D8 produced unsupported multidex output")
|
|
598
|
+
data = dex_files[0].read_bytes()
|
|
599
|
+
require(data[:8] in DEX_MAGICS, "D8 produced a DEX version unsupported by FluxGram")
|
|
600
|
+
return data, javac, sdk, tools
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def build_project(project_dir, output=None, java_home=None, android_sdk=None, build_tools=None):
|
|
604
|
+
project_dir, config, manifest = load_project(project_dir)
|
|
605
|
+
files = list_files(project_dir, config["python"]["root"], config["python"]["files"])
|
|
606
|
+
assets = config.get("assets", {"root": "assets", "files": []})
|
|
607
|
+
files.extend(list_files(project_dir, assets["root"], assets["files"]))
|
|
608
|
+
java = config.get("java")
|
|
609
|
+
|
|
610
|
+
if java is not None:
|
|
611
|
+
dex, _, _, _ = compile_java(project_dir, java, java_home, android_sdk, build_tools)
|
|
612
|
+
manifest["java_module"] = {
|
|
613
|
+
"path": "java/module.dex",
|
|
614
|
+
"entrypoint": java["entrypoint"],
|
|
615
|
+
"sha256": hashlib.sha256(dex).hexdigest(),
|
|
616
|
+
"api_version": JAVA_API_VERSION,
|
|
617
|
+
"min_api_minor": java["min_api_minor"],
|
|
618
|
+
}
|
|
619
|
+
validate_manifest(manifest)
|
|
620
|
+
files.append(("java/module.dex", dex))
|
|
621
|
+
|
|
622
|
+
manifest_data = (json.dumps(manifest, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
|
|
623
|
+
files.append(("manifest.json", manifest_data))
|
|
624
|
+
names = [name for name, _ in files]
|
|
625
|
+
require(len(names) == len(set(names)), "Project maps multiple files to the same package path")
|
|
626
|
+
package = zip_bytes(files)
|
|
627
|
+
require(len(package) <= MAX_ARCHIVE_BYTES, "Archive exceeds 8 MiB")
|
|
628
|
+
|
|
629
|
+
if output is None:
|
|
630
|
+
output = project_dir / "dist" / f"{manifest['id']}-{manifest['version']}.fgplugin"
|
|
631
|
+
else:
|
|
632
|
+
output = Path(output).resolve()
|
|
633
|
+
|
|
634
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
635
|
+
output.write_bytes(package)
|
|
636
|
+
inspect_archive(output)
|
|
637
|
+
return output
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def initialize_project(destination):
|
|
641
|
+
destination = Path(destination).resolve()
|
|
642
|
+
require(not destination.exists(), f"Destination already exists: {destination}")
|
|
643
|
+
template = Path(__file__).with_name("templates") / "python-java"
|
|
644
|
+
require(template.is_dir(), "Bundled project template is missing")
|
|
645
|
+
shutil.copytree(template, destination)
|
|
646
|
+
return destination
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def doctor(java_home=None, android_sdk=None, build_tools=None):
|
|
650
|
+
javac = find_java(java_home)
|
|
651
|
+
sdk, android_jar, tools, d8 = find_android(android_sdk, build_tools)
|
|
652
|
+
artifact, _ = api_artifact()
|
|
653
|
+
print(f"javac: {javac}")
|
|
654
|
+
print(f"android.jar: {android_jar}")
|
|
655
|
+
print(f"build-tools: {tools}")
|
|
656
|
+
print(f"d8: {d8}")
|
|
657
|
+
print(f"java-api: {JAVA_API_VERSION}.{JAVA_API_MINOR} {artifact}")
|
|
658
|
+
print(f"android-sdk: {sdk}")
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def parser():
|
|
662
|
+
value = argparse.ArgumentParser(prog="fluxgram-plugin")
|
|
663
|
+
commands = value.add_subparsers(dest="command", required=True)
|
|
664
|
+
init = commands.add_parser("init")
|
|
665
|
+
init.add_argument("destination")
|
|
666
|
+
build = commands.add_parser("build")
|
|
667
|
+
build.add_argument("project", nargs="?", default=".")
|
|
668
|
+
build.add_argument("--output")
|
|
669
|
+
validate = commands.add_parser("validate")
|
|
670
|
+
validate.add_argument("archive")
|
|
671
|
+
doctor_command = commands.add_parser("doctor")
|
|
672
|
+
|
|
673
|
+
for command in (build, doctor_command):
|
|
674
|
+
command.add_argument("--java-home")
|
|
675
|
+
command.add_argument("--android-sdk")
|
|
676
|
+
command.add_argument("--build-tools")
|
|
677
|
+
|
|
678
|
+
return value
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def main(argv=None):
|
|
682
|
+
arguments = parser().parse_args(argv)
|
|
683
|
+
|
|
684
|
+
try:
|
|
685
|
+
if arguments.command == "init":
|
|
686
|
+
print(initialize_project(arguments.destination))
|
|
687
|
+
elif arguments.command == "build":
|
|
688
|
+
print(build_project(arguments.project, arguments.output, arguments.java_home,
|
|
689
|
+
arguments.android_sdk, arguments.build_tools))
|
|
690
|
+
elif arguments.command == "validate":
|
|
691
|
+
result = inspect_archive(arguments.archive)
|
|
692
|
+
print(f"{result['id']} {result['version']} {result['sha256']}")
|
|
693
|
+
else:
|
|
694
|
+
doctor(arguments.java_home, arguments.android_sdk, arguments.build_tools)
|
|
695
|
+
except DevkitError as error:
|
|
696
|
+
print(f"error: {error}", file=sys.stderr)
|
|
697
|
+
return 2
|
|
698
|
+
|
|
699
|
+
return 0
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
if __name__ == "__main__":
|
|
703
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[plugin]
|
|
2
|
+
id = "example.hello_plugin"
|
|
3
|
+
name = "Hello Plugin"
|
|
4
|
+
version = "1.0.0"
|
|
5
|
+
description = "Minimal FluxGram Python and Java plugin"
|
|
6
|
+
author = "Plugin Author"
|
|
7
|
+
min_sdk_minor = 9
|
|
8
|
+
declared_capabilities = []
|
|
9
|
+
|
|
10
|
+
[python]
|
|
11
|
+
root = "plugin"
|
|
12
|
+
entrypoint = "main:PluginEntry"
|
|
13
|
+
files = ["main.py"]
|
|
14
|
+
|
|
15
|
+
[java]
|
|
16
|
+
sources = ["java/src"]
|
|
17
|
+
entrypoint = "example.plugin.Module"
|
|
18
|
+
min_api_minor = 19
|
|
19
|
+
|
|
20
|
+
[assets]
|
|
21
|
+
root = "assets"
|
|
22
|
+
files = []
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
package example.plugin;
|
|
2
|
+
|
|
3
|
+
import org.telegram.messenger.plugins.api.JavaPluginContext;
|
|
4
|
+
import org.telegram.messenger.plugins.api.JavaPluginModule;
|
|
5
|
+
|
|
6
|
+
public final class Module implements JavaPluginModule {
|
|
7
|
+
private JavaPluginContext context;
|
|
8
|
+
|
|
9
|
+
@Override
|
|
10
|
+
public void onLoad(JavaPluginContext context) {
|
|
11
|
+
this.context = context;
|
|
12
|
+
context.log("Hello Plugin Java module loaded");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@Override
|
|
16
|
+
public void onSettingChanged(String key, Object value) {
|
|
17
|
+
if (context != null && "enabled".equals(key)) {
|
|
18
|
+
context.log("enabled=" + value);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Override
|
|
23
|
+
public void onUnload() {
|
|
24
|
+
context = null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fluxgram-plugin-devkit
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Build and validate FluxGram plugin packages
|
|
5
|
+
License-Expression: GPL-2.0-only
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# FluxGram Plugin Devkit
|
|
10
|
+
|
|
11
|
+
Installable command-line tools and Java API artifacts for building external FluxGram plugins.
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
python3 -m pip install fluxgram-plugin-devkit
|
|
15
|
+
fluxgram-plugin doctor
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
See the [developer kit guide](https://github.com/mrhexvel/fluxgram-plugin/blob/main/docs/developer-kit.md) for requirements, trust boundaries, project format, commands, and testing guidance.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
fluxgram_devkit/__init__.py,sha256=ecmeC91h8032LinFnCCUhOQsovtgls_lnjsCkLTNqc4,149
|
|
2
|
+
fluxgram_devkit/cli.py,sha256=ulgJetXEuoYiLGCoe9GnWe9QWYkxy7GckJbIb4lE5HI,29116
|
|
3
|
+
fluxgram_devkit/artifacts/api.json,sha256=qPTYhm6QRAvix6MvPHEkIG8NCMgRdSsBkhtj5sQqNbs,374
|
|
4
|
+
fluxgram_devkit/artifacts/fluxgram-java-api-1.19.jar,sha256=ooVqb_pM4z-3YXIk_ZEeQRAyrWggDSV3mrHYCRdr3mI,25519
|
|
5
|
+
fluxgram_devkit/templates/python-java/fluxgram-plugin.toml,sha256=en3_sOPg2YDiZAAZU4l2X9-ME0tOUpoilZXRouAm83s,405
|
|
6
|
+
fluxgram_devkit/templates/python-java/java/src/example/plugin/Module.java,sha256=RH27sYlvOGgf4x60rIKIEg-CoaB4CkRDgqPut_b0wDo,672
|
|
7
|
+
fluxgram_devkit/templates/python-java/plugin/main.py,sha256=B087lAiXdqjTFRUJN9UOdQRAJHSSuE7YI6jgBh1JR7Q,226
|
|
8
|
+
fluxgram_plugin_devkit-1.0.0.dist-info/METADATA,sha256=09d4dLypC_-pe7ZTfvAJ6mXXURCIcEenomMRJi3zTGY,609
|
|
9
|
+
fluxgram_plugin_devkit-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
10
|
+
fluxgram_plugin_devkit-1.0.0.dist-info/entry_points.txt,sha256=Il1Sh5iqIS8qckBO0Lwqi7_EcFenbjWqhXL6CbXDYFU,61
|
|
11
|
+
fluxgram_plugin_devkit-1.0.0.dist-info/top_level.txt,sha256=WY5pDo5Gtg8QEp3cFbkryJj-JkXio6KUqJJ4zaQdm3I,16
|
|
12
|
+
fluxgram_plugin_devkit-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fluxgram_devkit
|