1121 lines
38 KiB
JavaScript
1121 lines
38 KiB
JavaScript
import { createHash } from "node:crypto";
|
|
import {
|
|
constants as fsConstants,
|
|
copyFile,
|
|
lstat,
|
|
mkdir,
|
|
mkdtemp,
|
|
readFile,
|
|
readdir,
|
|
realpath,
|
|
rename,
|
|
rm,
|
|
writeFile,
|
|
} from "node:fs/promises";
|
|
import { createRequire } from "node:module";
|
|
import path from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import { PHP, loadPHPRuntime } from "@php-wasm/universal";
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
|
export const PHP_ENGINE_LOCK = Object.freeze({
|
|
bridgeAbi: 1,
|
|
phpVersion: "8.5.8",
|
|
pcreVersion: "10.44",
|
|
packageVersion: "3.1.46",
|
|
asyncMode: "asyncify",
|
|
platform: "wasm32-emscripten",
|
|
loaderBytes: 270_942,
|
|
wasmBytes: 21_019_221,
|
|
maximumPatternUtf16: 65_536,
|
|
maximumSubjectBytes: 16_777_216,
|
|
maximumReplacementUtf16: 65_536,
|
|
maximumOutputBytes: 67_108_864,
|
|
maximumMatches: 10_000,
|
|
maximumCaptureRows: 100_000,
|
|
maximumCaptureGroups: 1_000,
|
|
npm: Object.freeze({
|
|
web: Object.freeze({
|
|
name: "@php-wasm/web-8-5",
|
|
version: "3.1.46",
|
|
resolved:
|
|
"https://registry.npmjs.org/@php-wasm/web-8-5/-/web-8-5-3.1.46.tgz",
|
|
integrity:
|
|
"sha512-j9wwlY6aNXuKBr4JPJzqE+vkM3Onk+byJsxLqaJs4U0gYDUBLr8CuKuVr+p13apSTUosConePYTAs0214aNxYA==",
|
|
packageJsonSha256:
|
|
"bc69e9a9f88ed9c2e2c3ccd06ce7f842007cc0ef8edd4496a91b0131afdef1e4",
|
|
}),
|
|
universal: Object.freeze({
|
|
name: "@php-wasm/universal",
|
|
version: "3.1.46",
|
|
resolved:
|
|
"https://registry.npmjs.org/@php-wasm/universal/-/universal-3.1.46.tgz",
|
|
integrity:
|
|
"sha512-sx0KyN5kcsCMzwy+XJAyoOukSA5TEZYd0LK3l9kRP3I0CSd5h6JItkf5eZ42WltB+wD+aTRdqSwc/Mvkc7qBjQ==",
|
|
packageJsonSha256:
|
|
"051ffd543901513ad76b5f935882b99988622e67086df16df48fef8f7c12a116",
|
|
}),
|
|
util: Object.freeze({
|
|
name: "@php-wasm/util",
|
|
version: "3.1.46",
|
|
resolved: "https://registry.npmjs.org/@php-wasm/util/-/util-3.1.46.tgz",
|
|
integrity:
|
|
"sha512-Yh9HNbx+rB2R1tScUvvAsbJVIB7msjWceWYogyR2YvAOqlDL0ymZyaQ65+K3QcHh/8HostTRwwgQh5oK4DLDrA==",
|
|
packageJsonSha256:
|
|
"4d2d42e0495a92e1fed76ac6273cf807d84466fe6dbd4718c5602012968d6e78",
|
|
}),
|
|
logger: Object.freeze({
|
|
name: "@php-wasm/logger",
|
|
version: "3.1.46",
|
|
resolved:
|
|
"https://registry.npmjs.org/@php-wasm/logger/-/logger-3.1.46.tgz",
|
|
integrity:
|
|
"sha512-PfnDq5ammh5FXxkGX1TGEck9gmW3i76IvBCqtX4D45xnSBIuCeu8uf2oWuty2j/c8wTEO8vyRDdvCj5oow7bXw==",
|
|
packageJsonSha256:
|
|
"4e04ad8620452b48b1f225431c8143f4e14888408db2e3a166e02e4ac6863fc9",
|
|
}),
|
|
compression: Object.freeze({
|
|
name: "@php-wasm/stream-compression",
|
|
version: "3.1.46",
|
|
resolved:
|
|
"https://registry.npmjs.org/@php-wasm/stream-compression/-/stream-compression-3.1.46.tgz",
|
|
integrity:
|
|
"sha512-+oUw4CPX52FgYXEh5C+MgJAg7Cy5OeaVuh7HnQR6oxb69nU63/r8tHTLV/WrWRyjBYh321uJzLV7Gg29qmiUtg==",
|
|
packageJsonSha256:
|
|
"5a2b0ca2dff1466bdeb38c9dca93d2fb0154cf9fef254a7c475d8cfe32344b72",
|
|
}),
|
|
progress: Object.freeze({
|
|
name: "@php-wasm/progress",
|
|
version: "3.1.46",
|
|
resolved:
|
|
"https://registry.npmjs.org/@php-wasm/progress/-/progress-3.1.46.tgz",
|
|
integrity:
|
|
"sha512-WmmgEm6yz322M3pzgAUjxckyhpVxr2WWlHyVYDyaMZWU6UIDUHfiRZRPzADohNyYvV3g0/zMNaDGmxz+TTXwQQ==",
|
|
packageJsonSha256:
|
|
"0408ea4cdb2d251ad357090e9532f50769e9ca7c135aa823ccad9e7b95950a36",
|
|
}),
|
|
ini: Object.freeze({
|
|
name: "ini",
|
|
version: "4.1.2",
|
|
resolved: "https://registry.npmjs.org/ini/-/ini-4.1.2.tgz",
|
|
integrity:
|
|
"sha512-AMB1mvwR1pyBFY/nSevUX6y8nJWS63/SzUKD3JyQn97s4xgIdgQPT75IRouIiBAN4yLQBUShNYVW0+UG25daCw==",
|
|
packageJsonSha256:
|
|
"0bc65f73405054313cab76d1d5f89b888ad2c0e1261d3a97644728852a5df8dc",
|
|
}),
|
|
featureDetect: Object.freeze({
|
|
name: "wasm-feature-detect",
|
|
version: "1.8.0",
|
|
resolved:
|
|
"https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz",
|
|
integrity:
|
|
"sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==",
|
|
packageJsonSha256:
|
|
"f511a8499a742e364d5bc9cdb23fb09108b12c7683fe06fb9e2bc324832b3b5b",
|
|
}),
|
|
}),
|
|
sourceAssets: Object.freeze({
|
|
loader: "0ed44eae7cd040826775b3dcd5376bb86a14257ae9fe1ff5cda1b7daf7572088",
|
|
wasm: "ed97ea5422dbf23e0687c98203cc8c6d7f2772a3d71b03d4991d2fdd93a048f0",
|
|
nativeInventory:
|
|
"c0da167d0a43b0d85f72ca17fed7d823c84b85718e4c8abf5f120f61f7c8654a",
|
|
phpWasmLicense:
|
|
"8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643",
|
|
iniLicense:
|
|
"4ec3d4c66cd87f5c8d8ad911b10f99bf27cb00cdfcff82621956e379186b016b",
|
|
featureDetectLicense:
|
|
"bcf29b4fd3ec2cb5f9d40a0866da446f6da62170d2ccedf4aeca9cf9406dd20c",
|
|
}),
|
|
legalAssets: Object.freeze({
|
|
"LICENSE.Emscripten-4.0.19.txt":
|
|
"620a78084fc7ca97c0b5dea9abf891f3ffcadfdbf305276f099c9c4e12fc1d86",
|
|
"LICENSE.Oniguruma-6.9.10.txt":
|
|
"70ba5469ea0bab6e18a32d7009068f996503168d27be57747e08da34337ff26f",
|
|
"LICENSE.OpenSSL-1.1.1t.txt":
|
|
"c32913b33252e71190af2066f08115c69bc9fddadf3bf29296e20c835389841c",
|
|
"LICENSE.PCRE2-10.44.txt":
|
|
"030087e2e8dd7c1bdd26057d25d4ded8f45bbf01ad458d68665ad04b8b0fbedf",
|
|
"LICENSE.PHP-4.0.txt":
|
|
"e66a38e0a83f49b6ad6ec72f6c0d7600cfa35ba95ee1a67eeeae676cd4416789",
|
|
"LICENSE.PHP-CLI-http-parser-MIT.txt":
|
|
"8539e2b4bd4e874b0f7d94a25b8f1b445d46ae22c8373d5a69f801a1a12f3265",
|
|
"LICENSE.PHP-Lexbor-Apache-2.0.txt":
|
|
"0d542e0c8804e39aa7f37eb00da5a762149dc682d7829451287e11b938e94594",
|
|
"LICENSE.PHP-Zend-2.0.txt":
|
|
"509aeb3be64e428d7e0004ce224620ab01bf94adaacefbb449611939df56bc5b",
|
|
"LICENSE.PHP-bcmath-LGPL-2.1.txt":
|
|
"7586d794d062a2a4a16513d95358aecaee4450e2062f7e64ce5ce63ac7212fd6",
|
|
"LICENSE.PHP-libavifinfo-BSD-2-Clause.txt":
|
|
"5d40dcb72f934c3d08e10ddb8ba4844de17bf6f9aabc1a382aacf8a8bf4567f8",
|
|
"LICENSE.PHP-libmagic-BSD.txt":
|
|
"0bfa856a9930bddadbef95d1be1cf4e163c0be618e76ea3275caaf255283e274",
|
|
"LICENSE.PHP-libmbfl-LGPL-2.1.txt":
|
|
"31d9cadcc63c15b79fc59c70f043956104a34c75f23f0ac0ada53c1edfeb88ca",
|
|
"LICENSE.PHP-timelib-MIT.txt":
|
|
"2120d029e7c67b9d08c1f665f1c3ed581206f00970b664cccdebe1419bf566e9",
|
|
"LICENSE.PHP-uriparser-BSD-3-Clause.txt":
|
|
"287f09e6546a9610f949f89e8fb937cacfeabd7bfaa8c8a0c18312193bf04ad3",
|
|
"LICENSE.curl-7.69.1.txt":
|
|
"db3c4a3b3695a0f317a0c5176acd2f656d18abc45b3ee78e50935a78eb1e132e",
|
|
"LICENSE.ini-ISC.txt":
|
|
"4ec3d4c66cd87f5c8d8ad911b10f99bf27cb00cdfcff82621956e379186b016b",
|
|
"LICENSE.libaom-3.12.1.txt":
|
|
"4764a286d8b2faeaf42f4418e7d7a28d58fc8fd4d00a3d0a7f44b0a4099de7f2",
|
|
"LICENSE.libavif-1.3.0.txt":
|
|
"165abf92cc04b39e80d29cadea7a6a7e8fddf59407d4ad2616507a7ebe8216f9",
|
|
"LICENSE.libcxx.txt":
|
|
"539dd7aed86e8a4f12cbdd0e6c50c189c7d74847e4fecc64ce2c6ee3a01da38b",
|
|
"LICENSE.libcxxabi.txt":
|
|
"e2b35be49f7284a45b7baca8fc7b3ab7440e7902392b2528a457816b5bb2a15c",
|
|
"LICENSE.libgd-2.3.3.txt":
|
|
"005f4b6b0141d1bd11d371bbf7d4f67947f85a4906b7f5465f942204cf918ba3",
|
|
"LICENSE.libiconv-1.17-LGPL-2.1.txt":
|
|
"dc626520dcd53a22f727af3ee42c770e56c97a64fe3adb063799d8ab032fe551",
|
|
"LICENSE.libjpeg-turbo-3.0.3.txt":
|
|
"e548c96c0c09397c94b5c5531836c02ff2c403f1f2b130cad8e3ec9439b5a7cc",
|
|
"LICENSE.libpng-1.6.39.txt":
|
|
"dfe5a536b0e5a531f844fb9c101a3089aca60772a503893b8e15f9457e369960",
|
|
"LICENSE.libwebp.txt":
|
|
"5aec868f669e384a22372a4e8a1a6cd7d44c64cd451f960ca69cc170d1e13acf",
|
|
"LICENSE.libxml2-2.9.10.txt":
|
|
"c5c63674f8a83c4d2e385d96d1c670a03cb871ba2927755467017317878574bd",
|
|
"LICENSE.libyuv.txt":
|
|
"2b2cc1180c7e6988328ad2033b04b80117419db9c4c584918bbb3cfec7e9364f",
|
|
"LICENSE.libzip-1.9.2.txt":
|
|
"01c022eca6d566e2e8792fd0f091a28653b2a608319922bcd4de91c49d1438e1",
|
|
"LICENSE.llvm-compiler-rt.txt":
|
|
"1a8f1058753f1ba890de984e48f0242a3a5c29a6a8f2ed9fd813f36985387e8d",
|
|
"LICENSE.musl.txt":
|
|
"f9bc4423732350eb0b3f7ed7e91d530298476f8fec0c6c427a1c04ade22655af",
|
|
"LICENSE.php-wasm-GPL-2.0-or-later.txt":
|
|
"8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643",
|
|
"LICENSE.wasm-feature-detect-Apache-2.0.txt":
|
|
"bcf29b4fd3ec2cb5f9d40a0866da446f6da62170d2ccedf4aeca9cf9406dd20c",
|
|
"LICENSE.zlib-1.2.13.txt":
|
|
"845efc77857d485d91fb3e0b884aaa929368c717ae8186b66fe1ed2495753243",
|
|
"NOTICE.IANA-TZDATA-2026.1.txt":
|
|
"8a19c6e511386301fa99ac61fec34c8be57be30255b29dad3ab294fe6b5da7c5",
|
|
"NOTICE.PHP-Lexbor.txt":
|
|
"b87f965fd2eba846a0a502d633dd7e7a680b93de5c514c404c948ccf1e5c9dc7",
|
|
"NOTICE.PHP-REDIST-BINS.txt":
|
|
"9159e8c784edf5ab9069c573bdc64a7a406ebe718768d2c25a0b243449b7665b",
|
|
"NOTICE.PHP-public-domain-hash-code.txt":
|
|
"1fc21940918bd2027d7b9ffccd23d45d35b33d6ca5b42a6fb481e75809763a98",
|
|
"NOTICE.SQLite-3.51.0.txt":
|
|
"b8262a33944a8937e6f38f573df2d945101b4bcb45de1b150c7e3a6ebd23b19b",
|
|
"NOTICE.dlmalloc-2.8.6.txt":
|
|
"ea9de40ffcf0d3536a71e22af9c33647c517256aa2a43167e933e74535569594",
|
|
"PATENTS.PHP-libavifinfo.txt":
|
|
"aef98cc6105f279463c37ec8c46dec5f2d133b6a91531ae9ec274e80fece9a88",
|
|
"PATENTS.libaom-3.12.1.txt":
|
|
"661fb8e504744e95587b556b94a58343448300606a41bea8c7a9b97125696e61",
|
|
"PATENTS.libwebp.txt":
|
|
"cc3273e0694ea5896145e0677699b53471b03ea43021ddc50e7923fbb9f5023c",
|
|
}),
|
|
});
|
|
|
|
const PACK_FILES = Object.freeze(
|
|
[
|
|
...Object.keys(PHP_ENGINE_LOCK.legalAssets),
|
|
"SHA256SUMS",
|
|
"engine-metadata.json",
|
|
"native-components.json",
|
|
"php-loader.mjs",
|
|
"php.wasm",
|
|
].sort(),
|
|
);
|
|
const CHECKSUM_FILES = Object.freeze(
|
|
PACK_FILES.filter((name) => name !== "SHA256SUMS"),
|
|
);
|
|
const LOADER_IMPORT = "import dependencyFilename from './8_5_8/php_8_5.wasm';";
|
|
const PATCHED_LOADER =
|
|
'const dependencyFilename = new URL("./php.wasm", import.meta.url).href;';
|
|
const PHP_NATIVE_COMPONENT_IDS = Object.freeze([
|
|
"wordpress-playground-php-wasm",
|
|
"php",
|
|
"pcre2",
|
|
"php-libbcmath",
|
|
"timelib",
|
|
"iana-tzdata",
|
|
"php-libmagic",
|
|
"lexbor",
|
|
"libmbfl",
|
|
"php-uriparser",
|
|
"php-libavifinfo",
|
|
"zlib",
|
|
"libzip",
|
|
"libxml2",
|
|
"sqlite",
|
|
"libgd",
|
|
"libjpeg-turbo",
|
|
"libpng",
|
|
"libwebp-and-libsharpyuv",
|
|
"libavif",
|
|
"libaom",
|
|
"libyuv",
|
|
"openssl",
|
|
"curl",
|
|
"libiconv",
|
|
"oniguruma",
|
|
"emscripten-runtime",
|
|
"musl-libc",
|
|
"llvm-compiler-rt",
|
|
"llvm-libcxx",
|
|
"llvm-libcxxabi",
|
|
"dlmalloc",
|
|
]);
|
|
const PHP_EXCLUDED_COMPONENT_IDS = Object.freeze([
|
|
"gmp",
|
|
"libsodium",
|
|
"tidy",
|
|
"intl-and-icu",
|
|
"freetype",
|
|
"imagemagick-and-imagick",
|
|
"standalone-libaom-3.13.1-recipe",
|
|
]);
|
|
const PHP_LOADED_EXTENSIONS = Object.freeze(
|
|
[
|
|
"Core",
|
|
"date",
|
|
"lexbor",
|
|
"openssl",
|
|
"pcre",
|
|
"sqlite3",
|
|
"zlib",
|
|
"bcmath",
|
|
"calendar",
|
|
"ctype",
|
|
"curl",
|
|
"dns_polyfill",
|
|
"uri",
|
|
"json",
|
|
"mbstring",
|
|
"SPL",
|
|
"fileinfo",
|
|
"filter",
|
|
"gd",
|
|
"hash",
|
|
"iconv",
|
|
"session",
|
|
"Zend OPcache",
|
|
"standard",
|
|
"libxml",
|
|
"PDO",
|
|
"pdo_sqlite",
|
|
"Phar",
|
|
"post_message_to_js",
|
|
"random",
|
|
"Reflection",
|
|
"exif",
|
|
"SimpleXML",
|
|
"soap",
|
|
"tokenizer",
|
|
"dom",
|
|
"wasm_memory_storage",
|
|
"xml",
|
|
"xmlreader",
|
|
"xmlwriter",
|
|
"zip",
|
|
].sort(compareText),
|
|
);
|
|
const PHP_BINARY_MARKERS = Object.freeze([
|
|
"AOMedia Project AV1 Encoder v3.12.1",
|
|
"AOMedia Project AV1 Decoder v3.12.1",
|
|
"libjpeg-turbo version 3.0.3 (build 20250321)",
|
|
]);
|
|
const PHP_EXCLUDED_BINARY_MARKERS = Object.freeze([
|
|
"AOMedia Project AV1 Encoder v3.13.1",
|
|
"AOMedia Project AV1 Decoder v3.13.1",
|
|
]);
|
|
|
|
function compareText(left, right) {
|
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
}
|
|
|
|
function sha256(value) {
|
|
return createHash("sha256").update(value).digest("hex");
|
|
}
|
|
|
|
async function sha256File(file) {
|
|
return sha256(await readFile(file));
|
|
}
|
|
|
|
async function assertRealDirectory(candidate, label) {
|
|
const details = await lstat(candidate).catch(() => null);
|
|
if (!details?.isDirectory() || details.isSymbolicLink()) {
|
|
throw new Error(`${label} must be a real directory: ${candidate}`);
|
|
}
|
|
return realpath(candidate);
|
|
}
|
|
|
|
async function assertRegularFile(candidate, label) {
|
|
const details = await lstat(candidate).catch(() => null);
|
|
if (!details?.isFile() || details.isSymbolicLink()) {
|
|
throw new Error(
|
|
`${label} must be a regular, non-symlink file: ${candidate}`,
|
|
);
|
|
}
|
|
return realpath(candidate);
|
|
}
|
|
|
|
async function assertHash(file, expected, label) {
|
|
const actual = await sha256File(file);
|
|
if (actual !== expected) {
|
|
throw new Error(
|
|
`${label} SHA-256 mismatch: expected ${expected}, got ${actual}.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertExactJson(actual, expected, label) {
|
|
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
throw new Error(`${label} drifted from the pinned PHP runtime inventory.`);
|
|
}
|
|
}
|
|
|
|
function validateNativeInventory(inventory) {
|
|
if (
|
|
inventory?.schemaVersion !== 1 ||
|
|
inventory.scope?.assetSha256 !== PHP_ENGINE_LOCK.sourceAssets.wasm ||
|
|
inventory.scope?.assetBytes !== PHP_ENGINE_LOCK.wasmBytes ||
|
|
inventory.scope?.phpVersion !== PHP_ENGINE_LOCK.phpVersion ||
|
|
inventory.scope?.platform !== PHP_ENGINE_LOCK.platform ||
|
|
inventory.buildProvenance?.wordpressPlayground?.release !==
|
|
`v${PHP_ENGINE_LOCK.packageVersion}` ||
|
|
inventory.buildProvenance?.wordpressPlayground?.commit !==
|
|
"581c7c172428159eb4e6c5309054a568cd39a97a" ||
|
|
inventory.buildProvenance?.php?.commit !==
|
|
"26b97507444c4fbda072f57dda1820f7b7d5e467" ||
|
|
inventory.buildProvenance?.emscripten?.version !== "4.0.19" ||
|
|
inventory.buildProvenance?.emscripten?.commit !==
|
|
"08e2de1031913e4ba7963b1c56f35f036a7d4d56"
|
|
) {
|
|
throw new Error("PHP native-component provenance identity drifted.");
|
|
}
|
|
const componentIds = inventory.linkedComponents?.map(({ id }) => id);
|
|
const excludedIds = inventory.excludedDetections?.map(({ id }) => id);
|
|
assertExactJson(
|
|
componentIds,
|
|
PHP_NATIVE_COMPONENT_IDS,
|
|
"PHP linked-component list",
|
|
);
|
|
assertExactJson(
|
|
excludedIds,
|
|
PHP_EXCLUDED_COMPONENT_IDS,
|
|
"PHP excluded-detection list",
|
|
);
|
|
const referencedLegalFiles = new Set();
|
|
for (const component of inventory.linkedComponents) {
|
|
if (
|
|
!Array.isArray(component.legalFiles) ||
|
|
component.legalFiles.length === 0
|
|
) {
|
|
throw new Error(
|
|
`PHP native component ${component.id} has no legal-file route.`,
|
|
);
|
|
}
|
|
for (const name of component.legalFiles) {
|
|
if (!Object.hasOwn(PHP_ENGINE_LOCK.legalAssets, name)) {
|
|
throw new Error(
|
|
`PHP native component ${component.id} references an unpinned legal file: ${name}.`,
|
|
);
|
|
}
|
|
referencedLegalFiles.add(name);
|
|
}
|
|
}
|
|
for (const name of Object.keys(PHP_ENGINE_LOCK.legalAssets)) {
|
|
if (
|
|
name !== "LICENSE.ini-ISC.txt" &&
|
|
name !== "LICENSE.wasm-feature-detect-Apache-2.0.txt" &&
|
|
!referencedLegalFiles.has(name)
|
|
) {
|
|
throw new Error(
|
|
`Pinned PHP native legal file is not routed from the inventory: ${name}.`,
|
|
);
|
|
}
|
|
}
|
|
return inventory;
|
|
}
|
|
|
|
async function verifyPackage(projectRoot, descriptor) {
|
|
const directory = path.join(
|
|
projectRoot,
|
|
"node_modules",
|
|
...descriptor.name.split("/"),
|
|
);
|
|
await assertRealDirectory(directory, `${descriptor.name} npm package`);
|
|
const packageFile = path.join(directory, "package.json");
|
|
await assertRegularFile(packageFile, `${descriptor.name} package.json`);
|
|
await assertHash(
|
|
packageFile,
|
|
descriptor.packageJsonSha256,
|
|
`${descriptor.name} package.json`,
|
|
);
|
|
const packageJson = JSON.parse(await readFile(packageFile, "utf8"));
|
|
if (
|
|
packageJson.name !== descriptor.name ||
|
|
packageJson.version !== descriptor.version
|
|
) {
|
|
throw new Error(`${descriptor.name} npm package identity drifted.`);
|
|
}
|
|
return directory;
|
|
}
|
|
|
|
async function verifySources(projectRoot) {
|
|
const lock = JSON.parse(
|
|
await readFile(path.join(projectRoot, "package-lock.json"), "utf8"),
|
|
);
|
|
const rootEntry = lock.packages?.[""];
|
|
for (const descriptor of Object.values(PHP_ENGINE_LOCK.npm)) {
|
|
const entry = lock.packages?.[`node_modules/${descriptor.name}`];
|
|
if (
|
|
entry?.version !== descriptor.version ||
|
|
entry?.resolved !== descriptor.resolved ||
|
|
entry?.integrity !== descriptor.integrity
|
|
) {
|
|
throw new Error(
|
|
`${descriptor.name}@${descriptor.version} is not exactly pinned in package-lock.json.`,
|
|
);
|
|
}
|
|
await verifyPackage(projectRoot, descriptor);
|
|
}
|
|
for (const direct of [
|
|
PHP_ENGINE_LOCK.npm.web,
|
|
PHP_ENGINE_LOCK.npm.universal,
|
|
]) {
|
|
if (rootEntry?.devDependencies?.[direct.name] !== direct.version) {
|
|
throw new Error(`${direct.name} must be an exact root devDependency.`);
|
|
}
|
|
}
|
|
|
|
const web = path.join(projectRoot, "node_modules", "@php-wasm", "web-8-5");
|
|
const loader = path.join(web, "asyncify", "php_8_5.js");
|
|
const wasm = path.join(web, "asyncify", "8_5_8", "php_8_5.wasm");
|
|
const phpWasmLicense = path.join(web, "LICENSE");
|
|
const iniLicense = path.join(projectRoot, "node_modules", "ini", "LICENSE");
|
|
const featureDetectLicense = path.join(
|
|
projectRoot,
|
|
"node_modules",
|
|
"wasm-feature-detect",
|
|
"LICENSE",
|
|
);
|
|
const engineSourceDirectory = path.join(projectRoot, "engines", "php");
|
|
const nativeInventory = path.join(
|
|
engineSourceDirectory,
|
|
"native-components.json",
|
|
);
|
|
const legalFiles = Object.fromEntries(
|
|
Object.keys(PHP_ENGINE_LOCK.legalAssets).map((name) => [
|
|
name,
|
|
path.join(engineSourceDirectory, name),
|
|
]),
|
|
);
|
|
Object.assign(legalFiles, {
|
|
"LICENSE.php-wasm-GPL-2.0-or-later.txt": phpWasmLicense,
|
|
"LICENSE.ini-ISC.txt": iniLicense,
|
|
"LICENSE.wasm-feature-detect-Apache-2.0.txt": featureDetectLicense,
|
|
});
|
|
for (const [file, expected, label] of [
|
|
[loader, PHP_ENGINE_LOCK.sourceAssets.loader, "PHP Asyncify loader"],
|
|
[wasm, PHP_ENGINE_LOCK.sourceAssets.wasm, "PHP Asyncify WebAssembly"],
|
|
[
|
|
nativeInventory,
|
|
PHP_ENGINE_LOCK.sourceAssets.nativeInventory,
|
|
"PHP native-component inventory",
|
|
],
|
|
]) {
|
|
await assertRegularFile(file, label);
|
|
await assertHash(file, expected, label);
|
|
}
|
|
for (const [name, expected] of Object.entries(PHP_ENGINE_LOCK.legalAssets)) {
|
|
const file = legalFiles[name];
|
|
await assertRegularFile(file, `PHP legal asset ${name}`);
|
|
await assertHash(file, expected, `PHP legal asset ${name}`);
|
|
}
|
|
validateNativeInventory(JSON.parse(await readFile(nativeInventory, "utf8")));
|
|
return {
|
|
loader,
|
|
wasm,
|
|
nativeInventory,
|
|
legalFiles,
|
|
};
|
|
}
|
|
|
|
function patchLoader(source) {
|
|
if (!source.startsWith(`${LOADER_IMPORT}\n`)) {
|
|
throw new Error("PHP Asyncify loader import prelude drifted.");
|
|
}
|
|
const patched = source.replace(LOADER_IMPORT, PATCHED_LOADER);
|
|
if (
|
|
patched === source ||
|
|
patched.includes(LOADER_IMPORT) ||
|
|
!patched.includes("export function init(RuntimeName, PHPLoader)")
|
|
) {
|
|
throw new Error("PHP Asyncify loader patch was not exact.");
|
|
}
|
|
return patched;
|
|
}
|
|
|
|
async function fileRecord(directory, name) {
|
|
const value = await readFile(path.join(directory, name));
|
|
return { path: name, sha256: sha256(value), bytes: value.byteLength };
|
|
}
|
|
|
|
async function expectedMetadata(projectRoot, pack) {
|
|
const bridgePath = path.join(projectRoot, "engines", "php", "bridge.php");
|
|
const bridge = await readFile(bridgePath);
|
|
const inventory = validateNativeInventory(
|
|
JSON.parse(
|
|
await readFile(path.join(pack, "native-components.json"), "utf8"),
|
|
),
|
|
);
|
|
const inventoryRecord = await fileRecord(pack, "native-components.json");
|
|
const files = [];
|
|
for (const name of CHECKSUM_FILES) {
|
|
if (name !== "engine-metadata.json") {
|
|
files.push(await fileRecord(pack, name));
|
|
}
|
|
}
|
|
return {
|
|
schemaVersion: 1,
|
|
engine: "php",
|
|
engineVersion: `PHP ${PHP_ENGINE_LOCK.phpVersion} preg / PCRE2 ${PHP_ENGINE_LOCK.pcreVersion}`,
|
|
runtime: `@php-wasm/web-8-5 ${PHP_ENGINE_LOCK.packageVersion} Asyncify`,
|
|
platform: PHP_ENGINE_LOCK.platform,
|
|
status: "production-runtime",
|
|
bridge: {
|
|
abiVersion: PHP_ENGINE_LOCK.bridgeAbi,
|
|
sourceFile: {
|
|
path: "engines/php/bridge.php",
|
|
sha256: sha256(bridge),
|
|
bytes: bridge.byteLength,
|
|
},
|
|
transport: "fixed VFS script plus JSON request",
|
|
userValuesAreJsonData: true,
|
|
replacement:
|
|
"bounded PHP 8.5.8 preg_get_backref-compatible expansion over native preg_match records",
|
|
replacementParityOracle:
|
|
"preg_replace is called only by the fixed identity self-test",
|
|
replacementTransport:
|
|
"bounded UTF-8 bytes encoded as base64 in the JSON envelope",
|
|
nativeOffsetUnit: "UTF-8 bytes (PREG_OFFSET_CAPTURE)",
|
|
maximumPatternUtf16: PHP_ENGINE_LOCK.maximumPatternUtf16,
|
|
maximumSubjectBytes: PHP_ENGINE_LOCK.maximumSubjectBytes,
|
|
maximumReplacementUtf16: PHP_ENGINE_LOCK.maximumReplacementUtf16,
|
|
maximumOutputBytes: PHP_ENGINE_LOCK.maximumOutputBytes,
|
|
maximumMatches: PHP_ENGINE_LOCK.maximumMatches,
|
|
maximumCaptureRows: PHP_ENGINE_LOCK.maximumCaptureRows,
|
|
maximumCaptureGroups: PHP_ENGINE_LOCK.maximumCaptureGroups,
|
|
},
|
|
source: {
|
|
repository: "https://github.com/WordPress/wordpress-playground",
|
|
packages: Object.values(PHP_ENGINE_LOCK.npm).map(
|
|
({ name, version, integrity }) => ({ name, version, integrity }),
|
|
),
|
|
runtimeVariant: "PHP 8.5 Asyncify web build (without intl extension)",
|
|
loaderPatch: {
|
|
originalSha256: PHP_ENGINE_LOCK.sourceAssets.loader,
|
|
originalBytes: PHP_ENGINE_LOCK.loaderBytes,
|
|
change:
|
|
"replace bundler-only wasm import with same-directory php.wasm URL",
|
|
},
|
|
php: {
|
|
version: PHP_ENGINE_LOCK.phpVersion,
|
|
pcreVersion: PHP_ENGINE_LOCK.pcreVersion,
|
|
asyncMode: PHP_ENGINE_LOCK.asyncMode,
|
|
platform: PHP_ENGINE_LOCK.platform,
|
|
},
|
|
buildProvenance: {
|
|
wordpressPlaygroundRelease: "v3.1.46",
|
|
wordpressPlaygroundCommit: "581c7c172428159eb4e6c5309054a568cd39a97a",
|
|
phpTag: "php-8.5.8",
|
|
phpTagObject: "8a3b5a5124006c11a8fbfce838ec7dd53615cc77",
|
|
phpCommit: "26b97507444c4fbda072f57dda1820f7b7d5e467",
|
|
emscriptenVersion: "4.0.19",
|
|
emscriptenCommit: "08e2de1031913e4ba7963b1c56f35f036a7d4d56",
|
|
},
|
|
nativeComponents: {
|
|
...inventoryRecord,
|
|
linkedComponentCount: inventory.linkedComponents.length,
|
|
excludedDetectionCount: inventory.excludedDetections.length,
|
|
scope:
|
|
"complete native/source/legal inventory for the exact shipped php.wasm",
|
|
},
|
|
embeddedPathClassification:
|
|
"Strings such as /home/web_user in generated Emscripten artifacts are virtual/build filesystem paths, not host source provenance.",
|
|
phpWasmLicenseAppliesTo: [
|
|
"@php-wasm/web-8-5",
|
|
"@php-wasm/universal",
|
|
"@php-wasm/util",
|
|
"@php-wasm/logger",
|
|
"@php-wasm/stream-compression",
|
|
"@php-wasm/progress",
|
|
],
|
|
},
|
|
licensing: {
|
|
phpCodebase: {
|
|
selectedLicense: "PHP License version 4 (BSD-3-Clause)",
|
|
election:
|
|
"PHP 8.5.8 is used under PHP License version 4 via the later-version option in section 5 of PHP License 3.01.",
|
|
provenanceUrl: "https://www.php.net/license/",
|
|
licenseTextUrl: "https://www.php.net/license/4_0.txt",
|
|
licenseTextSha256: PHP_ENGINE_LOCK.legalAssets["LICENSE.PHP-4.0.txt"],
|
|
},
|
|
pcre2: {
|
|
version: PHP_ENGINE_LOCK.pcreVersion,
|
|
license: "PCRE2 BSD license",
|
|
provenanceUrl:
|
|
"https://github.com/PCRE2Project/pcre2/blob/pcre2-10.44/LICENCE",
|
|
licenseTextSha256:
|
|
PHP_ENGINE_LOCK.legalAssets["LICENSE.PCRE2-10.44.txt"],
|
|
},
|
|
phpWasmPackages: "GPL-2.0-or-later",
|
|
ini: "ISC",
|
|
wasmFeatureDetect: "Apache-2.0",
|
|
nativeInventory: {
|
|
path: inventoryRecord.path,
|
|
sha256: inventoryRecord.sha256,
|
|
independentlyPinnedLegalAssets: Object.keys(PHP_ENGINE_LOCK.legalAssets)
|
|
.length,
|
|
statement:
|
|
"Every linked component routes to an independently SHA-256-pinned licence, notice, or patent file; raw-string false positives and unshipped side modules are explicitly excluded.",
|
|
},
|
|
},
|
|
files,
|
|
};
|
|
}
|
|
|
|
async function runPhpRequest(php, request) {
|
|
php.writeFile("/regex-tools/request.json", JSON.stringify(request));
|
|
const response = await php.run({ scriptPath: "/regex-tools/bridge.php" });
|
|
if (
|
|
response.exitCode !== 0 ||
|
|
response.errors !== "" ||
|
|
response.text.length === 0
|
|
) {
|
|
throw new Error("PHP bridge process failed its pack smoke test.");
|
|
}
|
|
return JSON.parse(response.text);
|
|
}
|
|
|
|
async function auditPhpRuntime(php) {
|
|
const response = await php.run({
|
|
code: `<?php
|
|
$extensions = get_loaded_extensions();
|
|
sort($extensions, SORT_STRING);
|
|
$gd = gd_info();
|
|
ob_start();
|
|
phpinfo(INFO_MODULES);
|
|
$moduleInfo = ob_get_clean();
|
|
$audit = [
|
|
'extensions' => $extensions,
|
|
'versions' => [
|
|
'php' => PHP_VERSION,
|
|
'pcre2' => PCRE_VERSION,
|
|
'openssl' => OPENSSL_VERSION_TEXT,
|
|
'curl' => curl_version()['version'],
|
|
'zlib' => ZLIB_VERSION,
|
|
'sqlite' => SQLite3::version()['versionString'],
|
|
'libxml2' => LIBXML_DOTTED_VERSION,
|
|
'libgd' => GD_VERSION,
|
|
'libiconv' => ICONV_VERSION,
|
|
'oniguruma' => MB_ONIGURUMA_VERSION,
|
|
'libzip' => ZipArchive::LIBZIP_VERSION,
|
|
],
|
|
'moduleMarkers' => [
|
|
'timelib-2022.15' => str_contains($moduleInfo, '2022.15'),
|
|
'tzdata-2026.1' => str_contains($moduleInfo, '2026.1'),
|
|
'lexbor-2.7.0' => str_contains($moduleInfo, '2.7.0'),
|
|
'libmbfl-1.3.2' => str_contains($moduleInfo, '1.3.2'),
|
|
'uriparser-1.0.2' => str_contains($moduleInfo, '1.0.2'),
|
|
],
|
|
'gdFeatures' => [
|
|
'jpeg' => $gd['JPEG Support'] ?? false,
|
|
'png' => $gd['PNG Support'] ?? false,
|
|
'webp' => $gd['WebP Support'] ?? false,
|
|
'avif' => $gd['AVIF Support'] ?? false,
|
|
'syntheticFreeTypeReport' => $gd['FreeType Support'] ?? false,
|
|
],
|
|
'hashAlgorithms' => array_fill_keys(
|
|
['fnv1a64', 'murmur3a', 'murmur3c', 'murmur3f', 'sha3-256', 'xxh32', 'xxh64', 'xxh3', 'xxh128'],
|
|
false,
|
|
),
|
|
'phpZts' => (bool) PHP_ZTS,
|
|
'excluded' => [
|
|
'gmp' => !extension_loaded('gmp') && !function_exists('gmp_init'),
|
|
'libsodium' => !extension_loaded('sodium') && !function_exists('sodium_crypto_box'),
|
|
'tidy' => !extension_loaded('tidy') && !function_exists('tidy_parse_string') && !class_exists('tidy'),
|
|
'intl-and-icu' => !extension_loaded('intl') && !class_exists('IntlDateFormatter'),
|
|
'imagemagick-and-imagick' => !extension_loaded('imagick') && !class_exists('Imagick'),
|
|
],
|
|
];
|
|
foreach (array_keys($audit['hashAlgorithms']) as $algorithm) {
|
|
$audit['hashAlgorithms'][$algorithm] = in_array($algorithm, hash_algos(), true);
|
|
}
|
|
echo json_encode($audit, JSON_THROW_ON_ERROR);`,
|
|
});
|
|
if (
|
|
response.exitCode !== 0 ||
|
|
response.errors !== "" ||
|
|
response.text.length === 0
|
|
) {
|
|
throw new Error("PHP native-component audit process failed.");
|
|
}
|
|
return JSON.parse(response.text);
|
|
}
|
|
|
|
async function smokePhp(pack, projectRoot) {
|
|
const wasm = await readFile(path.join(pack, "php.wasm"));
|
|
if (
|
|
wasm.byteLength !== PHP_ENGINE_LOCK.wasmBytes ||
|
|
!WebAssembly.validate(wasm)
|
|
) {
|
|
throw new Error("PHP runtime is not the expected WebAssembly module.");
|
|
}
|
|
for (const marker of PHP_BINARY_MARKERS) {
|
|
if (!wasm.includes(Buffer.from(marker, "utf8"))) {
|
|
throw new Error(`PHP WebAssembly is missing native marker: ${marker}.`);
|
|
}
|
|
}
|
|
for (const marker of PHP_EXCLUDED_BINARY_MARKERS) {
|
|
if (wasm.includes(Buffer.from(marker, "utf8"))) {
|
|
throw new Error(`PHP WebAssembly contains excluded marker: ${marker}.`);
|
|
}
|
|
}
|
|
const loaderUrl = `${pathToFileURL(path.join(pack, "php-loader.mjs")).href}?verify=${Date.now()}-${Math.random()}`;
|
|
const loader = await import(loaderUrl);
|
|
if (
|
|
loader.dependenciesTotalSize !== PHP_ENGINE_LOCK.wasmBytes ||
|
|
typeof loader.init !== "function"
|
|
) {
|
|
throw new Error("PHP Asyncify loader identity is invalid.");
|
|
}
|
|
|
|
const hadRequire = Object.hasOwn(globalThis, "require");
|
|
const previousRequire = globalThis.require;
|
|
globalThis.require = createRequire(import.meta.url);
|
|
let php;
|
|
try {
|
|
const runtimeId = await loadPHPRuntime(loader, {
|
|
wasmBinary: wasm,
|
|
phpWasmAsyncMode: "asyncify",
|
|
debug: false,
|
|
print: () => undefined,
|
|
printErr: () => undefined,
|
|
});
|
|
php = new PHP(runtimeId);
|
|
php.mkdir("/regex-tools");
|
|
php.writeFile(
|
|
"/regex-tools/bridge.php",
|
|
await readFile(
|
|
path.join(projectRoot, "engines", "php", "bridge.php"),
|
|
"utf8",
|
|
),
|
|
);
|
|
const identity = await runPhpRequest(php, {
|
|
abi: PHP_ENGINE_LOCK.bridgeAbi,
|
|
operation: "identity",
|
|
});
|
|
if (
|
|
identity?.ok !== true ||
|
|
identity.identity?.phpVersion !== PHP_ENGINE_LOCK.phpVersion ||
|
|
identity.identity?.pcreVersion !== PHP_ENGINE_LOCK.pcreVersion ||
|
|
identity.identity?.phpSapi !== "wasm" ||
|
|
identity.identity?.integerBytes !== 8 ||
|
|
identity.identity?.selfTest !== true
|
|
) {
|
|
throw new Error("PHP runtime failed its identity smoke test.");
|
|
}
|
|
const common = {
|
|
abi: PHP_ENGINE_LOCK.bridgeAbi,
|
|
pattern: "(?<word>é+)",
|
|
flags: "gu",
|
|
options: {},
|
|
subject: "😀éé x",
|
|
scanAll: true,
|
|
maximumMatches: 10,
|
|
maximumCaptureRows: 10,
|
|
};
|
|
const execution = await runPhpRequest(php, {
|
|
...common,
|
|
operation: "execute",
|
|
});
|
|
if (
|
|
execution?.ok !== true ||
|
|
execution.groupCount !== 1 ||
|
|
JSON.stringify(execution.groupNames) !== JSON.stringify([[1, "word"]]) ||
|
|
JSON.stringify(execution.matches?.[0]?.span) !== JSON.stringify([4, 8])
|
|
) {
|
|
throw new Error("PHP runtime failed its native match smoke test.");
|
|
}
|
|
const replacement = await runPhpRequest(php, {
|
|
...common,
|
|
operation: "replace",
|
|
replacement: "[$1]",
|
|
maximumOutputBytes: 1_024,
|
|
});
|
|
if (
|
|
replacement?.ok !== true ||
|
|
Object.hasOwn(replacement, "output") ||
|
|
replacement.outputBase64 !==
|
|
Buffer.from("😀[éé] x", "utf8").toString("base64") ||
|
|
Buffer.from(replacement.outputBase64, "base64").toString("utf8") !==
|
|
"😀[éé] x" ||
|
|
replacement.outputTruncated !== false
|
|
) {
|
|
throw new Error(
|
|
"PHP runtime failed its bounded replacement transport smoke test.",
|
|
);
|
|
}
|
|
const audit = await auditPhpRuntime(php);
|
|
assertExactJson(
|
|
audit.extensions,
|
|
PHP_LOADED_EXTENSIONS,
|
|
"PHP loaded-extension list",
|
|
);
|
|
assertExactJson(
|
|
audit.versions,
|
|
{
|
|
php: "8.5.8",
|
|
pcre2: "10.44 2024-06-07",
|
|
openssl: "OpenSSL 1.1.1t 7 Feb 2023",
|
|
curl: "7.69.1",
|
|
zlib: "1.2.13",
|
|
sqlite: "3.51.0",
|
|
libxml2: "2.9.10",
|
|
libgd: "2.3.3",
|
|
libiconv: "1.17",
|
|
oniguruma: "6.9.10",
|
|
libzip: "1.9.2",
|
|
},
|
|
"PHP linked-library versions",
|
|
);
|
|
if (
|
|
Object.values(audit.moduleMarkers).some((present) => present !== true) ||
|
|
Object.values(audit.gdFeatures).some((present) => present !== true) ||
|
|
Object.values(audit.hashAlgorithms).some((present) => present !== true) ||
|
|
audit.phpZts !== false ||
|
|
Object.values(audit.excluded).some((excluded) => excluded !== true)
|
|
) {
|
|
throw new Error(
|
|
"PHP native-component positive/negative runtime audit failed.",
|
|
);
|
|
}
|
|
} finally {
|
|
try {
|
|
php?.exit(0);
|
|
} catch {
|
|
// The Emscripten runtime may signal its normal exit as an exception.
|
|
}
|
|
if (hadRequire) globalThis.require = previousRequire;
|
|
else delete globalThis.require;
|
|
}
|
|
}
|
|
|
|
export async function verifyPhpPack(directory, projectRoot = root) {
|
|
const pack = await assertRealDirectory(directory, "PHP engine pack");
|
|
const names = (await readdir(pack, { withFileTypes: true }))
|
|
.map((entry) => {
|
|
if (!entry.isFile() || entry.isSymbolicLink()) {
|
|
throw new Error(`PHP pack contains a non-regular entry: ${entry.name}`);
|
|
}
|
|
return entry.name;
|
|
})
|
|
.sort(compareText);
|
|
if (JSON.stringify(names) !== JSON.stringify(PACK_FILES)) {
|
|
throw new Error(`PHP pack must contain only: ${PACK_FILES.join(", ")}.`);
|
|
}
|
|
const checksumLines = (await readFile(path.join(pack, "SHA256SUMS"), "utf8"))
|
|
.trimEnd()
|
|
.split("\n");
|
|
const expectedLines = [];
|
|
for (const name of CHECKSUM_FILES) {
|
|
expectedLines.push(`${await sha256File(path.join(pack, name))} ${name}`);
|
|
}
|
|
if (JSON.stringify(checksumLines) !== JSON.stringify(expectedLines)) {
|
|
throw new Error("PHP pack checksum manifest is stale or malformed.");
|
|
}
|
|
await assertHash(
|
|
path.join(pack, "php.wasm"),
|
|
PHP_ENGINE_LOCK.sourceAssets.wasm,
|
|
"Packed PHP WebAssembly",
|
|
);
|
|
await assertHash(
|
|
path.join(pack, "native-components.json"),
|
|
PHP_ENGINE_LOCK.sourceAssets.nativeInventory,
|
|
"Packed PHP native-component inventory",
|
|
);
|
|
validateNativeInventory(
|
|
JSON.parse(
|
|
await readFile(path.join(pack, "native-components.json"), "utf8"),
|
|
),
|
|
);
|
|
for (const [name, expected] of Object.entries(PHP_ENGINE_LOCK.legalAssets)) {
|
|
await assertHash(
|
|
path.join(pack, name),
|
|
expected,
|
|
`Packed PHP legal asset ${name}`,
|
|
);
|
|
}
|
|
const loader = await readFile(path.join(pack, "php-loader.mjs"), "utf8");
|
|
if (
|
|
!loader.startsWith(`${PATCHED_LOADER}\n`) ||
|
|
loader.includes(LOADER_IMPORT)
|
|
) {
|
|
throw new Error("Packed PHP loader patch is invalid.");
|
|
}
|
|
const metadata = JSON.parse(
|
|
await readFile(path.join(pack, "engine-metadata.json"), "utf8"),
|
|
);
|
|
const expected = await expectedMetadata(projectRoot, pack);
|
|
if (JSON.stringify(metadata) !== JSON.stringify(expected)) {
|
|
throw new Error("PHP engine metadata does not match the pinned pack.");
|
|
}
|
|
await smokePhp(pack, projectRoot);
|
|
return metadata;
|
|
}
|
|
|
|
export async function buildPhpPack({
|
|
projectRoot = root,
|
|
output = path.join(root, ".engine-build", "php"),
|
|
force = false,
|
|
} = {}) {
|
|
const sources = await verifySources(projectRoot);
|
|
const target = path.resolve(output);
|
|
const existing = await lstat(target).catch(() => null);
|
|
if (existing && !force) {
|
|
throw new Error(`${target} exists; pass --force to replace it.`);
|
|
}
|
|
if (existing && (existing.isSymbolicLink() || !existing.isDirectory())) {
|
|
throw new Error("PHP pack output must be a real directory when replaced.");
|
|
}
|
|
|
|
await mkdir(path.dirname(target), { recursive: true });
|
|
const stage = await mkdtemp(path.join(path.dirname(target), ".php-pack-"));
|
|
try {
|
|
await copyFile(
|
|
sources.wasm,
|
|
path.join(stage, "php.wasm"),
|
|
fsConstants.COPYFILE_EXCL,
|
|
);
|
|
await copyFile(
|
|
sources.nativeInventory,
|
|
path.join(stage, "native-components.json"),
|
|
fsConstants.COPYFILE_EXCL,
|
|
);
|
|
for (const name of Object.keys(PHP_ENGINE_LOCK.legalAssets)) {
|
|
await copyFile(
|
|
sources.legalFiles[name],
|
|
path.join(stage, name),
|
|
fsConstants.COPYFILE_EXCL,
|
|
);
|
|
}
|
|
const loaderSource = await readFile(sources.loader, "utf8");
|
|
await writeFile(
|
|
path.join(stage, "php-loader.mjs"),
|
|
patchLoader(loaderSource),
|
|
{ flag: "wx", mode: 0o644 },
|
|
);
|
|
const metadata = await expectedMetadata(projectRoot, stage);
|
|
await writeFile(
|
|
path.join(stage, "engine-metadata.json"),
|
|
`${JSON.stringify(metadata, null, 2)}\n`,
|
|
{ flag: "wx", mode: 0o644 },
|
|
);
|
|
const checksumLines = [];
|
|
for (const name of CHECKSUM_FILES) {
|
|
checksumLines.push(
|
|
`${await sha256File(path.join(stage, name))} ${name}`,
|
|
);
|
|
}
|
|
await writeFile(
|
|
path.join(stage, "SHA256SUMS"),
|
|
`${checksumLines.join("\n")}\n`,
|
|
{ flag: "wx", mode: 0o644 },
|
|
);
|
|
await verifyPhpPack(stage, projectRoot);
|
|
if (existing) await rm(target, { recursive: true });
|
|
await rename(stage, target);
|
|
} catch (error) {
|
|
await rm(stage, { recursive: true, force: true });
|
|
throw error;
|
|
}
|
|
return {
|
|
output: target,
|
|
metadata: await verifyPhpPack(target, projectRoot),
|
|
};
|
|
}
|
|
|
|
export async function installPhpPack(source, projectRoot = root) {
|
|
const sourcePath = await assertRealDirectory(source, "PHP staged pack");
|
|
const target = path.join(projectRoot, "public", "engines", "php");
|
|
if (sourcePath === (await realpath(target).catch(() => ""))) {
|
|
throw new Error("PHP staged pack and installation target must differ.");
|
|
}
|
|
const metadata = await verifyPhpPack(sourcePath, projectRoot);
|
|
await mkdir(path.dirname(target), { recursive: true });
|
|
const stage = await mkdtemp(path.join(path.dirname(target), ".php-install-"));
|
|
let backup;
|
|
try {
|
|
for (const name of PACK_FILES) {
|
|
await copyFile(
|
|
path.join(sourcePath, name),
|
|
path.join(stage, name),
|
|
fsConstants.COPYFILE_EXCL,
|
|
);
|
|
}
|
|
await verifyPhpPack(stage, projectRoot);
|
|
const existing = await lstat(target).catch(() => null);
|
|
if (existing) {
|
|
if (existing.isSymbolicLink() || !existing.isDirectory()) {
|
|
throw new Error("Installed PHP pack target must be a real directory.");
|
|
}
|
|
backup = `${target}.backup-${process.pid}-${Date.now()}`;
|
|
await rename(target, backup);
|
|
}
|
|
await rename(stage, target);
|
|
if (backup) await rm(backup, { recursive: true });
|
|
} catch (error) {
|
|
await rm(stage, { recursive: true, force: true });
|
|
if (backup && !(await lstat(target).catch(() => null))) {
|
|
await rename(backup, target);
|
|
}
|
|
throw error;
|
|
}
|
|
return { output: target, metadata };
|
|
}
|
|
|
|
function parseArguments(arguments_) {
|
|
let output = path.join(root, ".engine-build", "php");
|
|
let force = false;
|
|
for (let index = 0; index < arguments_.length; index += 1) {
|
|
const argument = arguments_[index];
|
|
if (argument === "--force") {
|
|
if (force) throw new Error("--force was provided more than once.");
|
|
force = true;
|
|
} else if (argument === "--output") {
|
|
const value = arguments_[index + 1];
|
|
if (!value || value.startsWith("--")) {
|
|
throw new Error("--output requires a directory.");
|
|
}
|
|
output = path.resolve(root, value);
|
|
index += 1;
|
|
} else {
|
|
throw new Error(`Unknown PHP engine-pack argument: ${argument}`);
|
|
}
|
|
}
|
|
return { output, force };
|
|
}
|
|
|
|
async function main() {
|
|
const result = await buildPhpPack(parseArguments(process.argv.slice(2)));
|
|
console.log(
|
|
`Built verified ${result.metadata.engineVersion} pack at ${result.output}.`,
|
|
);
|
|
}
|
|
|
|
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
await main();
|
|
}
|