laravel-workers 0.0.0-stage → 0.4.0
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/LICENSE +21 -0
- package/README.md +295 -2
- package/bin/laravel-workers.mjs +141 -0
- package/build.mjs +2599 -0
- package/compiler/bin/controller.mjs +92 -0
- package/compiler/bin/transpile.mjs +65 -0
- package/compiler/runtime/rt-from-context.mjs +2399 -0
- package/compiler/src/classes.mjs +1232 -0
- package/compiler/src/controller.mjs +1029 -0
- package/compiler/src/dist-swap.mjs +76 -0
- package/compiler/src/mail-reach.mjs +104 -0
- package/compiler/src/policy.mjs +345 -0
- package/compiler/src/transpiler.mjs +3094 -0
- package/compiler/src/view-name.mjs +21 -0
- package/kernel/auth.mjs +1502 -0
- package/kernel/cache.mjs +405 -0
- package/kernel/collections.mjs +1615 -0
- package/kernel/cookies.mjs +474 -0
- package/kernel/csrf.mjs +698 -0
- package/kernel/db.mjs +703 -0
- package/kernel/digest.mjs +360 -0
- package/kernel/encrypter.mjs +106 -0
- package/kernel/form-request.mjs +106 -0
- package/kernel/hash.mjs +113 -0
- package/kernel/html-entities.mjs +268 -0
- package/kernel/http.mjs +1803 -0
- package/kernel/image.mjs +341 -0
- package/kernel/mail.mjs +269 -0
- package/kernel/mailable.mjs +172 -0
- package/kernel/model.mjs +1226 -0
- package/kernel/multipart.mjs +161 -0
- package/kernel/paginator.mjs +336 -0
- package/kernel/php-json.mjs +220 -0
- package/kernel/php-serialize.mjs +245 -0
- package/kernel/queue.mjs +96 -0
- package/kernel/ratelimit.mjs +855 -0
- package/kernel/redis.mjs +264 -0
- package/kernel/resources.mjs +857 -0
- package/kernel/response.mjs +17 -0
- package/kernel/router.mjs +276 -0
- package/kernel/scheduler.mjs +236 -0
- package/kernel/service-adapters.mjs +49 -0
- package/kernel/session.mjs +934 -0
- package/kernel/storage.mjs +265 -0
- package/kernel/str.mjs +445 -0
- package/kernel/tests/auth.test.mjs +1074 -0
- package/kernel/tests/budget.test.mjs +527 -0
- package/kernel/tests/cache.test.mjs +290 -0
- package/kernel/tests/collections.test.mjs +534 -0
- package/kernel/tests/cookies.test.mjs +302 -0
- package/kernel/tests/csrf.test.mjs +597 -0
- package/kernel/tests/db.test.mjs +1069 -0
- package/kernel/tests/encrypter.test.mjs +90 -0
- package/kernel/tests/fixtures/laravel-1337-cookies.json +120 -0
- package/kernel/tests/hash.test.mjs +149 -0
- package/kernel/tests/http.test.mjs +1182 -0
- package/kernel/tests/image.test.mjs +235 -0
- package/kernel/tests/login-flow.test.mjs +509 -0
- package/kernel/tests/model.test.mjs +324 -0
- package/kernel/tests/oracle-app.mjs +32 -0
- package/kernel/tests/paginator.test.mjs +132 -0
- package/kernel/tests/php-json.test.mjs +263 -0
- package/kernel/tests/php-serialize.test.mjs +210 -0
- package/kernel/tests/ports.test.mjs +47 -0
- package/kernel/tests/ratelimit.test.mjs +975 -0
- package/kernel/tests/readiness.test.mjs +155 -0
- package/kernel/tests/redis.test.mjs +175 -0
- package/kernel/tests/registry-json.test.mjs +28 -0
- package/kernel/tests/resources.test.mjs +275 -0
- package/kernel/tests/router.test.mjs +281 -0
- package/kernel/tests/routes-json.test.mjs +32 -0
- package/kernel/tests/services.test.mjs +474 -0
- package/kernel/tests/session.test.mjs +709 -0
- package/kernel/tests/sqlite-integration.test.mjs +390 -0
- package/kernel/tests/storage.test.mjs +133 -0
- package/kernel/tests/str.test.mjs +83 -0
- package/kernel/tests/upload.test.mjs +193 -0
- package/kernel/tests/validation-messages.test.mjs +117 -0
- package/kernel/tests/validation.test.mjs +340 -0
- package/kernel/tests/validator-hooks.test.mjs +343 -0
- package/kernel/tests/validator.test.mjs +805 -0
- package/kernel/tests/vendor-lite.test.mjs +39 -0
- package/kernel/validation-messages.data.mjs +168 -0
- package/kernel/validation-messages.mjs +249 -0
- package/kernel/validation.mjs +123 -0
- package/kernel/validator.mjs +1538 -0
- package/package.json +68 -5
- package/shadow/run.mjs +784 -0
- package/shadow/scenarios/fresh-app.mjs +60 -0
- package/shadow/scenarios/lw-constructs-app.mjs +669 -0
- package/src/ports/dbcheck.mjs +22 -0
- package/src/worker.mjs +200 -0
- package/tools/_root.php +135 -0
- package/tools/bench.mjs +129 -0
- package/tools/class-dump.php +87 -0
- package/tools/compile-view.php +263 -0
- package/tools/config-dump.php +192 -0
- package/tools/constants-dump.php +46 -0
- package/tools/construct-audit.mjs +326 -0
- package/tools/constructs.json +1510 -0
- package/tools/docs-check.mjs +149 -0
- package/tools/event-dump.php +320 -0
- package/tools/formrequest-dump.php +229 -0
- package/tools/gate.mjs +36 -0
- package/tools/http-oracle.php +200 -0
- package/tools/init.mjs +73 -0
- package/tools/integrations.json +69 -0
- package/tools/mailable-views.php +116 -0
- package/tools/make-cookie-fixture.mjs +87 -0
- package/tools/make-html-entities.mjs +106 -0
- package/tools/model-dump.php +198 -0
- package/tools/php-auth-oracle.php +701 -0
- package/tools/php-cache-oracle.php +166 -0
- package/tools/php-crypto-oracle.php +113 -0
- package/tools/php-csrf-oracle.php +271 -0
- package/tools/php-http-pipeline-oracle.php +358 -0
- package/tools/php-json-oracle.php +102 -0
- package/tools/php-login-flow-oracle.php +342 -0
- package/tools/php-provision.mjs +172 -0
- package/tools/php-ratelimit-oracle.php +508 -0
- package/tools/php-runtime.mjs +342 -0
- package/tools/php-serialize-oracle.php +114 -0
- package/tools/php-session-oracle.php +266 -0
- package/tools/php-shadow-oracle.php +588 -0
- package/tools/policy-dump.php +240 -0
- package/tools/ports.mjs +51 -0
- package/tools/prerender.php +68 -0
- package/tools/readiness.mjs +255 -0
- package/tools/registry-json.mjs +22 -0
- package/tools/routes-dump.php +27 -0
- package/tools/routes-json.mjs +48 -0
- package/tools/schedule-dump.php +82 -0
- package/tools/schema-dump.php +158 -0
- package/tools/validation-messages.php +54 -0
- package/tools/vendor-lite.mjs +299 -0
- package/tools/verify-s0.php +209 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
// tools/php-runtime.mjs — which PHP interpreter boots the app?
|
|
2
|
+
//
|
|
3
|
+
// The build uses PHP only as a BUILD-TIME ORACLE: it boots the real Laravel app
|
|
4
|
+
// (route:list, config-dump, model-dump, compile-view, ...) and compiles the
|
|
5
|
+
// answers to JS. The edge never runs PHP. The interpreter is therefore a tool,
|
|
6
|
+
// not a runtime — and the only hard requirement is that it can parse and boot
|
|
7
|
+
// the app, i.e. that it satisfies the app's own `composer.json` `require.php`.
|
|
8
|
+
//
|
|
9
|
+
// That requirement belongs to the APP, not to the machine. An app declaring
|
|
10
|
+
// `^8.4` (Laravel 13 / Symfony 8) cannot be parsed by 8.3 at all; an older app
|
|
11
|
+
// may not run on 8.4. Hard-coding `php` from PATH therefore fails for every user
|
|
12
|
+
// whose default interpreter differs from the app's — which is the normal case
|
|
13
|
+
// once more than one PHP is installed. So the interpreter is RESOLVED:
|
|
14
|
+
//
|
|
15
|
+
// 1. PHP_BIN explicit override. Honoured as given; a version
|
|
16
|
+
// mismatch is an error, never silently replaced.
|
|
17
|
+
// 2. `php` on PATH the default. Used whenever it satisfies the app.
|
|
18
|
+
// 3. installed versions versioned binaries (`php8.4`, `php84`, ...) and the
|
|
19
|
+
// usual per-platform install roots (Laragon, XAMPP,
|
|
20
|
+
// C:\php, Herd, Homebrew, asdf, phpenv, mise),
|
|
21
|
+
// highest satisfying version first.
|
|
22
|
+
// 4. nothing satisfies an error naming every interpreter that was found,
|
|
23
|
+
// with the three ways out (install one / PHP_BIN /
|
|
24
|
+
// build where one exists). Never a bare "no".
|
|
25
|
+
//
|
|
26
|
+
// Provisioning (downloading a portable PHP) is deliberately NOT done here: it
|
|
27
|
+
// would add a platform-specific, network-dependent, unverified binary to a
|
|
28
|
+
// build that is supposed to be deterministic. The resolver's job is to use what
|
|
29
|
+
// the machine already has and to say precisely what is missing.
|
|
30
|
+
//
|
|
31
|
+
// Parity note: the compiler's byte-parity suite is pinned to PHP 8.3.30
|
|
32
|
+
// (`ORACLE_PHP`). Any interpreter that boots the app produces a valid build, but
|
|
33
|
+
// a different version is REPORTED by the caller (advisory, never fatal) so that
|
|
34
|
+
// a behavioural divergence has a visible cause instead of a silent one.
|
|
35
|
+
|
|
36
|
+
import { execFileSync } from 'node:child_process';
|
|
37
|
+
import { existsSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
38
|
+
import { homedir } from 'node:os';
|
|
39
|
+
import { join, resolve } from 'node:path';
|
|
40
|
+
|
|
41
|
+
/** The version the parity suite is pinned to. */
|
|
42
|
+
export const ORACLE_PHP = '8.3.30';
|
|
43
|
+
|
|
44
|
+
/** MSYS2/Git Bash hands Node POSIX paths ("/c/Users/...") that spawn() cannot use. */
|
|
45
|
+
function toNativePath(p) {
|
|
46
|
+
if (process.platform !== 'win32' || typeof p !== 'string') return p;
|
|
47
|
+
const s = p.replace(/\\/g, '/');
|
|
48
|
+
const m = s.match(/^\/([a-zA-Z])(?:\/|$)/);
|
|
49
|
+
if (!m) return p;
|
|
50
|
+
return `${m[1].toUpperCase()}:/${s.slice(3)}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseVer(v) {
|
|
54
|
+
const m = String(v || '').match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
55
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function cmpArr(a, b) {
|
|
59
|
+
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function cmpVer(a, b) {
|
|
63
|
+
const x = parseVer(a);
|
|
64
|
+
const y = parseVer(b);
|
|
65
|
+
return x && y ? cmpArr(x, y) : 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Does `version` satisfy a Composer constraint?
|
|
70
|
+
* Returns true / false, or null when the constraint uses syntax this parser
|
|
71
|
+
* does not understand. null means SKIP the gate: the build then surfaces the
|
|
72
|
+
* interpreter's real error instead of inventing one from a bad guess.
|
|
73
|
+
*/
|
|
74
|
+
export function satisfies(version, constraint) {
|
|
75
|
+
const v = parseVer(version);
|
|
76
|
+
if (!v || typeof constraint !== 'string' || !constraint.trim()) return null;
|
|
77
|
+
|
|
78
|
+
const one = (raw) => {
|
|
79
|
+
const c = raw.trim();
|
|
80
|
+
if (!c || c === '*') return true;
|
|
81
|
+
let m, lo, hi;
|
|
82
|
+
|
|
83
|
+
// ^8.4 / ^8.4.1 — next major
|
|
84
|
+
if ((m = c.match(/^\^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/))) {
|
|
85
|
+
lo = [Number(m[1]), Number(m[2] || 0), Number(m[3] || 0)];
|
|
86
|
+
hi = [Number(m[1]) + 1, 0, 0];
|
|
87
|
+
return cmpArr(v, lo) >= 0 && cmpArr(v, hi) < 0;
|
|
88
|
+
}
|
|
89
|
+
// ~8.4 (next major) / ~8.4.1 (next minor)
|
|
90
|
+
if ((m = c.match(/^~(\d+)(?:\.(\d+))?(?:\.(\d+))?$/))) {
|
|
91
|
+
lo = [Number(m[1]), Number(m[2] || 0), Number(m[3] || 0)];
|
|
92
|
+
hi = m[2] === undefined ? [Number(m[1]) + 1, 0, 0] : [Number(m[1]), Number(m[2]) + 1, 0];
|
|
93
|
+
return cmpArr(v, lo) >= 0 && cmpArr(v, hi) < 0;
|
|
94
|
+
}
|
|
95
|
+
// 8 / 8.3 / 8.3.0 / 8.3.*
|
|
96
|
+
if ((m = c.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.\*)?$/))) {
|
|
97
|
+
if (m[3] !== undefined) {
|
|
98
|
+
lo = [Number(m[1]), Number(m[2] || 0), Number(m[3])];
|
|
99
|
+
hi = [lo[0], lo[1], lo[2] + 1];
|
|
100
|
+
} else if (m[2] === undefined) {
|
|
101
|
+
lo = [Number(m[1]), 0, 0];
|
|
102
|
+
hi = [Number(m[1]) + 1, 0, 0];
|
|
103
|
+
} else {
|
|
104
|
+
lo = [Number(m[1]), Number(m[2]), 0];
|
|
105
|
+
hi = [Number(m[1]), Number(m[2]) + 1, 0];
|
|
106
|
+
}
|
|
107
|
+
return cmpArr(v, lo) >= 0 && cmpArr(v, hi) < 0;
|
|
108
|
+
}
|
|
109
|
+
// >=8.3 / <=8.3.1 / >8 / <9 / =8.3
|
|
110
|
+
if ((m = c.match(/^(>=|<=|>|<|=)\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?$/))) {
|
|
111
|
+
const t = [Number(m[2]), Number(m[3] || 0), Number(m[4] || 0)];
|
|
112
|
+
const d = cmpArr(v, t);
|
|
113
|
+
switch (m[1]) {
|
|
114
|
+
case '>=': return d >= 0;
|
|
115
|
+
case '<=': return d <= 0;
|
|
116
|
+
case '>': return d > 0;
|
|
117
|
+
case '<': return d < 0;
|
|
118
|
+
default: return d === 0;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// !=8.3.1 (exact) / !=8.3 (the whole 8.3.x line) / !=8 (the whole major)
|
|
122
|
+
if ((m = c.match(/^!=\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?$/))) {
|
|
123
|
+
const t = [Number(m[1]), Number(m[2] || 0), Number(m[3] || 0)];
|
|
124
|
+
if (m[3] !== undefined) return cmpArr(v, t) !== 0;
|
|
125
|
+
if (m[2] !== undefined) return v[0] !== t[0] || v[1] !== t[1];
|
|
126
|
+
return v[0] !== t[0];
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
let sawUnknown = false;
|
|
132
|
+
for (const branch of constraint.split('||')) {
|
|
133
|
+
const parts = branch.trim().split(/\s+/).filter(Boolean);
|
|
134
|
+
if (!parts.length) continue;
|
|
135
|
+
let branchUnknown = false;
|
|
136
|
+
let branchOk = true;
|
|
137
|
+
for (const part of parts) {
|
|
138
|
+
const r = one(part);
|
|
139
|
+
if (r === null) branchUnknown = true;
|
|
140
|
+
else if (!r) { branchOk = false; break; }
|
|
141
|
+
}
|
|
142
|
+
if (branchOk && !branchUnknown) return true;
|
|
143
|
+
if (branchUnknown) sawUnknown = true;
|
|
144
|
+
}
|
|
145
|
+
return sawUnknown ? null : false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Probe one interpreter: returns its version, or null when it cannot run. */
|
|
149
|
+
export function probePhp(bin) {
|
|
150
|
+
try {
|
|
151
|
+
const out = execFileSync(bin, ['-r', 'echo PHP_VERSION;'], {
|
|
152
|
+
encoding: 'utf8',
|
|
153
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
154
|
+
windowsHide: true,
|
|
155
|
+
timeout: 15000,
|
|
156
|
+
});
|
|
157
|
+
const matches = String(out).match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.\-]+)?/g);
|
|
158
|
+
return matches ? matches[matches.length - 1] : null;
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function readdirSafe(dir) {
|
|
165
|
+
try {
|
|
166
|
+
return readdirSync(dir);
|
|
167
|
+
} catch {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isFile(p) {
|
|
173
|
+
try {
|
|
174
|
+
return statSync(p).isFile();
|
|
175
|
+
} catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Interpreters this machine plausibly has, cheapest to discover first.
|
|
182
|
+
* Order is only a discovery order; the caller sorts by version.
|
|
183
|
+
*/
|
|
184
|
+
export function phpCandidates() {
|
|
185
|
+
const out = [];
|
|
186
|
+
const seen = new Set();
|
|
187
|
+
const add = (bin, source) => {
|
|
188
|
+
if (!bin) return;
|
|
189
|
+
let key = bin;
|
|
190
|
+
try {
|
|
191
|
+
key = realpathSync(bin);
|
|
192
|
+
} catch {
|
|
193
|
+
/* not resolvable yet — the probe decides */
|
|
194
|
+
}
|
|
195
|
+
if (seen.has(key)) return;
|
|
196
|
+
seen.add(key);
|
|
197
|
+
out.push({ bin, source });
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const names = ['php', 'php8.5', 'php85', 'php8.4', 'php84', 'php8.3', 'php83', 'php8.2', 'php82', 'php8.1', 'php81', 'php8.0', 'php80'];
|
|
201
|
+
for (const n of names) add(n, 'PATH');
|
|
202
|
+
|
|
203
|
+
const home = homedir();
|
|
204
|
+
|
|
205
|
+
if (process.platform === 'win32') {
|
|
206
|
+
const roots = [];
|
|
207
|
+
if (process.env.LARAGON) roots.push(join(process.env.LARAGON, 'bin', 'php'));
|
|
208
|
+
roots.push('C:\\laragon\\bin\\php', 'C:\\xampp\\php', 'C:\\php', 'C:\\tools', 'C:\\Program Files\\PHP');
|
|
209
|
+
for (const root of roots) {
|
|
210
|
+
const direct = join(root, 'php.exe');
|
|
211
|
+
if (isFile(direct)) add(direct, root);
|
|
212
|
+
for (const d of readdirSafe(root)) {
|
|
213
|
+
const exe = join(root, d, 'php.exe');
|
|
214
|
+
if (isFile(exe)) add(exe, root);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
for (const base of [
|
|
218
|
+
join(process.env.LOCALAPPDATA || '', 'Herd', 'bin'),
|
|
219
|
+
join(home, '.config', 'herd', 'bin'),
|
|
220
|
+
join(home, 'scoop', 'apps', 'php', 'current'),
|
|
221
|
+
join(home, 'scoop', 'apps', 'php'),
|
|
222
|
+
]) {
|
|
223
|
+
const direct = join(base, 'php.exe');
|
|
224
|
+
if (isFile(direct)) add(direct, base);
|
|
225
|
+
for (const d of readdirSafe(base)) {
|
|
226
|
+
const exe = join(base, d, 'php.exe');
|
|
227
|
+
if (isFile(exe)) add(exe, base);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// POSIX: versioned binaries in the usual bin dirs, Homebrew kegs, and the
|
|
234
|
+
// version managers that keep a directory per PHP version.
|
|
235
|
+
for (const dir of ['/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin']) {
|
|
236
|
+
for (const f of readdirSafe(dir)) {
|
|
237
|
+
if (!/^php(\d+(\.\d+)?)?$/.test(f)) continue;
|
|
238
|
+
const p = join(dir, f);
|
|
239
|
+
if (isFile(p)) add(p, dir);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
for (const dir of ['/opt/homebrew/opt', '/usr/local/opt']) {
|
|
243
|
+
for (const f of readdirSafe(dir)) {
|
|
244
|
+
if (!f.startsWith('php')) continue;
|
|
245
|
+
const p = join(dir, f, 'bin', 'php');
|
|
246
|
+
if (isFile(p)) add(p, dir);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (const base of [
|
|
250
|
+
join(home, '.asdf', 'installs', 'php'),
|
|
251
|
+
join(home, '.phpenv', 'versions'),
|
|
252
|
+
join(home, '.local', 'share', 'mise', 'installs', 'php'),
|
|
253
|
+
]) {
|
|
254
|
+
for (const v of readdirSafe(base)) {
|
|
255
|
+
const p = join(base, v, 'bin', 'php');
|
|
256
|
+
if (isFile(p)) add(p, base);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** One interpreter, resolved against the app's requirement. */
|
|
263
|
+
function pick(bin, source, requirement, probe) {
|
|
264
|
+
const version = probe(bin);
|
|
265
|
+
if (!version) return null;
|
|
266
|
+
return { bin, version, source, satisfies: requirement ? satisfies(version, requirement) : true };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Resolve the interpreter that can boot this app.
|
|
271
|
+
*
|
|
272
|
+
* @param {object} opts
|
|
273
|
+
* @param {string|null} opts.requirement the app's `require.php` (null = unknown)
|
|
274
|
+
* @param {string} [opts.explicit] PHP_BIN, when set
|
|
275
|
+
* @param {string} [opts.current] the default binary name ('php')
|
|
276
|
+
* @param {(bin:string)=>string|null} [opts.probe] injectable for tests
|
|
277
|
+
* @param {()=>{bin:string,source:string}[]} [opts.list] injectable for tests
|
|
278
|
+
* @returns {{bin:string,version:string,source:string}}
|
|
279
|
+
* @throws {Error} when no interpreter can boot the app
|
|
280
|
+
*/
|
|
281
|
+
export function resolvePhp(opts = {}) {
|
|
282
|
+
const {
|
|
283
|
+
requirement = null,
|
|
284
|
+
explicit = process.env.PHP_BIN,
|
|
285
|
+
current = 'php',
|
|
286
|
+
probe = probePhp,
|
|
287
|
+
list = phpCandidates,
|
|
288
|
+
} = opts;
|
|
289
|
+
|
|
290
|
+
// 1. An explicit override is a decision, not a hint: honour it and report a
|
|
291
|
+
// mismatch instead of quietly building with a different interpreter.
|
|
292
|
+
if (explicit) {
|
|
293
|
+
const bin = toNativePath(explicit);
|
|
294
|
+
const info = pick(bin, 'PHP_BIN', requirement, probe);
|
|
295
|
+
if (!info) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`PHP_BIN is set to "${explicit}", but that is not a working PHP interpreter.\n` +
|
|
298
|
+
` -> point PHP_BIN at a php executable (check it with: "${explicit}" -r "echo PHP_VERSION;")`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
if (info.satisfies === false) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
`PHP_BIN is set to "${explicit}" (PHP ${info.version}), but this app requires PHP ${requirement}.\n` +
|
|
304
|
+
` -> point PHP_BIN at a PHP that satisfies ${requirement}, or unset it to let the build pick one`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
return { bin: info.bin, version: info.version, source: info.source };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 2. The default interpreter, when it can boot the app.
|
|
311
|
+
const def = pick(current, 'PATH', requirement, probe);
|
|
312
|
+
if (def && def.satisfies !== false) return { bin: def.bin, version: def.version, source: def.source };
|
|
313
|
+
|
|
314
|
+
// 3. Anything else installed that satisfies the app; highest version wins.
|
|
315
|
+
const found = [];
|
|
316
|
+
for (const c of list()) {
|
|
317
|
+
const info = pick(c.bin, c.source, requirement, probe);
|
|
318
|
+
if (info) found.push(info);
|
|
319
|
+
}
|
|
320
|
+
const usable = found
|
|
321
|
+
.filter((i) => i.satisfies !== false)
|
|
322
|
+
.sort((a, b) => cmpVer(b.version, a.version));
|
|
323
|
+
if (usable.length) return { bin: usable[0].bin, version: usable[0].version, source: usable[0].source };
|
|
324
|
+
|
|
325
|
+
// 4. Nothing satisfies it. Name what exists and the ways out.
|
|
326
|
+
const seen = [];
|
|
327
|
+
for (const i of [def, ...found].filter(Boolean)) {
|
|
328
|
+
if (seen.some((s) => s.bin === i.bin)) continue;
|
|
329
|
+
seen.push(i);
|
|
330
|
+
}
|
|
331
|
+
const req = requirement ? `PHP ${requirement}` : 'a PHP that can boot this app';
|
|
332
|
+
const foundLines = seen.length
|
|
333
|
+
? seen.map((i) => ` ${i.bin} (PHP ${i.version}, from ${i.source})`).join('\n')
|
|
334
|
+
: ' (no PHP interpreter was found at all)';
|
|
335
|
+
throw new Error(
|
|
336
|
+
`no installed PHP can boot this app — it requires ${req}.\n` +
|
|
337
|
+
` interpreters found:\n${foundLines}\n` +
|
|
338
|
+
` -> install a PHP that satisfies ${req}, then re-run; or point at an existing one:\n` +
|
|
339
|
+
` PHP_BIN=/path/to/php npx laravel-workers build --strict\n` +
|
|
340
|
+
` (PHP_BIN is honoured by every build tool that boots the app)`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
// tools/php-serialize-oracle.php — PHP 8.3 serialize()/unserialize() oracle.
|
|
3
|
+
//
|
|
4
|
+
// Used by kernel/tests/php-serialize.test.mjs to prove that
|
|
5
|
+
// kernel/php-serialize.mjs produces byte-identical output to PHP 8.3
|
|
6
|
+
// (serialize_precision=-1) for the session payload subset.
|
|
7
|
+
//
|
|
8
|
+
// Protocol: JSON on stdin, JSON on stdout.
|
|
9
|
+
// { "mode": "encode", "values": [tagged, ...] }
|
|
10
|
+
// -> { "results": [ {"out": "<serialize()>"} | {"err": "..."}, ... ] }
|
|
11
|
+
// { "mode": "roundtrip", "strings": ["<serialized>", ...] }
|
|
12
|
+
// -> { "results": [ {"tagged": tagged, "reserialized": "..."} | {"err": "..."}, ... ] }
|
|
13
|
+
//
|
|
14
|
+
// Tagged value format (JSON-safe):
|
|
15
|
+
// {"t":"n"} null
|
|
16
|
+
// {"t":"b","v":true} bool
|
|
17
|
+
// {"t":"i","v":"42"} int (string to survive JS JSON)
|
|
18
|
+
// {"t":"d","v":"1.5"} float (string; "NAN"/"INF"/"-INF" supported)
|
|
19
|
+
// {"t":"s","v":"str"} string
|
|
20
|
+
// {"t":"a","v":{"k":tagged,...}} or {"t":"a","v":[tagged,...]} array
|
|
21
|
+
|
|
22
|
+
function fromTagged($t)
|
|
23
|
+
{
|
|
24
|
+
if (!is_array($t) || !isset($t['t'])) {
|
|
25
|
+
throw new Exception('bad tag structure');
|
|
26
|
+
}
|
|
27
|
+
switch ($t['t']) {
|
|
28
|
+
case 'n':
|
|
29
|
+
return null;
|
|
30
|
+
case 'b':
|
|
31
|
+
return (bool) $t['v'];
|
|
32
|
+
case 'i':
|
|
33
|
+
return intval($t['v']);
|
|
34
|
+
case 'd':
|
|
35
|
+
$s = (string) $t['v'];
|
|
36
|
+
if ($s === 'NAN') return NAN;
|
|
37
|
+
if ($s === 'INF') return INF;
|
|
38
|
+
if ($s === '-INF') return -INF;
|
|
39
|
+
return floatval($s);
|
|
40
|
+
case 's':
|
|
41
|
+
return (string) $t['v'];
|
|
42
|
+
case 'a':
|
|
43
|
+
$a = [];
|
|
44
|
+
if (!is_array($t['v'])) throw new Exception('bad array tag');
|
|
45
|
+
foreach ($t['v'] as $k => $v) {
|
|
46
|
+
// PHP auto-casts canonical decimal string keys to int, like a real $a["5"] = ...
|
|
47
|
+
$a[$k] = fromTagged($v);
|
|
48
|
+
}
|
|
49
|
+
return $a;
|
|
50
|
+
}
|
|
51
|
+
throw new Exception('unknown tag: ' . $t['t']);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function toTagged($v)
|
|
55
|
+
{
|
|
56
|
+
if (is_null($v)) return ['t' => 'n'];
|
|
57
|
+
if (is_bool($v)) return ['t' => 'b', 'v' => $v];
|
|
58
|
+
if (is_int($v)) return ['t' => 'i', 'v' => (string) $v];
|
|
59
|
+
if (is_float($v)) {
|
|
60
|
+
if (is_nan($v)) return ['t' => 'd', 'v' => 'NAN'];
|
|
61
|
+
if ($v === INF) return ['t' => 'd', 'v' => 'INF'];
|
|
62
|
+
if ($v === -INF) return ['t' => 'd', 'v' => '-INF'];
|
|
63
|
+
return ['t' => 'd', 'v' => sprintf('%.17G', $v)];
|
|
64
|
+
}
|
|
65
|
+
if (is_string($v)) return ['t' => 's', 'v' => $v];
|
|
66
|
+
if (is_array($v)) {
|
|
67
|
+
$pairs = [];
|
|
68
|
+
foreach ($v as $k => $vv) {
|
|
69
|
+
$pairs[(string) $k] = toTagged($vv); // order preserved for string+int keys in JSON objects? see note below
|
|
70
|
+
}
|
|
71
|
+
return ['t' => 'a', 'v' => $pairs];
|
|
72
|
+
}
|
|
73
|
+
throw new Exception('unsupported PHP type: ' . gettype($v));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
$raw = stream_get_contents(STDIN);
|
|
77
|
+
if ($raw === false || trim($raw) === '') {
|
|
78
|
+
fwrite(STDERR, "no input\n");
|
|
79
|
+
exit(1);
|
|
80
|
+
}
|
|
81
|
+
$in = json_decode($raw, true);
|
|
82
|
+
if (!is_array($in) || !isset($in['mode'])) {
|
|
83
|
+
fwrite(STDERR, "bad input json\n");
|
|
84
|
+
exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
$results = [];
|
|
88
|
+
if ($in['mode'] === 'encode') {
|
|
89
|
+
foreach ($in['values'] as $tagged) {
|
|
90
|
+
try {
|
|
91
|
+
$v = fromTagged($tagged);
|
|
92
|
+
$results[] = ['out' => serialize($v)];
|
|
93
|
+
} catch (Throwable $e) {
|
|
94
|
+
$results[] = ['err' => $e->getMessage()];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} elseif ($in['mode'] === 'roundtrip') {
|
|
98
|
+
foreach ($in['strings'] as $s) {
|
|
99
|
+
try {
|
|
100
|
+
$v = @unserialize($s);
|
|
101
|
+
if ($v === false && $s !== 'b:0;') {
|
|
102
|
+
throw new Exception('php unserialize failed');
|
|
103
|
+
}
|
|
104
|
+
$results[] = ['tagged' => toTagged($v), 'reserialized' => serialize($v)];
|
|
105
|
+
} catch (Throwable $e) {
|
|
106
|
+
$results[] = ['err' => $e->getMessage()];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
fwrite(STDERR, "unknown mode\n");
|
|
111
|
+
exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
echo json_encode(['results' => $results], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|