mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 05:02:50 +02:00
Merge remote-tracking branch 'origin/bundle-deps' into frame-sideload-features
# Conflicts: # app/main.js
This commit is contained in:
commit
ccf5f3e123
5 files changed
+66
-24
No files matched your search
+16
-6
@@ -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
@@ -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() : "");
|
||||||
|
|||||||
@@ -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
@@ -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):
|
||||||
|
|||||||
+20
-10
@@ -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,16 +202,21 @@ 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:
|
||||||
|
info['icon_png'] = _icon_png(z, names, _icons(app.get('icon'), res))
|
||||||
|
except Exception: # noqa: BLE001 - any unreadable icon just means no icon
|
||||||
|
pass
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
def _icon_png(z, names, icons):
|
||||||
|
for icon in icons:
|
||||||
if icon.endswith('.png') and icon in names:
|
if icon.endswith('.png') and icon in names:
|
||||||
info['icon_png'] = z.read(icon)
|
return z.read(icon)
|
||||||
break
|
# Adaptive icons are XML; fall back to the largest launcher PNG.
|
||||||
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),
|
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)
|
key=lambda n: z.getinfo(n).file_size)
|
||||||
if pngs:
|
return z.read(pngs[-1]) if pngs else None
|
||||||
info['icon_png'] = z.read(pngs[-1])
|
|
||||||
return info
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
Reference in new issue
Block a user