dshm-compat 0.1.2 → 0.1.4
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.
- package/AGENTS.md +17 -0
- package/CHANGELOG.md +36 -0
- package/INSTALL.md +6 -6
- package/README.md +27 -10
- package/assets/qemu-x86_64/NOTICE.md +17 -0
- package/assets/qemu-x86_64/qemu-x86_64 +0 -0
- package/assets/qemu-x86_64/rootfs/lib/x86_64-linux-gnu/libc.so.6 +0 -0
- package/assets/qemu-x86_64/rootfs/lib/x86_64-linux-gnu/libgcc_s.so.1 +0 -0
- package/assets/qemu-x86_64/rootfs/lib64/ld-linux-x86-64.so.2 +0 -0
- package/docs/ARCHITECTURE.md +14 -8
- package/docs/RECIPES.md +50 -81
- package/index.js +46 -1
- package/lib/analyze.js +6 -1
- package/lib/convert.js +5 -4
- package/lib/elf.js +41 -0
- package/lib/guest.js +3 -2
- package/lib/plan.js +10 -0
- package/lib/release.js +118 -0
- package/lib/rules.js +7 -22
- package/lib/s2-native.js +254 -0
- package/lib/sidecar.js +174 -0
- package/lib/zip.js +59 -0
- package/package.json +3 -1
- package/lib/recipes/dsh-ops.js +0 -200
package/AGENTS.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# dshm-compat development rules
|
|
2
|
+
|
|
3
|
+
## No per-plugin adaptation
|
|
4
|
+
|
|
5
|
+
- **Never** add a production branch, recipe, rule, or hand-built artifact for one
|
|
6
|
+
package name. `dshm-compat` must adapt plugins through its generic engines.
|
|
7
|
+
- S2/native plugins go through `lib/s2-native.js`: detect the worker and the
|
|
8
|
+
standard runtime/spawn patterns, stage the worker, normalize the manifest, and
|
|
9
|
+
validate the result. Package names may appear only in tests and documentation
|
|
10
|
+
examples, never in `if (name === ...)` logic.
|
|
11
|
+
- If a plugin has a genuinely unique protocol, expose a **generic extension
|
|
12
|
+
point** in the plugin engine (patterns, hooks, or sidecar descriptor), not a
|
|
13
|
+
one-off patch.
|
|
14
|
+
- `dsh-ops` is a regression fixture. It proves the generic S2 adapter; it is not
|
|
15
|
+
a special case.
|
|
16
|
+
- Deliverables are the plugin and its generic tooling, not a converted tarball
|
|
17
|
+
for a single plugin.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.4
|
|
4
|
+
|
|
5
|
+
- The generic S2 adapter now accepts **x86_64** workers: it bundles
|
|
6
|
+
`qemu-x86_64` and a minimal x86_64 glibc rootfs, stages a shell wrapper, and
|
|
7
|
+
runs the worker under QEMU in the guest. No AArch64 rebuild is required for
|
|
8
|
+
plugins that publish a `linux-x64` runtime.
|
|
9
|
+
- If the package contains no worker, the adapter looks at the GitHub repository
|
|
10
|
+
release assets, prefers `linux-arm64` then `linux-x64`, extracts the archive
|
|
11
|
+
(ZIP or tar.gz), and uses the first AArch64/x86_64 ELF. No package name is
|
|
12
|
+
special-cased.
|
|
13
|
+
- Source-only native packages (for example a Rust `native/` tree with no
|
|
14
|
+
prebuilt runtime) now classify as S2 instead of S0, so the native adapter gets
|
|
15
|
+
a chance to build or acquire a worker.
|
|
16
|
+
- Verified end-to-end: `compat_convert("https://github.com/T-Auto/dsh-ops")`
|
|
17
|
+
with **no workerPath** acquires the upstream `linux-x64` release asset, wraps
|
|
18
|
+
it in QEMU, patches the runtime/spawn/peer/os-cpu gates, and validates.
|
|
19
|
+
- Offline regression suite: 27/27.
|
|
20
|
+
|
|
21
|
+
## 0.1.3
|
|
22
|
+
|
|
23
|
+
- Added a **generic S2 native adapter** (`lib/s2-native.js`). It discovers the
|
|
24
|
+
worker and the runtime matrix in the package, appends the DSHM target,
|
|
25
|
+
rewrites `${platform}-${arch}` templates, wraps standard `const/let argv =
|
|
26
|
+
[file]` spawn calls in `bash -c`, removes foreign `os`/`cpu` gates and
|
|
27
|
+
non-Linux optional payloads, and relaxes the DSH `peerDependencies` range.
|
|
28
|
+
There are no package-name branches and no built-in per-package rules.
|
|
29
|
+
- Added a generic guest sidecar bridge (`compat_sidecar` / `lib/sidecar.js`):
|
|
30
|
+
stage a Linux/AArch64 worker from a converted tarball into
|
|
31
|
+
`$DSH_HOME/dshm-compat/sidecars/<id>`, list/probe it, and call it with one
|
|
32
|
+
JSON request/response through the Alpine guest.
|
|
33
|
+
- `compat_plan` now infers `rust`, `c/c++`, or `node-gyp` from source files in
|
|
34
|
+
the report instead of relying on a package-specific rule.
|
|
35
|
+
- S2 conversion dispatches on the strategy and the generic patterns, not on a
|
|
36
|
+
package name; GitHub specs work through the same path.
|
|
37
|
+
- Offline regression suite: 25/25.
|
|
38
|
+
|
|
3
39
|
## 0.1.2
|
|
4
40
|
|
|
5
41
|
- Added `compat_doctor`: one read-only diagnosis of DSHM mode, core version,
|
package/INSTALL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Installing dshm-compat
|
|
2
2
|
|
|
3
|
-
The artifact is a normal DSH bundle package: `dshm-compat-0.1.
|
|
4
|
-
It is host-only, has no runtime dependencies, and adds
|
|
3
|
+
The artifact is a normal DSH bundle package: `dshm-compat-0.1.4.tgz`.
|
|
4
|
+
It is host-only, has no runtime dependencies, and adds ten agent tools.
|
|
5
5
|
|
|
6
6
|
## A. DSHM (the tablet)
|
|
7
7
|
|
|
@@ -15,7 +15,7 @@ input calls `pnpm view` before installing and fails with
|
|
|
15
15
|
`Command failed with EACCES: pnpm view ... spawn pnpm EACCES`. Use one of these
|
|
16
16
|
instead:
|
|
17
17
|
|
|
18
|
-
- ask the DSHM Agent to run `pnpm add dshm-compat@0.1.
|
|
18
|
+
- ask the DSHM Agent to run `pnpm add dshm-compat@0.1.4` in its bash tool (the
|
|
19
19
|
bundled pnpm shim routes that command to the host install queue); or
|
|
20
20
|
- install from a GitHub repository URL; or
|
|
21
21
|
- after any working install, use the plugin's own `compat_install` tool for
|
|
@@ -28,7 +28,7 @@ not accept local paths, `file:` or `http://` specs.
|
|
|
28
28
|
|
|
29
29
|
### A2. HTTPS tarball
|
|
30
30
|
|
|
31
|
-
Put `dshm-compat-0.1.
|
|
31
|
+
Put `dshm-compat-0.1.4.tgz` on any HTTPS host (GitHub Release, Gitee Release,
|
|
32
32
|
object storage) and paste the `.tgz` URL into the same Add plugin input. DSHM's
|
|
33
33
|
installer accepts tarball specs; if the current market UI refuses a plain URL,
|
|
34
34
|
use A1.
|
|
@@ -38,7 +38,7 @@ use A1.
|
|
|
38
38
|
If `hdc` is available and the app can read the path:
|
|
39
39
|
|
|
40
40
|
```sh
|
|
41
|
-
hdc file send dshm-compat-0.1.
|
|
41
|
+
hdc file send dshm-compat-0.1.4.tgz /data/storage/el2/base/haps/entry/files/dshm-compat-0.1.4.tgz
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
Then enter the absolute path (or `file:/...`) in the Add plugin input. This
|
|
@@ -47,7 +47,7 @@ depends on the DSHM build's local-file support; A1 is safer.
|
|
|
47
47
|
## B. Desktop DSH (for testing only)
|
|
48
48
|
|
|
49
49
|
```powershell
|
|
50
|
-
& 'D:\安装\dsh-desktop\resources\runtime\cli\bin\dsh.cmd' plugin --profile desktop add 'D:\dsh\dsh-26-09-30\dshm-compat\dshm-compat-0.1.
|
|
50
|
+
& 'D:\安装\dsh-desktop\resources\runtime\cli\bin\dsh.cmd' plugin --profile desktop add 'D:\dsh\dsh-26-09-30\dshm-compat\dshm-compat-0.1.4.tgz'
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
If the profile reports `ERR_PNPM_UNEXPECTED_VIRTUAL_STORE`, its `node_modules`
|
package/README.md
CHANGED
|
@@ -5,10 +5,12 @@ them through the least invasive strategy that can work.
|
|
|
5
5
|
|
|
6
6
|
## Current phase
|
|
7
7
|
|
|
8
|
-
0.1.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
0.1.4 adds a generic S2 native adapter (including an x86_64 worker + bundled
|
|
9
|
+
QEMU fallback and GitHub Release worker acquisition), `compat_doctor`,
|
|
10
|
+
rollback-safe local installs, and codeload fallbacks for GitHub. Phase 1 ships
|
|
11
|
+
the analyzer, the S0/S1 converter, the generic S2 engine, guest
|
|
12
|
+
probing/provisioning, the DSHM install-queue transport, and a profile-local
|
|
13
|
+
fallback for artifacts the host queue rejects:
|
|
12
14
|
|
|
13
15
|
- `compat_analyze` downloads an npm/GitHub/local package and classifies it.
|
|
14
16
|
- `compat_plan` returns the concrete conversion plan without changing anything.
|
|
@@ -17,12 +19,19 @@ and a profile-local fallback for artifacts the host queue rejects:
|
|
|
17
19
|
install lifecycle scripts, client platform), vendors missing pure-JS runtime
|
|
18
20
|
dependencies into a flat `node_modules`, validates the resulting tarball, and
|
|
19
21
|
repacks it as a DSHM tarball; for S1 it removes a native dependency that the
|
|
20
|
-
DSHM core already provides. For
|
|
21
|
-
|
|
22
|
-
`
|
|
22
|
+
DSHM core already provides. For S2 it uses the generic native adapter: it
|
|
23
|
+
detects the worker, `PLATFORMS` matrix, `${platform}-${arch}` templates,
|
|
24
|
+
standard spawn calls, and foreign `os`/`cpu`/peer gates. A worker can come from
|
|
25
|
+
`workerPath`, the package's `runtime/linux-*`, or the GitHub Release assets
|
|
26
|
+
(linux-arm64 first, then linux-x64). An x86_64 worker is executed through the
|
|
27
|
+
bundled `qemu-x86_64` and glibc rootfs, so no AArch64 rebuild is required.
|
|
28
|
+
Only when none of these exist does it return `needs-native-build` with the
|
|
29
|
+
cross-build command. Package names are not special-cased.
|
|
23
30
|
- `compat_guest_probe` reports the Alpine guest kernel/packages/tools.
|
|
24
31
|
- `compat_guest_provision` installs a reviewed allowlist of Alpine packages in
|
|
25
32
|
the guest (`confirm:true` required).
|
|
33
|
+
- `compat_sidecar` stages a native Linux/AArch64 worker from a converted
|
|
34
|
+
tarball and lists/probes/calls it in the guest with JSON stdin/stdout.
|
|
26
35
|
- `compat_status` lists stored reports and install attempts.
|
|
27
36
|
- `compat_doctor` checks DSHM mode, core, profile/bundle, queue, state, and the
|
|
28
37
|
guest in one read-only call.
|
|
@@ -40,16 +49,24 @@ conversion.
|
|
|
40
49
|
|
|
41
50
|
See [architecture](docs/ARCHITECTURE.md) and [conversion recipes](docs/RECIPES.md).
|
|
42
51
|
|
|
52
|
+
## Development policy
|
|
53
|
+
|
|
54
|
+
There are **no per-plugin production branches**. S2 native plugins go through
|
|
55
|
+
the generic `lib/s2-native.js` engine; package names appear only in tests and
|
|
56
|
+
documentation examples. If a plugin has a unique protocol, add a generic
|
|
57
|
+
extension point (pattern, hook, or sidecar descriptor) instead of a one-off
|
|
58
|
+
patch. See [AGENTS.md](AGENTS.md).
|
|
59
|
+
|
|
43
60
|
## CLI
|
|
44
61
|
|
|
45
62
|
The same converter runs on a PC without installing the DSH plugin:
|
|
46
63
|
|
|
47
64
|
```sh
|
|
48
65
|
node bin/dshm-compat.mjs analyze dsh-m
|
|
49
|
-
node bin/dshm-compat.mjs plan
|
|
66
|
+
node bin/dshm-compat.mjs plan some-native-plugin
|
|
50
67
|
node bin/dshm-compat.mjs convert dsh-cache-billing --no-vendor
|
|
51
|
-
node bin/dshm-compat.mjs convert
|
|
52
|
-
node bin/dshm-compat.mjs scan dsh-m
|
|
68
|
+
node bin/dshm-compat.mjs convert some-native-plugin --worker ./some-native-worker
|
|
69
|
+
node bin/dshm-compat.mjs scan dsh-m some-native-plugin some-plugin
|
|
53
70
|
```
|
|
54
71
|
|
|
55
72
|
`convert` writes the tarball under `$DSH_HOME/dshm-compat/builds` (override with
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Bundled QEMU x86_64 user-mode runtime
|
|
2
|
+
|
|
3
|
+
This directory is a generic fallback for S2 plugins that ship a `linux-x64`
|
|
4
|
+
native worker but no AArch64 build. `qemu-x86_64` executes the x86_64 worker in
|
|
5
|
+
the DSHM Alpine guest; the `rootfs/` directory supplies the glibc loader and
|
|
6
|
+
libraries the worker links against.
|
|
7
|
+
|
|
8
|
+
- `qemu-x86_64`: Alpine 3.22 `qemu-x86_64` 10.0.0-r1 (aarch64 build), GPL-2.0.
|
|
9
|
+
Source: https://pkgs.alpinelinux.org/package/v3.22/community/aarch64/qemu-x86_64
|
|
10
|
+
- `rootfs/lib64/ld-linux-x86-64.so.2`,
|
|
11
|
+
`rootfs/lib/x86_64-linux-gnu/libc.so.6`,
|
|
12
|
+
`rootfs/lib/x86_64-linux-gnu/libgcc_s.so.1`: Ubuntu 24.04 x86_64 base rootfs,
|
|
13
|
+
LGPL-2.1/GPL-2.0 with GCC runtime exception.
|
|
14
|
+
Source: https://cdimage.ubuntu.com/ubuntu-base/releases/24.04/release/
|
|
15
|
+
|
|
16
|
+
The fallback is optional: when a plugin has a native AArch64 worker, the generic
|
|
17
|
+
S2 adapter stages that directly and these files are not used.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -37,7 +37,7 @@ spec ──► resolve (npm / GitHub / local .tgz)
|
|
|
37
37
|
──► extract (ustar/pax, no dependency)
|
|
38
38
|
──► analyze (manifest, native files, scripts, platform/Electron scan, core inventory)
|
|
39
39
|
──► plan (S0..S4 + concrete steps)
|
|
40
|
-
──► convert (S0/S1
|
|
40
|
+
──► convert (S0/S1 manifest, S2 generic native adapter)
|
|
41
41
|
──► pack (ustar writer)
|
|
42
42
|
──► install queue (DSHM only)
|
|
43
43
|
```
|
|
@@ -50,14 +50,18 @@ spec ──► resolve (npm / GitHub / local .tgz)
|
|
|
50
50
|
| `lib/tar.js` | ustar/pax extraction with traversal and size guards |
|
|
51
51
|
| `lib/analyze.js` | classification evidence, strategy, confidence, next action |
|
|
52
52
|
| `lib/core.js` | locate `$DSH_HOME/cores/<version>/node_modules` and compare runtime dependencies |
|
|
53
|
-
| `lib/rules.js` |
|
|
54
|
-
| `lib/convert.js` | S0 manifest rewrite/repack and vendoring; S1 native-dependency removal |
|
|
53
|
+
| `lib/rules.js` | operator-supplied custom rules only; no built-in per-package rules |
|
|
54
|
+
| `lib/convert.js` | S0 manifest rewrite/repack and vendoring; S1 native-dependency removal; dispatch to the S2 adapter |
|
|
55
|
+
| `lib/s2-native.js` | generic S2 worker/runtime/spawn/peer adapter |
|
|
56
|
+
| `lib/release.js` | GitHub Release Linux worker discovery and extraction |
|
|
57
|
+
| `lib/zip.js` | dependency-free ZIP reader for release assets |
|
|
55
58
|
| `lib/vendor.js` | flat `node_modules` vendoring for missing pure-JS dependencies |
|
|
56
59
|
| `lib/validate.js` | post-conversion manifest/entry/dependency validation |
|
|
57
60
|
| `lib/plan.js` | user-visible conversion plan for every strategy |
|
|
58
61
|
| `lib/guest.js` | read-only Alpine guest probe through `subprocess` |
|
|
59
62
|
| `lib/install.js` | DSHM install-queue producer with polling and cancellation |
|
|
60
63
|
| `lib/local-install.js` | profile-local tarball install with staging, backup, and rollback |
|
|
64
|
+
| `lib/sidecar.js` | generic guest worker staging and JSON-stdio calls |
|
|
61
65
|
| `lib/doctor.js` | one-shot DSHM/core/profile/queue/guest diagnosis |
|
|
62
66
|
| `lib/state.js` | bounded report/install index and atomic writes |
|
|
63
67
|
| `lib/pack.js` | dependency-free ustar/gzip writer |
|
|
@@ -72,6 +76,7 @@ spec ──► resolve (npm / GitHub / local .tgz)
|
|
|
72
76
|
| `compat_convert` | writes a converted tarball; optionally submits it to the install queue |
|
|
73
77
|
| `compat_guest_probe` | read-only guest command |
|
|
74
78
|
| `compat_guest_provision` | installs allowlisted Alpine packages in the guest |
|
|
79
|
+
| `compat_sidecar` | stages/lists/probes/calls a JSON-stdio native worker in the guest |
|
|
75
80
|
| `compat_doctor` | read-only DSHM/core/profile/queue/guest diagnosis |
|
|
76
81
|
| `compat_install` | writes one install-queue request; runs third-party code |
|
|
77
82
|
| `compat_status` | read-only |
|
|
@@ -79,11 +84,12 @@ spec ──► resolve (npm / GitHub / local .tgz)
|
|
|
79
84
|
## Phase status
|
|
80
85
|
|
|
81
86
|
- **Phase 1 (done):** analyzer, batch triage, S0 manifest converter + flat
|
|
82
|
-
dependency vendoring + output validation, S1 manifest substitution,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
dependency vendoring + output validation, S1 manifest substitution, generic S2
|
|
88
|
+
native adapter (AArch64 worker, x86_64 worker through bundled QEMU, GitHub
|
|
89
|
+
Release acquisition), dependency preflight, guest probe/provisioning, worker
|
|
90
|
+
execution primitive, install queue, offline tests.
|
|
91
|
+
- **Phase 2 (next):** on-device guest verification, automatic worker build
|
|
92
|
+
(desktop/CI), nested version-conflict vendoring, sidecar RPC.
|
|
87
93
|
- **Phase 3:** WASM/remote fallback, market UI entry, compatibility registry
|
|
88
94
|
updates.
|
|
89
95
|
|
package/docs/RECIPES.md
CHANGED
|
@@ -1,104 +1,73 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Generic conversion patterns
|
|
2
2
|
|
|
3
|
-
These are the
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
These are the patterns behind `S1`/`S2` plans. They are **generic**: no
|
|
4
|
+
production branch is keyed on a package name. A package name may appear only in
|
|
5
|
+
tests or as a documentation example.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## S2: native worker
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
The generic adapter in `lib/s2-native.js` recognizes the common shape of a
|
|
10
|
+
native DSH plugin:
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
1. A worker binary under `runtime/<platform>-<arch>/<name>`, a worker passed
|
|
13
|
+
explicitly with `workerPath`, or a Linux asset on the repository's GitHub
|
|
14
|
+
Release (arm64 preferred, then x64).
|
|
15
|
+
2. A JS module exporting a `PLATFORMS` array and interpolating
|
|
16
|
+
`${platform}-${arch}` into runtime paths.
|
|
17
|
+
3. Spawn calls of the form `const argv = [file]` or `let argv = [file]`.
|
|
18
|
+
4. Optional `package.json` `os`/`cpu` gates and a DSH `peerDependencies` range.
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
For each match it:
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
- appends `linux-arm64` (AArch64 worker) or `linux-x64` (x86_64 worker) to
|
|
23
|
+
`PLATFORMS` and injects a `resolveTarget()` helper so `openharmony`/arm64
|
|
24
|
+
resolves to that target in both `resolveWorker` and `tryResolveWorker`;
|
|
25
|
+
- replaces `${platform}-${arch}` **inside template literals** with
|
|
26
|
+
`${resolveTarget(platform, arch)}`;
|
|
27
|
+
- wraps every standard spawn call in
|
|
28
|
+
`['bash', '-c', 'exec "$0" "$@"', file]`, which `dshm-vm-bash` routes to the
|
|
29
|
+
Alpine guest;
|
|
30
|
+
- removes `os`/`cpu`, drops win32/darwin optional payloads, and relaxes
|
|
31
|
+
`@deepseek-ai/dsh` to `workspace:*`;
|
|
32
|
+
- adds `openharmony`/target/arch to `dsh-plugin.json` component targets;
|
|
33
|
+
- stages the worker at `runtime/<target>/<worker>` with a matching
|
|
34
|
+
`provenance.json` (`version`, `target`, `executable`, `sha256`).
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- look for `runtime/linux-arm64/dsh-ops-worker`.
|
|
36
|
+
An x86_64 worker is staged as a shell wrapper plus `qemu-x86_64`, a minimal
|
|
37
|
+
x86_64 glibc rootfs, and the original `<worker>-x64`; the wrapper runs QEMU in
|
|
38
|
+
the guest. This makes a `linux-x64` runtime usable without an AArch64 rebuild.
|
|
29
39
|
|
|
30
|
-
|
|
40
|
+
If no worker is supplied, none is found in the package, and the GitHub Release
|
|
41
|
+
has no Linux asset, the result is `needs-native-build` with the cross-build
|
|
42
|
+
command. The adapter never builds the worker itself.
|
|
31
43
|
|
|
32
|
-
|
|
33
|
-
- `dsh-plugin.json` `platforms`, `architectures`, and `targets` for each
|
|
34
|
-
`x-components` entry.
|
|
44
|
+
### Building the worker
|
|
35
45
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
From a Linux build host or CI:
|
|
46
|
+
For a Rust crate under `native/`:
|
|
39
47
|
|
|
40
48
|
```sh
|
|
41
|
-
|
|
42
|
-
#
|
|
43
|
-
cross build --release --locked --manifest-path native/Cargo.toml --target aarch64-unknown-linux-musl
|
|
44
|
-
# or
|
|
49
|
+
cargo build --release --locked --manifest-path native/Cargo.toml --target aarch64-unknown-linux-musl
|
|
50
|
+
# or, with a Zig-based cross linker:
|
|
45
51
|
cargo zigbuild --release --locked --manifest-path native/Cargo.toml --target aarch64-unknown-linux-musl
|
|
46
52
|
```
|
|
47
53
|
|
|
48
|
-
The
|
|
49
|
-
|
|
50
|
-
`serde`, `serde_json`, `uuid`, `regex`.
|
|
51
|
-
|
|
52
|
-
Stage the result as `runtime/linux-arm64/dsh-ops-worker` with a matching
|
|
53
|
-
`provenance.json` (`version`, `target`, `executable`, `sha256`).
|
|
54
|
+
The result must be a static AArch64 musl ELF: `e_machine == 183`, no
|
|
55
|
+
`PT_INTERP`. Dynamic glibc binaries will not start in the Alpine guest.
|
|
54
56
|
|
|
55
|
-
###
|
|
57
|
+
### Generic sidecar
|
|
56
58
|
|
|
57
|
-
|
|
59
|
+
A native worker that follows one-JSON-request / one-JSON-response can also be
|
|
60
|
+
staged and called without patching the plugin's own tool layer:
|
|
58
61
|
|
|
59
|
-
```
|
|
60
|
-
|
|
62
|
+
```json
|
|
63
|
+
compat_sidecar { "action": "stage", "tarball": "<converted.tgz>", "workerPath": "runtime/linux-arm64/worker", "id": "my-worker" }
|
|
64
|
+
compat_sidecar { "action": "probe", "id": "my-worker" }
|
|
65
|
+
compat_sidecar { "action": "call", "id": "my-worker", "input": { "ping": true }, "confirm": true }
|
|
61
66
|
```
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
argv: ['bash', '-c', 'exec "$0"', file]
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
The worker path is under the profile's 9p share, so the guest sees the same
|
|
70
|
-
absolute path. If exec from 9p fails on a device, copy the worker to
|
|
71
|
-
`/tmp/dsh-ops-worker` once (hash-keyed) and execute that copy.
|
|
72
|
-
|
|
73
|
-
### 4. Shell component
|
|
74
|
-
|
|
75
|
-
`lib/shells/resolve.js` treats every non-win32 host as POSIX and probes the
|
|
76
|
-
**host** for `/bin/bash`. DSHM's host has no bash. Patch the DSHM branch to
|
|
77
|
-
report the guest's `/bin/bash` as available; `ops_bash` then spawns
|
|
78
|
-
`bash -c`, which `dshm-vm-bash` routes into the guest.
|
|
79
|
-
|
|
80
|
-
Disable the PowerShell rung: the pwsh payload is win32-only.
|
|
81
|
-
|
|
82
|
-
### 5. PTC vs file
|
|
83
|
-
|
|
84
|
-
`lib/ptc/worker.js` calls `sandbox.confine()` for non-`danger-full-access`
|
|
85
|
-
policies. If DSHM's sandbox provider cannot confine the worker, PTC tools fail
|
|
86
|
-
closed. The `file` component uses policy checks but no `sandbox.confine`, so
|
|
87
|
-
port `ops_*` file tools first and treat PTC as a later optimization.
|
|
88
|
-
|
|
89
|
-
### 6. Verification
|
|
90
|
-
|
|
91
|
-
Run on the device:
|
|
92
|
-
|
|
93
|
-
- `ops_stat` on a workspace file;
|
|
94
|
-
- `ops_grep` over a small tree;
|
|
95
|
-
- `ops_write_file` then `ops_stat` (check the new revision);
|
|
96
|
-
- `ops_copy` / `ops_move` (9p rename/atomicity);
|
|
97
|
-
- a failure case with a read-only policy.
|
|
98
|
-
|
|
99
|
-
Watch for `cap-std`/`rustix` failures on 9p (`openat2`, `statx`, `renameat2`,
|
|
100
|
-
`flock`). If those fail, the fallback is a JS reimplementation of the worker
|
|
101
|
-
protocol rather than a guest port.
|
|
68
|
+
Staging checks the worker is an AArch64 ELF and puts it under
|
|
69
|
+
`$DSH_HOME/dshm-compat/sidecars/<id>/`. Calls run through
|
|
70
|
+
`bash -c 'exec "$0" "$@"'`, which `dshm-vm-bash` routes into Alpine.
|
|
102
71
|
|
|
103
72
|
## Common native dependencies
|
|
104
73
|
|
package/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { probeGuest, provisionGuest } from './lib/guest.js'
|
|
|
15
15
|
import { enqueueInstall } from './lib/install.js'
|
|
16
16
|
import { installLocalTarball, isLocalSpec } from './lib/local-install.js'
|
|
17
17
|
import { buildPlan } from './lib/plan.js'
|
|
18
|
+
import { callSidecar, listSidecars, probeSidecar, stageSidecar } from './lib/sidecar.js'
|
|
18
19
|
import { recordInstall, recordReport, status } from './lib/state.js'
|
|
19
20
|
|
|
20
21
|
export const name = 'dshm-compat'
|
|
@@ -152,7 +153,7 @@ export function apply(ctx) {
|
|
|
152
153
|
profileDir: { type: 'string', description: 'Absolute DSHM profile directory for install:true. Omit to let the host choose.' },
|
|
153
154
|
confirm: { type: 'boolean', description: 'Required when install is true. Confirms that installing the converted package runs third-party code.' },
|
|
154
155
|
vendor: { type: 'boolean', description: 'Vendor missing pure-JS runtime dependencies into the tarball. Default true.' },
|
|
155
|
-
workerPath: { type: 'string', description: 'Absolute path to a prebuilt AArch64 Linux worker for
|
|
156
|
+
workerPath: { type: 'string', description: 'Absolute path to a prebuilt AArch64 Linux worker for an S2 plugin. Omit to let the generic adapter return the build command.' },
|
|
156
157
|
allowLocal: { type: 'boolean', description: 'When install is true, unpack the converted tarball into the profile instead of the host queue. Default true; set false to force the queue.' },
|
|
157
158
|
},
|
|
158
159
|
required: ['spec'],
|
|
@@ -232,6 +233,50 @@ export function apply(ctx) {
|
|
|
232
233
|
},
|
|
233
234
|
), 'compat_guest_provision')
|
|
234
235
|
|
|
236
|
+
register(definition(
|
|
237
|
+
'compat_sidecar',
|
|
238
|
+
'Stage and call a native Linux/AArch64 worker in the DSHM Alpine guest. Actions: stage (extract a worker from a converted tarball into $DSH_HOME/dshm-compat/sidecars/<id>), list, probe (run --version), call (one JSON request on stdin, one JSON value on stdout; requires confirm:true). '
|
|
239
|
+
+ 'This is the generic S2 bridge; the worker runs through bash -c and is routed to the guest by dshm-vm-bash.',
|
|
240
|
+
{
|
|
241
|
+
type: 'object',
|
|
242
|
+
properties: {
|
|
243
|
+
action: { type: 'string', enum: ['stage', 'list', 'probe', 'call'] },
|
|
244
|
+
id: { type: 'string', description: 'Sidecar id (letters, digits, dot, underscore, dash).' },
|
|
245
|
+
tarball: { type: 'string', description: 'Converted package tarball containing the worker.' },
|
|
246
|
+
workerPath: { type: 'string', description: 'Worker path inside the tarball, e.g. runtime/linux-arm64/<worker>.' },
|
|
247
|
+
input: { type: 'object', additionalProperties: true, description: 'JSON request for action=call. Default {}.' },
|
|
248
|
+
timeoutMs: { type: 'integer', description: 'Per-call timeout. Default 120000.' },
|
|
249
|
+
confirm: { type: 'boolean', description: 'Required for action=call: confirms that the worker runs native code in the guest.' },
|
|
250
|
+
},
|
|
251
|
+
required: ['action'],
|
|
252
|
+
additionalProperties: false,
|
|
253
|
+
},
|
|
254
|
+
async (args, exec) => {
|
|
255
|
+
try {
|
|
256
|
+
if (args?.action === 'stage') {
|
|
257
|
+
if (typeof args.tarball !== 'string' || typeof args.workerPath !== 'string') {
|
|
258
|
+
return { status: 'error', error: { code: 'INVALID_ARGUMENT', message: 'stage requires tarball and workerPath' } }
|
|
259
|
+
}
|
|
260
|
+
const result = stageSidecar(args.tarball, args.workerPath, { id: args.id })
|
|
261
|
+
return result.ok ? { status: 'ok', ...result } : { status: 'error', ...result }
|
|
262
|
+
}
|
|
263
|
+
if (args?.action === 'list') return { status: 'ok', sidecars: listSidecars() }
|
|
264
|
+
if (args?.action === 'probe') {
|
|
265
|
+
if (typeof args.id !== 'string') return { status: 'error', error: { code: 'INVALID_ARGUMENT', message: 'probe requires id' } }
|
|
266
|
+
return { status: 'ok', ...(await probeSidecar(ctx, args.id, { timeoutMs: args.timeoutMs, signal: exec?.signal })) }
|
|
267
|
+
}
|
|
268
|
+
if (args?.action === 'call') {
|
|
269
|
+
if (typeof args.id !== 'string') return { status: 'error', error: { code: 'INVALID_ARGUMENT', message: 'call requires id' } }
|
|
270
|
+
if (args.confirm !== true) return { status: 'confirmation-required', message: 'Re-run with confirm:true to execute the worker in the guest.' }
|
|
271
|
+
return { status: 'ok', ...(await callSidecar(ctx, args.id, args.input ?? {}, { timeoutMs: args.timeoutMs, signal: exec?.signal })) }
|
|
272
|
+
}
|
|
273
|
+
return { status: 'error', error: { code: 'INVALID_ACTION', message: `unknown sidecar action: ${String(args?.action)}` } }
|
|
274
|
+
} catch (error) {
|
|
275
|
+
return errorResult(error)
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
), 'compat_sidecar')
|
|
279
|
+
|
|
235
280
|
register(definition(
|
|
236
281
|
'compat_install',
|
|
237
282
|
'Install an already-built plugin artifact. Local .tgz paths (or file:/link: paths) are unpacked into the owning DSH profile and its bundle list, because DSHM\'s host queue rejects local specs; npm names and GitHub repositories still go through the host install queue. '
|
package/lib/analyze.js
CHANGED
|
@@ -245,10 +245,11 @@ export async function analyze(spec, options = {}) {
|
|
|
245
245
|
confidence = 'high'
|
|
246
246
|
reasons.push('the archive carries only win32 payloads and no C/C++/Rust source to rebuild')
|
|
247
247
|
next.push('ask the plugin author for source or a linux-arm64 build')
|
|
248
|
-
} else if (nativeFiles.length > 0 || unknownNativeDeps.length > 0 || electronSurface) {
|
|
248
|
+
} else if (nativeFiles.length > 0 || unknownNativeDeps.length > 0 || electronSurface || hasNativeSource) {
|
|
249
249
|
strategy = 'S2'
|
|
250
250
|
confidence = 'medium'
|
|
251
251
|
if (nativeFiles.length > 0) reasons.push(`${nativeFiles.length} native file(s) need a linux-arm64 runtime outside the DSHM host process`)
|
|
252
|
+
if (hasNativeSource && nativeFiles.length === 0) reasons.push('native source needs a Linux/guest build; no prebuilt runtime is present')
|
|
252
253
|
if (unknownNativeDeps.length > 0) reasons.push(`unrecognized native dependencies: ${unknownNativeDeps.map(dep => dep.name).join(', ')}`)
|
|
253
254
|
if (Object.keys(installScripts).length > 0) reasons.push(`install scripts: ${Object.keys(installScripts).join(', ')}`)
|
|
254
255
|
if (electronSurface) reasons.push(`Electron API surface detected (${scan.electron} reference(s)); needs a shim or a sidecar UI`)
|
|
@@ -360,6 +361,10 @@ export async function analyze(spec, options = {}) {
|
|
|
360
361
|
},
|
|
361
362
|
nativeDeps,
|
|
362
363
|
unknownNativeDeps: unknownNativeDeps.map(dep => dep.name),
|
|
364
|
+
nativeSource: hasNativeSource,
|
|
365
|
+
rustSource: files.some(file => /(?:^|\/)Cargo\.toml$/.test(file.path) || /\.rs$/.test(file.path)),
|
|
366
|
+
cSource: files.some(file => /\.(?:c|cc|cpp)$/.test(file.path)),
|
|
367
|
+
nodeGyp: files.some(file => /(?:^|\/)binding\.gyp$/.test(file.path)),
|
|
363
368
|
dependencies: {
|
|
364
369
|
core: dependencyCheck.core,
|
|
365
370
|
present: dependencyCheck.present.slice(0, 100),
|
package/lib/convert.js
CHANGED
|
@@ -47,12 +47,13 @@ function safeName(name) {
|
|
|
47
47
|
*/
|
|
48
48
|
export async function convert(spec, options = {}) {
|
|
49
49
|
const report = options.report ?? await analyze(spec, { textScan: false, signal: options.signal, registry: options.registry })
|
|
50
|
-
if (report.
|
|
51
|
-
const {
|
|
52
|
-
return await
|
|
50
|
+
if (report.strategy === 'S2') {
|
|
51
|
+
const { convertS2Native } = await import('./s2-native.js')
|
|
52
|
+
return await convertS2Native(spec, {
|
|
53
53
|
report,
|
|
54
54
|
workerPath: options.workerPath,
|
|
55
55
|
workerBinary: options.workerBinary,
|
|
56
|
+
workerName: options.workerName,
|
|
56
57
|
signal: options.signal,
|
|
57
58
|
})
|
|
58
59
|
}
|
|
@@ -62,7 +63,7 @@ export async function convert(spec, options = {}) {
|
|
|
62
63
|
strategy: report.strategy,
|
|
63
64
|
reasons: report.reasons,
|
|
64
65
|
next: report.next,
|
|
65
|
-
message: `strategy ${report.strategy}
|
|
66
|
+
message: `strategy ${report.strategy} needs a recipe or an external native build; see next`,
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
if (report.signals?.hasDsh !== true) {
|
package/lib/elf.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal AArch64 ELF checks shared by the generic S2 adapter and the generic
|
|
3
|
+
* sidecar staging path.
|
|
4
|
+
*
|
|
5
|
+
* @module dshm-compat/elf
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** @param {Buffer} buffer */
|
|
9
|
+
export function isAarch64Elf(buffer) {
|
|
10
|
+
return buffer.length >= 64
|
|
11
|
+
&& buffer[0] === 0x7f && buffer[1] === 0x45 && buffer[2] === 0x4c && buffer[3] === 0x46
|
|
12
|
+
&& buffer[4] === 2 && buffer[5] === 1
|
|
13
|
+
&& buffer.readUInt16LE(18) === 183
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** @param {Buffer} buffer */
|
|
17
|
+
export function isX64Elf(buffer) {
|
|
18
|
+
return buffer.length >= 64
|
|
19
|
+
&& buffer[0] === 0x7f && buffer[1] === 0x45 && buffer[2] === 0x4c && buffer[3] === 0x46
|
|
20
|
+
&& buffer[4] === 2 && buffer[5] === 1
|
|
21
|
+
&& buffer.readUInt16LE(18) === 62
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** @param {Buffer} buffer @returns {'arm64'|'x64'|undefined} */
|
|
25
|
+
export function elfArch(buffer) {
|
|
26
|
+
if (isAarch64Elf(buffer)) return 'arm64'
|
|
27
|
+
if (isX64Elf(buffer)) return 'x64'
|
|
28
|
+
return undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @param {Buffer} buffer */
|
|
32
|
+
export function elfSummary(buffer) {
|
|
33
|
+
if (!isAarch64Elf(buffer)) return undefined
|
|
34
|
+
return {
|
|
35
|
+
class: 64,
|
|
36
|
+
endian: 'little',
|
|
37
|
+
machine: 'AArch64',
|
|
38
|
+
type: buffer.readUInt16LE(16) === 3 ? 'DYN' : buffer.readUInt16LE(16) === 2 ? 'EXEC' : String(buffer.readUInt16LE(16)),
|
|
39
|
+
bytes: buffer.length,
|
|
40
|
+
}
|
|
41
|
+
}
|
package/lib/guest.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module dshm-compat/guest
|
|
10
10
|
*/
|
|
11
|
+
import { isAbsolute } from 'node:path'
|
|
12
|
+
|
|
11
13
|
const PROBE_SCRIPT = [
|
|
12
14
|
'echo DSHM_COMPAT_PROBE_BEGIN',
|
|
13
15
|
'uname -srm 2>/dev/null || true',
|
|
@@ -19,7 +21,6 @@ const PROBE_SCRIPT = [
|
|
|
19
21
|
'command -v python3 || true',
|
|
20
22
|
'echo DSHM_COMPAT_PROBE_END',
|
|
21
23
|
].join('\n')
|
|
22
|
-
|
|
23
24
|
/** Packages the provisioning tool may install. */
|
|
24
25
|
export const PROVISION_PACKAGES = Object.freeze([
|
|
25
26
|
'bash',
|
|
@@ -98,7 +99,7 @@ export async function runGuest(ctx, script, options = {}) {
|
|
|
98
99
|
* @param {{ timeoutMs?: number, signal?: AbortSignal, cwd?: string }} [options]
|
|
99
100
|
*/
|
|
100
101
|
export async function runGuestFile(ctx, file, args = [], input = '', options = {}) {
|
|
101
|
-
if (typeof file !== 'string' || !file
|
|
102
|
+
if (typeof file !== 'string' || !isAbsolute(file)) {
|
|
102
103
|
return { status: 'invalid', reason: 'worker path must be absolute and visible inside the guest' }
|
|
103
104
|
}
|
|
104
105
|
const subprocess = ctx.get('subprocess')
|
package/lib/plan.js
CHANGED
|
@@ -18,6 +18,16 @@ function record(value) {
|
|
|
18
18
|
function nativeBuildOf(report) {
|
|
19
19
|
const rule = record(report.rule)
|
|
20
20
|
if (record(rule?.nativeBuild) !== undefined) return rule.nativeBuild
|
|
21
|
+
const signals = record(report.signals) ?? {}
|
|
22
|
+
if (signals.nodeGyp === true) {
|
|
23
|
+
return { language: 'node-gyp', target: 'aarch64-unknown-linux-musl', notes: 'build in the Alpine guest with build-base and python3' }
|
|
24
|
+
}
|
|
25
|
+
if (signals.rustSource === true) {
|
|
26
|
+
return { language: 'rust', target: 'aarch64-unknown-linux-musl' }
|
|
27
|
+
}
|
|
28
|
+
if (signals.cSource === true) {
|
|
29
|
+
return { language: 'c/c++', target: 'aarch64-unknown-linux-musl', notes: 'build in the Alpine guest' }
|
|
30
|
+
}
|
|
21
31
|
const native = Array.isArray(report.files?.native) ? report.files.native : []
|
|
22
32
|
if (native.some(file => typeof file.path === 'string' && file.path.endsWith('binding.gyp'))) {
|
|
23
33
|
return { language: 'node-gyp', target: 'aarch64-unknown-linux-musl', notes: 'build in the Alpine guest with build-base and python3' }
|