Merge remote-tracking branch 'origin/bundle-deps' into frame-sideload-features

# Conflicts:
#	app/main.js
This commit is contained in:
saphid committed 2026-09-26 22:01:46 +10:00
commit ccf5f3e123
5 files changed
+68 -26

No files matched your search

+16 -6
View File
@@ -39,22 +39,26 @@ const PRUNE = [
"include", "share", "Scripts", "libs", "tcl", "lib/pkgconfig", "lib/itcl4", "lib/tcl8", "lib/tcl8.6", "include", "share", "Scripts", "libs", "tcl", "lib/pkgconfig", "lib/itcl4", "lib/tcl8", "lib/tcl8.6",
"lib/tk8.6", "lib/thread2.8", "bin/idle3", "bin/idle3.12", "bin/pip", "bin/pip3", "bin/pip3.12", "lib/tk8.6", "lib/thread2.8", "bin/idle3", "bin/idle3.12", "bin/pip", "bin/pip3", "bin/pip3.12",
"bin/pydoc3", "bin/pydoc3.12", "bin/2to3", "bin/2to3-3.12", "bin/python3-config", "bin/python3.12-config", "bin/pydoc3", "bin/pydoc3.12", "bin/2to3", "bin/2to3-3.12", "bin/python3-config", "bin/python3.12-config",
...["test", "idlelib", "tkinter", "turtledemo", "ensurepip", "lib2to3", "site-packages/pip"] ...["test", "idlelib", "tkinter", "turtledemo", "ensurepip", "lib2to3", "site-packages/pip", "pydoc_data", "venv"]
.flatMap((d) => [`lib/python3.12/${d}`, `Lib/${d}`]), .flatMap((d) => [`lib/python3.12/${d}`, `Lib/${d}`]),
]; ];
function get(url) { function get(url, redirects = 5) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
https.get(url, (res) => { https.get(url, { timeout: 60000 }, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
res.resume(); res.resume();
return resolve(get(res.headers.location)); if (!redirects) return reject(new Error(`${url}: too many redirects`));
let next;
try { next = new URL(res.headers.location, url).href; }
catch { return reject(new Error(`${url}: bad redirect ${res.headers.location}`)); }
return resolve(get(next, redirects - 1));
} }
if (res.statusCode !== 200) return reject(new Error(`${url}: HTTP ${res.statusCode}`)); if (res.statusCode !== 200) return reject(new Error(`${url}: HTTP ${res.statusCode}`));
const chunks = []; const chunks = [];
res.on("data", (c) => chunks.push(c)); res.on("data", (c) => chunks.push(c));
res.on("end", () => resolve(Buffer.concat(chunks))); res.on("end", () => resolve(Buffer.concat(chunks)));
}).on("error", reject); }).on("timeout", function () { this.destroy(new Error(`${url}: timed out`)); }).on("error", reject);
}); });
} }
@@ -66,7 +70,7 @@ async function download(url, sha256, file) {
} }
// Windows' own bsdtar: Git's GNU tar, often first on PATH, reads C:\ as a remote host. // Windows' own bsdtar: Git's GNU tar, often first on PATH, reads C:\ as a remote host.
const TAR = process.platform === "win32" ? path.join(process.env.SystemRoot, "System32", "tar.exe") : "tar"; const TAR = process.platform === "win32" ? path.join(process.env.SystemRoot || "C:\\Windows", "System32", "tar.exe") : "tar";
function extract(file, dir) { function extract(file, dir) {
fs.mkdirSync(dir, { recursive: true }); fs.mkdirSync(dir, { recursive: true });
@@ -97,6 +101,12 @@ async function fetch(os, arch) {
await download(PY_URL(triple), pySha, tgz); await download(PY_URL(triple), pySha, tgz);
extract(tgz, out); // unpacks to python/ extract(tgz, out); // unpacks to python/
for (const p of PRUNE) fs.rmSync(path.join(out, "python", p), { recursive: true, force: true }); for (const p of PRUNE) fs.rmSync(path.join(out, "python", p), { recursive: true, force: true });
const stdlib = path.join(out, "python", "lib", "python3.12"); // macOS and Linux: drop the static libpython
if (fs.existsSync(stdlib)) {
for (const d of fs.readdirSync(stdlib)) {
if (d.startsWith("config-3.12")) fs.rmSync(path.join(stdlib, d), { recursive: true, force: true });
}
}
const tools = path.join(out, "tools"); const tools = path.join(out, "tools");
fs.mkdirSync(tools); fs.mkdirSync(tools);
+4 -2
View File
@@ -236,8 +236,10 @@ async function firstRunCheck() {
// IPC only from our own page in our own window. // IPC only from our own page in our own window.
function fromUi(e) { function fromUi(e) {
return !!(win && e.sender === win.webContents && url && e.senderFrame if (!win || e.sender !== win.webContents || !url || !e.senderFrame) return false;
&& new URL(e.senderFrame.url).origin === new URL(url).origin); try {
return new URL(e.senderFrame.url).origin === new URL(url).origin;
} catch { return false; }
} }
ipcMain.handle("clipboard:read", (e) => fromUi(e) ? clipboard.readText() : ""); ipcMain.handle("clipboard:read", (e) => fromUi(e) ? clipboard.readText() : "");
+3 -1
View File
@@ -124,7 +124,9 @@ The arm64 build also needs your distribution's `adb` for Android apps, because
Google publishes no arm64 Linux platform-tools. Set Up Connection runs Google publishes no arm64 Linux platform-tools. Set Up Connection runs
`ui/frame_connect.py` in your terminal emulator (GNOME Terminal, Konsole, xterm `ui/frame_connect.py` in your terminal emulator (GNOME Terminal, Konsole, xterm
and others). The log is at and others). The log is at
`~/.config/Frame Control/logs/server.log`. `~/.config/Frame Control/logs/server.log`. Running `ui/server.py` in a browser
instead of the app, sending the clipboard needs `wl-clipboard` (Wayland) or
`xclip` (X11).
## Building ## Building
+23 -5
View File
@@ -27,10 +27,14 @@ def pool(strings, utf8=False):
return struct.pack('<HHIIIIII', 1, 28, 28 + len(body), len(strings), 0, 0x100 if utf8 else 0, start, 0) + body return struct.pack('<HHIIIIII', 1, 28, 28 + len(body), len(strings), 0, 0x100 if utf8 else 0, start, 0) + body
def manifest(package, label_ref, version_ref, min_sdk): def manifest(package, label_ref, version_ref, min_sdk, package_raw=True, foreign_label=False):
"""<manifest package versionName><uses-sdk minSdkVersion/><application label icon/></manifest>.""" """<manifest package versionName><uses-sdk minSdkVersion/><application label icon/></manifest>.
package_raw=False drops the package's raw string (as some repackers do);
foreign_label adds a non-android `label` attribute after android:label.
"""
strings = ['label', 'icon', 'versionName', 'minSdkVersion', 'package', 'manifest', 'uses-sdk', strings = ['label', 'icon', 'versionName', 'minSdkVersion', 'package', 'manifest', 'uses-sdk',
'application', package] 'application', package, 'junk', 'label'] # the second 'label' has no android id
resmap = struct.pack('<4I', 0x01010001, 0x01010002, 0x0101021c, 0x0101020c) resmap = struct.pack('<4I', 0x01010001, 0x01010002, 0x0101021c, 0x0101020c)
resmap = struct.pack('<HHI', 0x0180, 8, 8 + len(resmap)) + resmap resmap = struct.pack('<HHI', 0x0180, 8, 8 + len(resmap)) + resmap
@@ -42,9 +46,11 @@ def manifest(package, label_ref, version_ref, min_sdk):
none = 0xffffffff none = 0xffffffff
chunks = (pool(strings) + resmap chunks = (pool(strings) + resmap
+ element(5, [(4, 8, frame_apk.T_STRING, 8), (2, none, frame_apk.T_REF, version_ref)]) + element(5, [(4, 8 if package_raw else none, frame_apk.T_STRING, 8),
(2, none, frame_apk.T_REF, version_ref)])
+ element(6, [(3, none, frame_apk.T_INT_DEC, min_sdk)]) + element(6, [(3, none, frame_apk.T_INT_DEC, min_sdk)])
+ element(7, [(0, none, frame_apk.T_REF, label_ref), (1, none, frame_apk.T_REF, 0x7f020000)])) + element(7, [(0, none, frame_apk.T_REF, label_ref), (1, none, frame_apk.T_REF, 0x7f020000)]
+ ([(10, 9, frame_apk.T_STRING, 9)] if foreign_label else [])))
return struct.pack('<HHI', 3, 8, 8 + len(chunks)) + chunks return struct.pack('<HHI', 3, 8, 8 + len(chunks)) + chunks
@@ -111,6 +117,18 @@ class ApkInfo(unittest.TestCase):
self.assertEqual(info['version'], '') self.assertEqual(info['version'], '')
self.assertEqual(info['abis'], []) self.assertEqual(info['abis'], [])
def test_repacked_manifest(self):
# Package kept only as a typed value; a foreign `label` mustn't beat android:label.
arsc = resources({(1, '', 0): {0: 1, 1: 2}})
info = self.read(apk({
'AndroidManifest.xml': manifest('com.example.repacked', 0x7f010000, 0x7f010001, 24,
package_raw=False, foreign_label=True),
'resources.arsc': arsc,
}))
self.assertEqual(info['package'], 'com.example.repacked')
self.assertEqual(info['label'], 'App label')
self.assertIsNone(info['icon_png'])
def test_rejects_non_apks(self): def test_rejects_non_apks(self):
for data in (b'not a zip', apk({'classes.dex': b''}), apk({'AndroidManifest.xml': b'<manifest/>'})): for data in (b'not a zip', apk({'classes.dex': b''}), apk({'AndroidManifest.xml': b'<manifest/>'})):
with self.assertRaises(frame_apk.ApkError): with self.assertRaises(frame_apk.ApkError):
+22 -12
View File
@@ -71,9 +71,14 @@ def manifest_elements(data):
for i in range(count): for i in range(count):
a = off + hsize + astart + i * asize a = off + hsize + astart + i * asize
aname, raw, dtype, value = struct.unpack_from('<4xII3xBI', data, a) aname, raw, dtype, value = struct.unpack_from('<4xII3xBI', data, a)
key = ATTR.get(resmap[aname]) if aname < len(resmap) else None raw = strings[raw] if raw < len(strings) else None
key = key or (strings[aname] if aname < len(strings) else '') if raw is None and dtype == T_STRING and value < len(strings):
attrs[key] = (dtype, value, strings[raw] if raw < len(strings) else None) raw = strings[value] # some repackers keep only the typed value
android = ATTR.get(resmap[aname]) if aname < len(resmap) else None
if android: # android: attributes win over same-named ones in other namespaces
attrs[android] = (dtype, value, raw)
else:
attrs.setdefault(strings[aname] if aname < len(strings) else '', (dtype, value, raw))
out.append((strings[name] if name < len(strings) else '', attrs)) out.append((strings[name] if name < len(strings) else '', attrs))
return out return out
@@ -197,18 +202,23 @@ def apk_info(path):
'min_sdk': min_sdk[1] if min_sdk and min_sdk[0] in (T_INT_DEC, T_INT_HEX) else None, 'min_sdk': min_sdk[1] if min_sdk and min_sdk[0] in (T_INT_DEC, T_INT_HEX) else None,
'icon_png': None, 'icon_png': None,
} }
for icon in _icons(app.get('icon'), res): try:
if icon.endswith('.png') and icon in names: info['icon_png'] = _icon_png(z, names, _icons(app.get('icon'), res))
info['icon_png'] = z.read(icon) except Exception: # noqa: BLE001 - any unreadable icon just means no icon
break pass
else: # adaptive icons are XML; fall back to the largest launcher PNG
pngs = sorted((n for n in names if n.endswith('.png') and 'ic_launcher' in n and 'foreground' not in n),
key=lambda n: z.getinfo(n).file_size)
if pngs:
info['icon_png'] = z.read(pngs[-1])
return info return info
def _icon_png(z, names, icons):
for icon in icons:
if icon.endswith('.png') and icon in names:
return z.read(icon)
# Adaptive icons are XML; fall back to the largest launcher PNG.
pngs = sorted((n for n in names if n.endswith('.png') and 'ic_launcher' in n and 'foreground' not in n),
key=lambda n: z.getinfo(n).file_size)
return z.read(pngs[-1]) if pngs else None
if __name__ == '__main__': if __name__ == '__main__':
import sys import sys
for p in sys.argv[1:]: for p in sys.argv[1:]: