@perryts/perry 0.5.1220 → 0.5.1519

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/README.md CHANGED
@@ -23,13 +23,26 @@ Installing picks the right prebuilt binary for your platform automatically — `
23
23
  | Linux x64 (musl / Alpine) | `@perryts/perry-linux-x64-musl` |
24
24
  | Linux arm64 (musl / Alpine) | `@perryts/perry-linux-arm64-musl` |
25
25
  | Windows x64 | `@perryts/perry-win32-x64` |
26
+ | Windows arm64 | `@perryts/perry-win32-arm64` |
27
+
28
+ ### Linux: glibc version
29
+
30
+ The glibc packages are built against a glibc 2.31 sysroot, so they need **glibc ≥ 2.31**. That covers Ubuntu 20.04+, Debian 11+, RHEL 9+ and Amazon Linux 2023 with the native glibc build. On an older glibc the launcher automatically runs the fully-static musl build instead, which has no libc dependency ([#6298](https://github.com/PerryTS/perry/issues/6298), [#6351](https://github.com/PerryTS/perry/issues/6351)). It prints a one-time notice when it does; set `PERRY_NO_FALLBACK_NOTICE=1` to silence it.
31
+
32
+ npm only installs the musl package when it thinks the machine is musl-based, so on an old-glibc host you have to ask for it once:
33
+
34
+ ```bash
35
+ npm install --force @perryts/perry-linux-x64-musl # or -arm64-musl
36
+ ```
37
+
38
+ The launcher tells you this (with the exact command) instead of letting the binary fail in the dynamic loader. The static build is the same compiler; the one thing it cannot do is build `perry/ui` GTK4 desktop apps. The glibc package includes GTK4 support, whose system development libraries must be available when linking a UI app.
26
39
 
27
40
  ## Host requirements
28
41
 
29
42
  Perry produces native binaries by linking its runtime and stdlib (shipped as static archives in the platform package) into your code. That link step uses your system C toolchain, so you need:
30
43
 
31
44
  - **macOS** — Xcode Command Line Tools (`xcode-select --install`)
32
- - **Linux** — `gcc` or `clang` (e.g. `apt install build-essential` on Debian/Ubuntu, `apk add build-base` on Alpine)
45
+ - **Linux** — `gcc` or `clang` (e.g. `apt install build-essential` on Debian/Ubuntu, `apk add build-base` on Alpine), plus **clang ≥ 15**: codegen emits opaque-pointer LLVM IR, which clang 14 and older reject with `error: expected type`. Ubuntu 22.04 defaults to clang 14 — `apt install clang-15` and `export PERRY_LLVM_CLANG=/usr/bin/clang-15`.
33
46
  - **Windows** — MSVC / Visual Studio Build Tools with the C++ workload
34
47
 
35
48
  Node.js 16 or later is required for the wrapper itself.
package/bin/detect.cjs ADDED
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ // Platform detection for the @perryts/perry launcher.
3
+ //
4
+ // Split out of bin/perry.js so the resolution rules can be exercised directly
5
+ // (see ../test/detect.test.js) with a synthetic host description instead of
6
+ // whatever machine happens to run the tests.
7
+
8
+ const PLATFORM_PACKAGES = {
9
+ "darwin-arm64": "@perryts/perry-darwin-arm64",
10
+ "darwin-x64": "@perryts/perry-darwin-x64",
11
+ "linux-arm64": "@perryts/perry-linux-arm64",
12
+ "linux-arm64-musl": "@perryts/perry-linux-arm64-musl",
13
+ "linux-x64": "@perryts/perry-linux-x64",
14
+ "linux-x64-musl": "@perryts/perry-linux-x64-musl",
15
+ "win32-x64": "@perryts/perry-win32-x64",
16
+ "win32-arm64": "@perryts/perry-win32-arm64",
17
+ };
18
+
19
+ // Minimum glibc the prebuilt *glibc* Linux binaries can run on.
20
+ //
21
+ // KEEP IN SYNC WITH THE BUILDER IMAGE. `.github/workflows/release-packages.yml`
22
+ // builds both GNU targets in architecture-matched glibc 2.31 containers.
23
+ // The release build checks the final ELF symbol versions and runs a compile
24
+ // smoke test inside that container before packaging it (#6351).
25
+ //
26
+ // A binary only requires the glibc version whose symbols it actually pulls in,
27
+ // so this is an upper bound: it is the version of the builder image, and the
28
+ // safe assumption is that the binary needs all of it. If the release matrix
29
+ // moves to a different base image, update this constant in the same commit —
30
+ // otherwise hosts that *could* run the glibc build get silently pushed onto the
31
+ // static build (too high a value), or hosts that can't get the loader error back
32
+ // (too low a value).
33
+ const GLIBC_BUILD_FLOOR = "2.31";
34
+
35
+ // Compare two dotted numeric versions ("2.35", "2.39.1"). Returns <0, 0, >0.
36
+ // Non-numeric components sort as 0 — glibc versions are always numeric, and a
37
+ // garbage value should not be read as "newer than the floor".
38
+ function compareVersions(a, b) {
39
+ const pa = String(a).split(".");
40
+ const pb = String(b).split(".");
41
+ const len = Math.max(pa.length, pb.length);
42
+ for (let i = 0; i < len; i++) {
43
+ const na = parseInt(pa[i], 10) || 0;
44
+ const nb = parseInt(pb[i], 10) || 0;
45
+ if (na !== nb) return na - nb;
46
+ }
47
+ return 0;
48
+ }
49
+
50
+ // Read the host description this module reasons about. `glibcVersionRuntime` is
51
+ // what Node itself uses for the `libc` field of optional deps: a version string
52
+ // on glibc, and empty/absent on musl.
53
+ function readHost() {
54
+ const host = {
55
+ platform: process.platform,
56
+ arch: process.arch,
57
+ hasGlibcField: false,
58
+ glibcVersionRuntime: undefined,
59
+ osRelease: null,
60
+ };
61
+ try {
62
+ const header = process.report && process.report.getReport().header;
63
+ if (header && "glibcVersionRuntime" in header) {
64
+ host.hasGlibcField = true;
65
+ host.glibcVersionRuntime = header.glibcVersionRuntime;
66
+ }
67
+ } catch (_) {
68
+ /* process.report unavailable — fall back to /etc/os-release below. */
69
+ }
70
+ try {
71
+ host.osRelease = require("fs").readFileSync("/etc/os-release", "utf8");
72
+ } catch (_) {
73
+ /* not Linux, or no os-release — leave null. */
74
+ }
75
+ return host;
76
+ }
77
+
78
+ function isMusl(host) {
79
+ if (host.platform !== "linux") return false;
80
+ // Node reports an empty glibc version on musl. This is the same signal npm
81
+ // uses for the `libc` selector, so it agrees with what npm installed.
82
+ if (host.hasGlibcField) return !host.glibcVersionRuntime;
83
+ // No report header (very old Node, or a hardened runtime): sniff os-release.
84
+ if (host.osRelease) return /\bID=alpine\b|\bmusl\b/i.test(host.osRelease);
85
+ return false;
86
+ }
87
+
88
+ function glibcVersion(host) {
89
+ if (host.platform !== "linux") return null;
90
+ if (!host.hasGlibcField) return null;
91
+ const v = host.glibcVersionRuntime;
92
+ return typeof v === "string" && /^\d+(\.\d+)*$/.test(v) ? v : null;
93
+ }
94
+
95
+ // Resolve a host to the ordered list of platform packages that could serve it.
96
+ //
97
+ // `candidates[0]` is the preferred package; later entries are tried only if the
98
+ // preferred one is not installed. `reason` explains the choice:
99
+ //
100
+ // "native" — the plain os-arch build is the right one
101
+ // "musl" — musl libc host (Alpine/distroless): the static build
102
+ // "glibc-too-old" — glibc host whose glibc predates the builder image, so the
103
+ // glibc build's loader would reject it (#6298). The musl
104
+ // build is fully static, so it runs here.
105
+ function detectPlatform(host) {
106
+ const base = `${host.platform}-${host.arch}`;
107
+
108
+ if (host.platform !== "linux") {
109
+ return { candidates: [base], reason: "native", glibc: null };
110
+ }
111
+
112
+ if (isMusl(host)) {
113
+ // Fall back to the glibc package: some glibc systems (custom kernels, odd
114
+ // container images) report an empty glibcVersionRuntime and land here by
115
+ // mistake — see #116 / v0.5.118. If the musl package really isn't there,
116
+ // the glibc one is the better guess than a hard failure.
117
+ return { candidates: [`${base}-musl`, base], reason: "musl", glibc: null };
118
+ }
119
+
120
+ const glibc = glibcVersion(host);
121
+ if (glibc && compareVersions(glibc, GLIBC_BUILD_FLOOR) < 0) {
122
+ // Deliberately no glibc fallback: that binary physically cannot load here.
123
+ return { candidates: [`${base}-musl`], reason: "glibc-too-old", glibc };
124
+ }
125
+
126
+ return { candidates: [base], reason: "native", glibc };
127
+ }
128
+
129
+ module.exports = {
130
+ PLATFORM_PACKAGES,
131
+ GLIBC_BUILD_FLOOR,
132
+ compareVersions,
133
+ detectPlatform,
134
+ glibcVersion,
135
+ isMusl,
136
+ readHost,
137
+ };
package/bin/perry.js CHANGED
@@ -5,62 +5,51 @@
5
5
  // optional-dependency packages that npm picks by os/cpu/libc.
6
6
 
7
7
  const { spawn } = require("child_process");
8
+ const {
9
+ PLATFORM_PACKAGES,
10
+ GLIBC_BUILD_FLOOR,
11
+ detectPlatform,
12
+ readHost,
13
+ } = require("./detect.cjs");
8
14
 
9
- const PLATFORM_PACKAGES = {
10
- "darwin-arm64": "@perryts/perry-darwin-arm64",
11
- "darwin-x64": "@perryts/perry-darwin-x64",
12
- "linux-arm64": "@perryts/perry-linux-arm64",
13
- "linux-arm64-musl": "@perryts/perry-linux-arm64-musl",
14
- "linux-x64": "@perryts/perry-linux-x64",
15
- "linux-x64-musl": "@perryts/perry-linux-x64-musl",
16
- "win32-x64": "@perryts/perry-win32-x64",
17
- };
18
-
19
- function isMusl() {
20
- if (process.platform !== "linux") return false;
21
- try {
22
- const header = process.report && process.report.getReport().header;
23
- if (header && "glibcVersionRuntime" in header) {
24
- return !header.glibcVersionRuntime;
25
- }
26
- } catch (_) {}
27
- try {
28
- const release = require("fs").readFileSync("/etc/os-release", "utf8");
29
- return /\bID=alpine\b|\bmusl\b/i.test(release);
30
- } catch (_) {}
31
- return false;
32
- }
15
+ const host = readHost();
16
+ const detected = detectPlatform(host);
17
+ const binName = process.platform === "win32" ? "perry.exe" : "perry";
33
18
 
34
- function detectKey() {
35
- let key = `${process.platform}-${process.arch}`;
36
- if (process.platform === "linux" && isMusl()) key += "-musl";
37
- return key;
19
+ // Walk the candidate packages in preference order. On Linux the first entry may
20
+ // be the fully-static musl build (musl host, or a glibc host older than the one
21
+ // the glibc binaries were built on — #6298); everywhere else there is exactly
22
+ // one candidate and this loop is the old single require.resolve.
23
+ let binPath = null;
24
+ let usedKey = null;
25
+ const resolveErrors = [];
26
+ for (const key of detected.candidates) {
27
+ const pkg = PLATFORM_PACKAGES[key];
28
+ if (!pkg) continue;
29
+ try {
30
+ binPath = require.resolve(`${pkg}/bin/${binName}`);
31
+ usedKey = key;
32
+ break;
33
+ } catch (err) {
34
+ resolveErrors.push(`${pkg}: ${err.message}`);
35
+ }
38
36
  }
39
37
 
40
- const key = detectKey();
41
- const pkg = PLATFORM_PACKAGES[key];
42
- if (!pkg) {
43
- console.error(
44
- `[perry] No prebuilt binary for ${key}.\n` +
45
- `Supported: ${Object.keys(PLATFORM_PACKAGES).join(", ")}\n` +
46
- `File an issue: https://github.com/PerryTS/perry/issues`
47
- );
38
+ if (!binPath) {
39
+ reportResolutionFailure();
48
40
  process.exit(1);
49
41
  }
50
42
 
51
- const binName = process.platform === "win32" ? "perry.exe" : "perry";
52
- let binPath;
53
- try {
54
- binPath = require.resolve(`${pkg}/bin/${binName}`);
55
- } catch (err) {
56
- console.error(
57
- `[perry] The ${pkg} package is not installed.\n` +
58
- `This usually means npm skipped the optional dependency for ${key}.\n` +
59
- `Try: npm install --force ${pkg}\n` +
60
- `Or reinstall @perryts/perry with a matching npm (\u22658.12) so os/cpu/libc selectors apply.\n` +
61
- `Underlying error: ${err.message}`
43
+ // Tell the user once not on every invocation — when they are not running the
44
+ // binary their platform string implies.
45
+ if (detected.reason === "glibc-too-old" && usedKey.endsWith("-musl")) {
46
+ noticeOnce(
47
+ `[perry] glibc ${detected.glibc} is older than the prebuilt glibc binary requires ` +
48
+ `(>= ${GLIBC_BUILD_FLOOR}), so Perry is running its fully-static Linux build ` +
49
+ `(${PLATFORM_PACKAGES[usedKey]}).\n` +
50
+ `[perry] Same compiler; the static build cannot produce perry/ui (GTK4) apps. ` +
51
+ `Set PERRY_NO_FALLBACK_NOTICE=1 to silence this. Details: https://github.com/PerryTS/perry/issues/6298`
62
52
  );
63
- process.exit(1);
64
53
  }
65
54
 
66
55
  const child = spawn(binPath, process.argv.slice(2), { stdio: "inherit" });
@@ -87,3 +76,90 @@ child.on("error", (err) => {
87
76
  console.error(`[perry] Failed to spawn ${binPath}: ${err.message}`);
88
77
  process.exit(1);
89
78
  });
79
+
80
+ // --- helpers ---------------------------------------------------------------
81
+
82
+ // Print `msg` at most once per (version, platform key) on this machine. The
83
+ // stamp lives in the temp dir, so it comes back after a reboot/cleanup — that's
84
+ // deliberate: the message is worth seeing again occasionally, just not on every
85
+ // single `perry` invocation in a build loop.
86
+ function noticeOnce(msg) {
87
+ if (process.env.PERRY_NO_FALLBACK_NOTICE) return;
88
+ const fs = require("fs");
89
+ const path = require("path");
90
+ const os = require("os");
91
+ let version = "unknown";
92
+ try {
93
+ version = require("../package.json").version;
94
+ } catch (_) {}
95
+ const stamp = path.join(
96
+ os.tmpdir(),
97
+ `perry-static-fallback-${version}-${usedKey}.stamp`
98
+ );
99
+ try {
100
+ // "wx" fails with EEXIST if we already printed this.
101
+ fs.closeSync(fs.openSync(stamp, "wx"));
102
+ } catch (err) {
103
+ if (err && err.code === "EEXIST") return;
104
+ // Any other stamp problem (read-only tmp, etc.) — print, don't crash.
105
+ }
106
+ console.error(msg);
107
+ }
108
+
109
+ function reportResolutionFailure() {
110
+ const key = detected.candidates[0];
111
+ const pkg = PLATFORM_PACKAGES[key];
112
+
113
+ if (!pkg) {
114
+ console.error(
115
+ `[perry] No prebuilt binary for ${key}.\n` +
116
+ `Supported: ${Object.keys(PLATFORM_PACKAGES).join(", ")}\n` +
117
+ `File an issue: https://github.com/PerryTS/perry/issues`
118
+ );
119
+ return;
120
+ }
121
+
122
+ let version = "";
123
+ try {
124
+ version = `@${require("../package.json").version}`;
125
+ } catch (_) {}
126
+
127
+ if (detected.reason === "glibc-too-old") {
128
+ // npm skipped the musl package because this host is glibc — its `libc`
129
+ // selector says "musl". Without --force npm refuses to install it here, so
130
+ // spell the command out rather than leaving the user with a loader error.
131
+ console.error(
132
+ `[perry] This system has glibc ${detected.glibc}, but Perry's prebuilt Linux binary\n` +
133
+ ` needs glibc >= ${GLIBC_BUILD_FLOOR} (it is built on a glibc 2.31 sysroot). Running it would fail\n` +
134
+ ` in the dynamic loader with "GLIBC_${GLIBC_BUILD_FLOOR} not found".\n` +
135
+ `\n` +
136
+ ` Perry also ships a fully-static Linux build that needs no glibc at all, but\n` +
137
+ ` npm skipped it here because that package is tagged libc: ["musl"]. Install it\n` +
138
+ ` the same way you installed perry:\n` +
139
+ `\n` +
140
+ ` if perry is GLOBAL (npm i -g @perryts/perry):\n` +
141
+ ` npm install -g --force ${pkg}${version}\n` +
142
+ `\n` +
143
+ ` if perry is a PROJECT dependency:\n` +
144
+ ` npm install --force ${pkg}${version}\n` +
145
+ `\n` +
146
+ ` ...then re-run perry — this launcher picks it up automatically.\n` +
147
+ `\n` +
148
+ ` Or install outside npm (this picks the static build for you):\n` +
149
+ ` curl -fsSL https://perryts.com/install.sh | sh\n` +
150
+ `\n` +
151
+ ` Tracking: https://github.com/PerryTS/perry/issues/6298`
152
+ );
153
+ return;
154
+ }
155
+
156
+ console.error(
157
+ `[perry] The ${pkg} package is not installed.\n` +
158
+ `This usually means npm skipped the optional dependency for ${key}.\n` +
159
+ `Install it the same way you installed perry:\n` +
160
+ ` global: npm install -g --force ${pkg}${version}\n` +
161
+ ` project: npm install --force ${pkg}${version}\n` +
162
+ `Or reinstall @perryts/perry with a matching npm (≥8.12) so os/cpu/libc selectors apply.\n` +
163
+ `Underlying error: ${resolveErrors.join("; ")}`
164
+ );
165
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@perryts/perry",
3
- "version": "0.5.1220",
3
+ "version": "0.5.1519",
4
4
  "description": "Native TypeScript compiler. Compiles TypeScript source directly to native executables via LLVM.",
5
5
  "bin": {
6
6
  "perry": "bin/perry.js"
@@ -11,13 +11,12 @@
11
11
  "LICENSE"
12
12
  ],
13
13
  "optionalDependencies": {
14
- "@perryts/perry-darwin-arm64": "0.5.1220",
15
- "@perryts/perry-darwin-x64": "0.5.1220",
16
- "@perryts/perry-linux-x64": "0.5.1220",
17
- "@perryts/perry-linux-arm64": "0.5.1220",
18
- "@perryts/perry-linux-x64-musl": "0.5.1220",
19
- "@perryts/perry-linux-arm64-musl": "0.5.1220",
20
- "@perryts/perry-win32-x64": "0.5.1220"
14
+ "@perryts/perry-darwin-arm64": "0.5.1519",
15
+ "@perryts/perry-darwin-x64": "0.5.1519",
16
+ "@perryts/perry-linux-x64": "0.5.1519",
17
+ "@perryts/perry-linux-arm64": "0.5.1519",
18
+ "@perryts/perry-win32-x64": "0.5.1519",
19
+ "@perryts/perry-win32-arm64": "0.5.1519"
21
20
  },
22
21
  "keywords": [
23
22
  "typescript",