diff --git a/src/extensions/artifacts/native/tools/extension-artifact-packager.mjs b/src/extensions/artifacts/native/tools/extension-artifact-packager.mjs index 2367ce435..4a666efad 100755 --- a/src/extensions/artifacts/native/tools/extension-artifact-packager.mjs +++ b/src/extensions/artifacts/native/tools/extension-artifact-packager.mjs @@ -320,6 +320,75 @@ export function selectCatalogExtensions(extensions, qualificationSqlNames = []) return selected; } +export function selectMobileStaticModuleSqlNames(rows, sqlNames) { + if (!Array.isArray(rows)) { + fail('mobile static catalog rows must be an array'); + } + const selected = new Set(sortedDeduped(sqlNames)); + return sortedDeduped( + rows + .filter((row) => { + if (row === null || Array.isArray(row) || typeof row !== 'object') { + fail('mobile static catalog rows must be objects'); + } + return selected.has(row.sqlName) + && row.mobilePrebuilt === true + && row.mobileStaticRequired === true + && typeof row.stem === 'string' + && row.stem.length > 0; + }) + .map((row) => row.sqlName), + ); +} + +function mobileStaticSpecTable(text, label) { + const lines = text.replace(/\r\n/g, '\n').replace(/\n+$/u, '').split('\n'); + if (lines.length < 2 || !lines[0].startsWith('# @generated by ')) { + fail(`${label} mobile static registry is missing its generated marker`); + } + const header = lines[1]; + const columns = header.split('\t'); + if (columns[0] !== 'sql-name' || columns.length !== 16) { + fail(`${label} mobile static registry has an invalid header`); + } + const rows = new Map(); + for (const [offset, line] of lines.slice(2).entries()) { + if (line.length === 0) continue; + const fields = line.split('\t'); + if ( + fields.length < 4 + || fields.length > columns.length + || fields.slice(0, 4).some((field) => field.length === 0) + ) { + fail(`${label} mobile static registry row ${offset + 3} is invalid`); + } + if (rows.has(fields[0])) { + fail(`${label} mobile static registry repeats SQL name '${fields[0]}'`); + } + rows.set(fields[0], line); + } + return { generatedMarker: lines[0], header, rows }; +} + +export function mergeMobileStaticSpecRegistries(publicText, qualificationText) { + const publicTable = mobileStaticSpecTable(publicText, 'public'); + const qualificationTable = mobileStaticSpecTable(qualificationText, 'qualification'); + if (publicTable.header !== qualificationTable.header) { + fail('public and qualification mobile static registry headers disagree'); + } + const rows = new Map(publicTable.rows); + for (const [sqlName, row] of qualificationTable.rows) { + if (rows.has(sqlName)) { + fail(`qualification mobile static registry overlaps public SQL name '${sqlName}'`); + } + rows.set(sqlName, row); + } + const body = [...rows] + .sort(([left], [right]) => compareText(left, right)) + .map(([, row]) => row); + return [publicTable.generatedMarker, publicTable.header, ...body, ''].join('\n'); +} + export async function catalogRows({ qualificationSqlNames = [], qualificationTarget = undefined, @@ -432,6 +501,32 @@ async function listCatalog(args = []) { } } +async function mobileStaticModuleSqlNames(args = []) { + if (args.length !== 4 || args[0] !== '--target' || args[2] !== '--sql-names') { + fail('mobile-static-module-sql-names requires --target --sql-names '); + } + const target = args[1]; + if (!MOBILE_NATIVE_TARGETS.has(target)) { + fail(`mobile-static-module-sql-names requires a mobile native target, got '${target}'`); + } + const qualificationSqlNames = qualificationCandidateSqlNamesForTarget(target, { family: 'native' }); + const rows = await catalogRows({ qualificationSqlNames, qualificationTarget: target }); + console.log(selectMobileStaticModuleSqlNames(rows, splitCsv(args[3])).join(',')); +} + +async function mergeMobileStaticSpecs(args = []) { + if (args.length !== 3) { + fail('merge-mobile-static-specs requires '); + } + const [publicPath, qualificationPath, outputPath] = args; + const merged = mergeMobileStaticSpecRegistries( + await readText(publicPath), + await readText(qualificationPath), + ); + await fs.mkdir(path.dirname(path.resolve(outputPath)), { recursive: true }); + await fs.writeFile(path.resolve(outputPath), merged); +} + async function releasePackageByProduct(product) { const releaseConfig = await readJson(RELEASE_CONFIG_PATH); const packages = releaseConfig.packages ?? {}; @@ -1279,6 +1374,12 @@ async function main() { case 'selected-sql-names': await selectedSqlNames(args[0] ?? ''); break; + case 'mobile-static-module-sql-names': + await mobileStaticModuleSqlNames(args); + break; + case 'merge-mobile-static-specs': + await mergeMobileStaticSpecs(args); + break; case 'product-version': await productVersion(args[0] ?? ''); break; @@ -1286,7 +1387,7 @@ async function main() { await createArtifact(args); break; default: - fail('usage: extension-artifact-packager.mjs [options]'); + fail('usage: extension-artifact-packager.mjs [options]'); } } diff --git a/src/extensions/artifacts/native/tools/package-release-assets.sh b/src/extensions/artifacts/native/tools/package-release-assets.sh index c7326d964..f5394acb0 100755 --- a/src/extensions/artifacts/native/tools/package-release-assets.sh +++ b/src/extensions/artifacts/native/tools/package-release-assets.sh @@ -87,10 +87,6 @@ build_sql_names="$({ printf '%s\n' "$selected_sql_names" | tr ',' '\n' printf '%s\n' "$qualification_sql_names" | tr ',' '\n' } | sed '/^$/d' | LC_ALL=C sort -u | paste -sd ',' -)" -if [ -n "$qualification_sql_names" ]; then - export OLIPHAUNT_MOBILE_STATIC_SPECS_TSV="$root/src/extensions/generated/mobile/qualification-static-extensions.tsv" -fi - version="${OLIPHAUNT_EXTENSION_RELEASE_VERSION:-$(bun "$packager" product-version liboliphaunt-native)}" native_runtime_version="$(tr -d '[:space:]' < "$root/src/runtimes/liboliphaunt/native/VERSION")" if [ "$qualification_only" = "1" ]; then @@ -136,44 +132,37 @@ fi rm -rf "$stage_root" mkdir -p "$out_dir" "$stage_root" -csv_join() { - paste -sd ',' - -} - catalog_rows() { awk -F '\t' 'NR > 1 { print }' "$catalog_file" } mobile_module_extensions_csv() { - if [ "$qualification_only" = "1" ]; then - local extension - IFS=',' read -r -a qualification_extensions <<<"$selected_sql_names" - for extension in "${qualification_extensions[@]}"; do - [ -n "$extension" ] || continue - oliphaunt_mobile_static_extension_spec "$extension" >/dev/null || - fail "deferred candidate $extension has no generated mobile qualification spec" - done - printf '%s\n' "$selected_sql_names" - return 0 - fi - { - catalog_rows | awk -F '\t' -v selected="$selected_sql_names" ' - function selected_match(sql_name, selected, parts, count, i) { - if (selected == "") { - return 1 - } - count = split(selected, parts, ",") - for (i = 1; i <= count; i++) { - if (parts[i] == sql_name) { - return 1 - } - } - return 0 - } - $8 == "yes" && $9 == "yes" && $4 != "-" && selected_match($1, selected) { print $1 } - ' - printf '%s\n' "$qualification_sql_names" | tr ',' '\n' | sed '/^$/d' - } | LC_ALL=C sort -u | csv_join + local extension module_extensions + local -a selected_extensions + module_extensions="$(bun "$packager" mobile-static-module-sql-names \ + --target "$target_id" \ + --sql-names "$build_sql_names")" + IFS=',' read -r -a selected_extensions <<<"$module_extensions" + for extension in "${selected_extensions[@]}"; do + [ -n "$extension" ] || continue + oliphaunt_mobile_static_extension_spec "$extension" >/dev/null || + fail "selected mobile extension $extension has no generated static build spec" + done + printf '%s\n' "$module_extensions" +} + +prepare_mobile_static_specs() { + [ "$qualification_only" = "0" ] || return 0 + [ -n "$qualification_sql_names" ] || return 0 + local public_specs qualification_specs merged_specs + public_specs="$root/src/extensions/generated/mobile/static-extensions.tsv" + qualification_specs="$root/src/extensions/generated/mobile/qualification-static-extensions.tsv" + merged_specs="$stage_root/mobile-static-extensions.tsv" + bun "$packager" merge-mobile-static-specs \ + "$public_specs" \ + "$qualification_specs" \ + "$merged_specs" + export OLIPHAUNT_MOBILE_STATIC_SPECS_TSV="$merged_specs" } selected_sql_name_matches() { @@ -664,6 +653,10 @@ package_ios_target() { require_dir "$source_runtime" "mobile host extension runtime" runtime="$(prepare_extension_release_runtime "$source_runtime")" tools/dev/bun.sh tools/release/platform-binary-contract.mjs --target macos-arm64 --root "$runtime" + if [ "$qualification_only" = "1" ] && [ -z "$mobile_extensions" ]; then + echo "qualified deferred native extension candidate(s) $qualification_sql_names for $target_id" + return 0 + fi ios_sim_root="$mobile_extension_work_root/$target_id/ios-simulator" ios_device_root="$mobile_extension_work_root/$target_id/ios-device" macos_archive_root="$mobile_extension_work_root/$target_id/macos-extension-archives" @@ -679,7 +672,6 @@ package_ios_target() { if [ "$qualification_only" = "1" ]; then return 0 fi - local sql_name pg_major creates_extension stem dependencies shared_preload desktop_prebuilt mobile_prebuilt mobile_static_required mobile_static_targets data_files artifact_policy runtime_artifact ios_artifact static_prefix registration_artifact dependency dependency_xcframework dependency_artifact while IFS=$'\t' read -r sql_name pg_major creates_extension stem dependencies shared_preload desktop_prebuilt mobile_prebuilt mobile_static_required mobile_static_targets data_files artifact_policy; do [ -n "$sql_name" ] || continue @@ -775,6 +767,10 @@ package_android_target() { require_dir "$source_runtime" "mobile host extension runtime" runtime="$(prepare_extension_release_runtime "$source_runtime")" tools/dev/bun.sh tools/release/platform-binary-contract.mjs --target linux-x64-gnu --root "$runtime" + if [ "$qualification_only" = "1" ] && [ -z "$mobile_extensions" ]; then + echo "qualified deferred native extension candidate(s) $qualification_sql_names for $target_id" + return 0 + fi case "$target_id" in android-arm64-v8a) android_root="$mobile_extension_work_root/$target_id/android-arm64" @@ -793,7 +789,6 @@ package_android_target() { if [ "$qualification_only" = "1" ]; then return 0 fi - local sql_name pg_major creates_extension stem dependencies shared_preload desktop_prebuilt mobile_prebuilt mobile_static_required mobile_static_targets data_files artifact_policy runtime_artifact android_archive static_prefix while IFS=$'\t' read -r sql_name pg_major creates_extension stem dependencies shared_preload desktop_prebuilt mobile_prebuilt mobile_static_required mobile_static_targets data_files artifact_policy; do [ -n "$sql_name" ] || continue @@ -829,6 +824,7 @@ package_android_target() { fetch_extension_source_assets echo "==> Reading exact extension catalog" bun "$packager" list-catalog --qualification-target "$target_id" >"$catalog_file" +prepare_mobile_static_specs if [ "$qualification_only" = "0" ]; then write_indexes fi diff --git a/src/extensions/catalog/extensions.promoted.toml b/src/extensions/catalog/extensions.promoted.toml index a4befb017..5983c8b73 100644 --- a/src/extensions/catalog/extensions.promoted.toml +++ b/src/extensions/catalog/extensions.promoted.toml @@ -122,6 +122,12 @@ stable = true id = "pgcrypto" stable = true +[[extensions]] +id = "pgmq" +build = true +stable = false +blocker = "PGMQ 1.12.0 is pinned for native desktop, mobile, and WASIX qualification, but exact-commit package, lifecycle, restart, and dump/restore evidence has not yet passed; keep it out of public catalogs until that CI evidence exists." + [[extensions]] id = "pgtap" stable = true diff --git a/src/extensions/catalog/extensions.smoke.toml b/src/extensions/catalog/extensions.smoke.toml index 0406c8ece..6fdb29fd5 100644 --- a/src/extensions/catalog/extensions.smoke.toml +++ b/src/extensions/catalog/extensions.smoke.toml @@ -210,6 +210,13 @@ server = "passed" restart = "passed" dump-restore = "passed" +[[extensions]] +id = "pgmq" +direct = "not-run" +server = "not-run" +restart = "not-run" +dump-restore = "not-run" + [[extensions]] id = "pgtap" direct = "passed" diff --git a/src/extensions/catalog/extensions.source.json b/src/extensions/catalog/extensions.source.json index 83cf220a3..3a677a29c 100644 --- a/src/extensions/catalog/extensions.source.json +++ b/src/extensions/catalog/extensions.source.json @@ -1147,6 +1147,44 @@ "native-module-file": "pgcrypto.so", "notes": [] }, + { + "id": "pgmq", + "sql-name": "pgmq", + "rust-constant": "PGMQ", + "display-name": "PGMQ", + "source-kind": "oliphaunt-other-extension", + "upstream-import-name": "pgmq", + "upstream-import-path": "pgmq-extension", + "package-export": "./pgmq", + "tags": [ + "postgres extension", + "qualification candidate" + ], + "control-file": "target/oliphaunt-sources/checkouts/pgmq/pgmq-extension/pgmq.control", + "control": { + "module-pathname": "$libdir/pgmq", + "requires": [], + "relocatable": "false", + "schema": "pgmq" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pgmq", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "src/extensions/external/pgmq/tests/smoke.sql" + ], + "notes": [] + }, { "id": "pgtap", "sql-name": "pgtap", diff --git a/src/extensions/contrib/.release-semantic-inputs.json b/src/extensions/contrib/.release-semantic-inputs.json index 2cae8a548..fad864b37 100644 --- a/src/extensions/contrib/.release-semantic-inputs.json +++ b/src/extensions/contrib/.release-semantic-inputs.json @@ -130,11 +130,11 @@ "inputs": [ { "path": "src/extensions/artifacts/native/tools/extension-artifact-packager.mjs", - "sha256": "14abaf5b3828900fdc3c561aa7298c3e026623f14f71292b708c3cf3edaca3ae" + "sha256": "46dd355991a820ce8cd4cb9e0649a56f1d8d0d0ec7158b66686e4ca9706a09af" }, { "path": "src/extensions/artifacts/native/tools/package-release-assets.sh", - "sha256": "3bba3ae4486549321911a68c2e6e6ecd2d1c22640edc8253ec5079aaeb3fe657" + "sha256": "cff8891218fbfcf6d71665b28840d8da6b78cec7b114db312a2aa3d636e5d919" }, { "path": "src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs", @@ -377,5 +377,5 @@ ] } ], - "sha256": "419f620e12df219054374188e11b362780ba9e436f547348c20acd0bf7c29d24" + "sha256": "7c5e0abdb40e3fd08962f78f3345018d55fbfc0433df30eceaef8cf63dd01931" } diff --git a/src/extensions/evidence/matrix.toml b/src/extensions/evidence/matrix.toml index 3d5c7155a..fce75019e 100644 --- a/src/extensions/evidence/matrix.toml +++ b/src/extensions/evidence/matrix.toml @@ -15,6 +15,7 @@ source-digest-inputs = [ "src/extensions/external/pg_ivm/source.toml", "src/extensions/external/pg_textsearch/source.toml", "src/extensions/external/pg_uuidv7/source.toml", + "src/extensions/external/pgmq/source.toml", "src/extensions/external/pgtap/source.toml", "src/extensions/external/postgis/dependencies/geos/source.toml", "src/extensions/external/postgis/dependencies/json-c/source.toml", @@ -41,6 +42,14 @@ source-digest-inputs = [ "src/extensions/external/pg_textsearch/upstream-license-data.json", "src/extensions/external/pg_uuidv7/moon.yml", "src/extensions/external/pg_uuidv7/upstream-license-data.json", + "src/extensions/external/pgmq/moon.yml", + "src/extensions/external/pgmq/recipe.toml", + "src/extensions/external/pgmq/targets/mobile.toml", + "src/extensions/external/pgmq/targets/native.toml", + "src/extensions/external/pgmq/targets/wasix.toml", + "src/extensions/external/pgmq/tests/smoke.sql", + "src/extensions/external/pgmq/tests/upstream.toml", + "src/extensions/external/pgmq/upstream-license-data.json", "src/extensions/external/pgtap/moon.yml", "src/extensions/external/pgtap/recipe.toml", "src/extensions/external/pgtap/targets/native-static-registry.toml", diff --git a/src/extensions/external/pg_hashids/.release-semantic-inputs.json b/src/extensions/external/pg_hashids/.release-semantic-inputs.json index e10d6f048..184caf8a3 100644 --- a/src/extensions/external/pg_hashids/.release-semantic-inputs.json +++ b/src/extensions/external/pg_hashids/.release-semantic-inputs.json @@ -96,11 +96,11 @@ "inputs": [ { "path": "src/extensions/artifacts/native/tools/extension-artifact-packager.mjs", - "sha256": "14abaf5b3828900fdc3c561aa7298c3e026623f14f71292b708c3cf3edaca3ae" + "sha256": "46dd355991a820ce8cd4cb9e0649a56f1d8d0d0ec7158b66686e4ca9706a09af" }, { "path": "src/extensions/artifacts/native/tools/package-release-assets.sh", - "sha256": "3bba3ae4486549321911a68c2e6e6ecd2d1c22640edc8253ec5079aaeb3fe657" + "sha256": "cff8891218fbfcf6d71665b28840d8da6b78cec7b114db312a2aa3d636e5d919" }, { "path": "src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs", @@ -343,5 +343,5 @@ ] } ], - "sha256": "cd9da3b78ef5e8df01dacab7774205aef15270bf3d20dee1718b81bad78c13b1" + "sha256": "ee22c0b1c0f38105e73a248c2ebc61ec73350a92330dfaa911fb058820c9c773" } diff --git a/src/extensions/external/pg_ivm/.release-semantic-inputs.json b/src/extensions/external/pg_ivm/.release-semantic-inputs.json index 46bcd81a4..83e49c0eb 100644 --- a/src/extensions/external/pg_ivm/.release-semantic-inputs.json +++ b/src/extensions/external/pg_ivm/.release-semantic-inputs.json @@ -96,11 +96,11 @@ "inputs": [ { "path": "src/extensions/artifacts/native/tools/extension-artifact-packager.mjs", - "sha256": "14abaf5b3828900fdc3c561aa7298c3e026623f14f71292b708c3cf3edaca3ae" + "sha256": "46dd355991a820ce8cd4cb9e0649a56f1d8d0d0ec7158b66686e4ca9706a09af" }, { "path": "src/extensions/artifacts/native/tools/package-release-assets.sh", - "sha256": "3bba3ae4486549321911a68c2e6e6ecd2d1c22640edc8253ec5079aaeb3fe657" + "sha256": "cff8891218fbfcf6d71665b28840d8da6b78cec7b114db312a2aa3d636e5d919" }, { "path": "src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs", @@ -343,5 +343,5 @@ ] } ], - "sha256": "93852eada395596f863f6ff4bf11709c09758e8720e2f92ec0ae315a80ea07ef" + "sha256": "43adc9db90eba7525004528da16e81a939edccdcc121ac44810c0ce1180166d8" } diff --git a/src/extensions/external/pg_textsearch/.release-semantic-inputs.json b/src/extensions/external/pg_textsearch/.release-semantic-inputs.json index aeac4dcf2..7bcf29a7b 100644 --- a/src/extensions/external/pg_textsearch/.release-semantic-inputs.json +++ b/src/extensions/external/pg_textsearch/.release-semantic-inputs.json @@ -96,11 +96,11 @@ "inputs": [ { "path": "src/extensions/artifacts/native/tools/extension-artifact-packager.mjs", - "sha256": "14abaf5b3828900fdc3c561aa7298c3e026623f14f71292b708c3cf3edaca3ae" + "sha256": "46dd355991a820ce8cd4cb9e0649a56f1d8d0d0ec7158b66686e4ca9706a09af" }, { "path": "src/extensions/artifacts/native/tools/package-release-assets.sh", - "sha256": "3bba3ae4486549321911a68c2e6e6ecd2d1c22640edc8253ec5079aaeb3fe657" + "sha256": "cff8891218fbfcf6d71665b28840d8da6b78cec7b114db312a2aa3d636e5d919" }, { "path": "src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs", @@ -343,5 +343,5 @@ ] } ], - "sha256": "93af1bc0cc1039d7285d1bfdefeca1b1a959c89bb0c523490711b59601b3c427" + "sha256": "d1ed88749139aee8b8494655fc6eb4e3aef1aae0b36e109c623f1fe7b1b02921" } diff --git a/src/extensions/external/pg_uuidv7/.release-semantic-inputs.json b/src/extensions/external/pg_uuidv7/.release-semantic-inputs.json index 36aa011eb..0efd7bca7 100644 --- a/src/extensions/external/pg_uuidv7/.release-semantic-inputs.json +++ b/src/extensions/external/pg_uuidv7/.release-semantic-inputs.json @@ -96,11 +96,11 @@ "inputs": [ { "path": "src/extensions/artifacts/native/tools/extension-artifact-packager.mjs", - "sha256": "14abaf5b3828900fdc3c561aa7298c3e026623f14f71292b708c3cf3edaca3ae" + "sha256": "46dd355991a820ce8cd4cb9e0649a56f1d8d0d0ec7158b66686e4ca9706a09af" }, { "path": "src/extensions/artifacts/native/tools/package-release-assets.sh", - "sha256": "3bba3ae4486549321911a68c2e6e6ecd2d1c22640edc8253ec5079aaeb3fe657" + "sha256": "cff8891218fbfcf6d71665b28840d8da6b78cec7b114db312a2aa3d636e5d919" }, { "path": "src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs", @@ -343,5 +343,5 @@ ] } ], - "sha256": "582f1d4797443f6cda96d94f8f3a6c1bf66bcd4778d3f8518b937233aab11e2e" + "sha256": "fb145ed3681af9e2333379d299637b895473e15ead1908013ecd4f1fdd20b930" } diff --git a/src/extensions/external/pgmq/moon.yml b/src/extensions/external/pgmq/moon.yml new file mode 100644 index 000000000..6d83eeb27 --- /dev/null +++ b/src/extensions/external/pgmq/moon.yml @@ -0,0 +1,27 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "extension-pgmq" +language: "unknown" +layer: "configuration" +stack: "systems" +tags: ["extensions", "external", "qualification-candidate"] +dependsOn: + - "extension-runtime-contract" + - id: "liboliphaunt-native" + scope: "build" + - id: "liboliphaunt-wasix" + scope: "build" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/external/pgmq" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/external/pgmq/**/*" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/external/pgmq/publication-blocker.toml b/src/extensions/external/pgmq/publication-blocker.toml new file mode 100644 index 000000000..29c006d25 --- /dev/null +++ b/src/extensions/external/pgmq/publication-blocker.toml @@ -0,0 +1,7 @@ +schema = "oliphaunt-extension-publication-blocker-v2" +extension_id = "pgmq" +product = "oliphaunt-extension-pgmq" +reason = "PGMQ 1.12.0 is pinned for native desktop, mobile, and WASIX qualification, but exact-commit package, lifecycle, restart, and dump/restore evidence has not yet passed; keep it out of public catalogs until that CI evidence exists." +resolutions = ["defer-product-from-publication-catalog"] +sql_name = "pgmq" +status = "deferred" diff --git a/src/extensions/external/pgmq/recipe.toml b/src/extensions/external/pgmq/recipe.toml new file mode 100644 index 000000000..998811fbf --- /dev/null +++ b/src/extensions/external/pgmq/recipe.toml @@ -0,0 +1,43 @@ +schema = "oliphaunt-extension-recipe-v1" +sql_name = "pgmq" +display_name = "PGMQ" +kind = "external-simple-pgxs" +license = "PostgreSQL" +source = "pgmq" +postgres_majors = [18] + +[lifecycle] +creates_extension = true +requires = [] +implicit_sql_dependencies = [] +load_sql = [] +post_create_sql = [] +shared_preload_libraries = [] +restart_required = false +background_workers = false +shared_memory = false +session_load_required = false +needs_superuser = false +trusted = false + +[artifacts] +control_files = ["share/postgresql/extension/pgmq.control"] +sql_globs = ["share/postgresql/extension/pgmq--*.sql"] +native_modules = [] +native_dependency_modules = [] +data_files = [] +headers = [] +licenses = ["share/licenses/pgmq/LICENSE"] + +[support.native] +direct = "candidate" +broker = "candidate" +server = "candidate" + +[support.wasix] +direct = "candidate" +server = "candidate" + +[support.mobile] +ios = "candidate" +android = "candidate" diff --git a/src/extensions/external/pgmq/source.toml b/src/extensions/external/pgmq/source.toml new file mode 100644 index 000000000..264482a64 --- /dev/null +++ b/src/extensions/external/pgmq/source.toml @@ -0,0 +1,9 @@ +name = "pgmq" +url = "https://github.com/pgmq/pgmq.git" +branch = "pinned" +commit = "08ace4087dbf00e51704c5a3d9df2e15fd566127" + +[extension-control] +sql-name = "pgmq" +source-path = "pgmq-extension/pgmq.control" +default-version = "1.12.0" diff --git a/src/extensions/external/pgmq/targets/artifacts.toml b/src/extensions/external/pgmq/targets/artifacts.toml new file mode 100644 index 000000000..1604f4b1f --- /dev/null +++ b/src/extensions/external/pgmq/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "qualification-required" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "qualification-required" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "qualification-required" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/external/pgmq/targets/mobile.toml b/src/extensions/external/pgmq/targets/mobile.toml new file mode 100644 index 000000000..a9e01db1f --- /dev/null +++ b/src/extensions/external/pgmq/targets/mobile.toml @@ -0,0 +1,7 @@ +schema = "oliphaunt-extension-target-v1" +artifact_family = "sql-only" +status = "candidate" +build_kind = "pgxs-sql-only" +dependencies = [] +module_files = [] +data_files = [] diff --git a/src/extensions/external/pgmq/targets/native.toml b/src/extensions/external/pgmq/targets/native.toml new file mode 100644 index 000000000..a9e01db1f --- /dev/null +++ b/src/extensions/external/pgmq/targets/native.toml @@ -0,0 +1,7 @@ +schema = "oliphaunt-extension-target-v1" +artifact_family = "sql-only" +status = "candidate" +build_kind = "pgxs-sql-only" +dependencies = [] +module_files = [] +data_files = [] diff --git a/src/extensions/external/pgmq/targets/wasix.toml b/src/extensions/external/pgmq/targets/wasix.toml new file mode 100644 index 000000000..a9e01db1f --- /dev/null +++ b/src/extensions/external/pgmq/targets/wasix.toml @@ -0,0 +1,7 @@ +schema = "oliphaunt-extension-target-v1" +artifact_family = "sql-only" +status = "candidate" +build_kind = "pgxs-sql-only" +dependencies = [] +module_files = [] +data_files = [] diff --git a/src/extensions/external/pgmq/tests/smoke.sql b/src/extensions/external/pgmq/tests/smoke.sql new file mode 100644 index 000000000..d47fcba87 --- /dev/null +++ b/src/extensions/external/pgmq/tests/smoke.sql @@ -0,0 +1,30 @@ +-- oliphaunt-statement +CREATE EXTENSION IF NOT EXISTS pgmq; + +-- oliphaunt-statement +SELECT pgmq.create('oliphaunt_smoke'); + +-- oliphaunt-statement +SELECT pgmq.send('oliphaunt_smoke', '{"source":"oliphaunt","target":"pgmq"}'::jsonb); + +-- oliphaunt-statement +DO $oliphaunt$ +DECLARE + received pgmq.message_record; +BEGIN + SELECT * + INTO received + FROM pgmq.read('oliphaunt_smoke', 30, 1); + + IF received.message IS DISTINCT FROM '{"source":"oliphaunt","target":"pgmq"}'::jsonb THEN + RAISE EXCEPTION 'PGMQ smoke received %, expected the queued Oliphaunt payload', received.message; + END IF; + + IF NOT pgmq.archive('oliphaunt_smoke', received.msg_id) THEN + RAISE EXCEPTION 'PGMQ smoke could not archive message %', received.msg_id; + END IF; +END +$oliphaunt$; + +-- oliphaunt-statement +SELECT pgmq.drop_queue('oliphaunt_smoke'); diff --git a/src/extensions/external/pgmq/tests/upstream.toml b/src/extensions/external/pgmq/tests/upstream.toml new file mode 100644 index 000000000..48df6c778 --- /dev/null +++ b/src/extensions/external/pgmq/tests/upstream.toml @@ -0,0 +1,6 @@ +schema = "oliphaunt-extension-upstream-tests-v1" +runner = "pgxs-installcheck" +status = "candidate" +reason = "PGMQ's upstream regression and isolation suites remain qualification inputs; they must pass in the exact native and WASIX candidate jobs before publication." +included_suites = ["pgmq"] +excluded_suites = [] diff --git a/src/extensions/external/pgmq/upstream-license-data.json b/src/extensions/external/pgmq/upstream-license-data.json new file mode 100644 index 000000000..918287b9d --- /dev/null +++ b/src/extensions/external/pgmq/upstream-license-data.json @@ -0,0 +1,30 @@ +{ + "schema": "oliphaunt-extension-upstream-license-data-v1", + "sources": [ + { + "id": "pgmq", + "manifest": "src/extensions/external/pgmq/source.toml", + "kind": "git", + "url": "https://github.com/pgmq/pgmq.git", + "branch": "pinned", + "commit": "08ace4087dbf00e51704c5a3d9df2e15fd566127" + } + ], + "extension": { + "sql_name": "pgmq", + "files": [ + { + "source": "pgmq", + "path": "LICENSE", + "destination": "share/licenses/pgmq/LICENSE", + "role": "license", + "spdx": "PostgreSQL", + "license_url": "https://github.com/pgmq/pgmq/blob/08ace4087dbf00e51704c5a3d9df2e15fd566127/LICENSE", + "sha256": "a3ce61e0031bdcad1024b1bf50ded2359602160f00e0cac8e509752273df6179" + } + ] + }, + "blobs": { + "a3ce61e0031bdcad1024b1bf50ded2359602160f00e0cac8e509752273df6179": "VGhlIFBvc3RncmVTUUwgTGljZW5zZQoKQ29weXJpZ2h0IChjKSAyMDIzLCBUZW1ibwoKUGVybWlzc2lvbiB0byB1c2UsIGNvcHksIG1vZGlmeSwgYW5kIGRpc3RyaWJ1dGUgdGhpcyBzb2Z0d2FyZSBhbmQgaXRzIGRvY3VtZW50YXRpb24gZm9yIGFueSBwdXJwb3NlLCB3aXRob3V0IGZlZSwgYW5kIHdpdGhvdXQgYSB3cml0dGVuIGFncmVlbWVudCBpcyBoZXJlYnkgZ3JhbnRlZCwgcHJvdmlkZWQgdGhhdCB0aGUgYWJvdmUgY29weXJpZ2h0IG5vdGljZSBhbmQgdGhpcyBwYXJhZ3JhcGggYW5kIHRoZSBmb2xsb3dpbmcgdHdvIHBhcmFncmFwaHMgYXBwZWFyIGluIGFsbCBjb3BpZXMuCgpJTiBOTyBFVkVOVCBTSEFMTCBURU1CTyBCRSBMSUFCTEUgVE8gQU5ZIFBBUlRZIEZPUiBESVJFQ1QsIElORElSRUNULCBTUEVDSUFMLCBJTkNJREVOVEFMLCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMsIElOQ0xVRElORyBMT1NUIFBST0ZJVFMsIEFSSVNJTkcgT1VUIE9GIFRIRSBVU0UgT0YgVEhJUyBTT0ZUV0FSRSBBTkQgSVRTIERPQ1VNRU5UQVRJT04sIEVWRU4gSUYgVEVNQk8gSEFTIEJFRU4gQURWSVNFRCBPRiBUSEUgUE9TU0lCSUxJVFkgT0YgU1VDSCBEQU1BR0UuCgpURU1CTyBTUEVDSUZJQ0FMTFkgRElTQ0xBSU1TIEFOWSBXQVJSQU5USUVTLCBJTkNMVURJTkcsIEJVVCBOT1QgTElNSVRFRCBUTywgVEhFIElNUExJRUQgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFkgQU5EIEZJVE5FU1MgRk9SIEEgUEFSVElDVUxBUiBQVVJQT1NFLiBUSEUgU09GVFdBUkUgUFJPVklERUQgSEVSRVVOREVSIElTIE9OIEFOICJBUyBJUyIgQkFTSVMsIEFORCBURU1CTyBIQVMgTk8gT0JMSUdBVElPTlMgVE8gUFJPVklERSBNQUlOVEVOQU5DRSwgU1VQUE9SVCwgVVBEQVRFUywgRU5IQU5DRU1FTlRTLCBPUiBNT0RJRklDQVRJT05TLgo=" + } +} diff --git a/src/extensions/external/pgtap/.release-semantic-inputs.json b/src/extensions/external/pgtap/.release-semantic-inputs.json index 8e6eb89a2..2b2c1c48a 100644 --- a/src/extensions/external/pgtap/.release-semantic-inputs.json +++ b/src/extensions/external/pgtap/.release-semantic-inputs.json @@ -96,11 +96,11 @@ "inputs": [ { "path": "src/extensions/artifacts/native/tools/extension-artifact-packager.mjs", - "sha256": "14abaf5b3828900fdc3c561aa7298c3e026623f14f71292b708c3cf3edaca3ae" + "sha256": "46dd355991a820ce8cd4cb9e0649a56f1d8d0d0ec7158b66686e4ca9706a09af" }, { "path": "src/extensions/artifacts/native/tools/package-release-assets.sh", - "sha256": "3bba3ae4486549321911a68c2e6e6ecd2d1c22640edc8253ec5079aaeb3fe657" + "sha256": "cff8891218fbfcf6d71665b28840d8da6b78cec7b114db312a2aa3d636e5d919" }, { "path": "src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs", @@ -343,5 +343,5 @@ ] } ], - "sha256": "5bd705d032c08abd0cee58e21f15836ee67f512822017e61ce4bea347a8c6031" + "sha256": "ae7470245259121966661334c2574ae13fb52d833ce735b001dd01fbf1a16a82" } diff --git a/src/extensions/external/postgis/.release-semantic-inputs.json b/src/extensions/external/postgis/.release-semantic-inputs.json index fd32cd81e..cf40984ab 100644 --- a/src/extensions/external/postgis/.release-semantic-inputs.json +++ b/src/extensions/external/postgis/.release-semantic-inputs.json @@ -143,11 +143,11 @@ "inputs": [ { "path": "src/extensions/artifacts/native/tools/extension-artifact-packager.mjs", - "sha256": "14abaf5b3828900fdc3c561aa7298c3e026623f14f71292b708c3cf3edaca3ae" + "sha256": "46dd355991a820ce8cd4cb9e0649a56f1d8d0d0ec7158b66686e4ca9706a09af" }, { "path": "src/extensions/artifacts/native/tools/package-release-assets.sh", - "sha256": "3bba3ae4486549321911a68c2e6e6ecd2d1c22640edc8253ec5079aaeb3fe657" + "sha256": "cff8891218fbfcf6d71665b28840d8da6b78cec7b114db312a2aa3d636e5d919" }, { "path": "src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs", @@ -390,5 +390,5 @@ ] } ], - "sha256": "a8d8102915899d7308025b7c9fd5401f64e18c347d2d53aed9c083c7b001c2a4" + "sha256": "ea7d5b6163b065cef4263171d771d5eb1460bc720c0472b1771d5a58a00c1890" } diff --git a/src/extensions/external/vector/.release-semantic-inputs.json b/src/extensions/external/vector/.release-semantic-inputs.json index baf87a34a..97c8cdb41 100644 --- a/src/extensions/external/vector/.release-semantic-inputs.json +++ b/src/extensions/external/vector/.release-semantic-inputs.json @@ -96,11 +96,11 @@ "inputs": [ { "path": "src/extensions/artifacts/native/tools/extension-artifact-packager.mjs", - "sha256": "14abaf5b3828900fdc3c561aa7298c3e026623f14f71292b708c3cf3edaca3ae" + "sha256": "46dd355991a820ce8cd4cb9e0649a56f1d8d0d0ec7158b66686e4ca9706a09af" }, { "path": "src/extensions/artifacts/native/tools/package-release-assets.sh", - "sha256": "3bba3ae4486549321911a68c2e6e6ecd2d1c22640edc8253ec5079aaeb3fe657" + "sha256": "cff8891218fbfcf6d71665b28840d8da6b78cec7b114db312a2aa3d636e5d919" }, { "path": "src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs", @@ -343,5 +343,5 @@ ] } ], - "sha256": "f97b62f53ca3ddf959883c4f057a4a077b08a9f0529169768bc426468a8b5d27" + "sha256": "d1d3003d216d892401b1029d9bfd6229f26dd73a047cd205c2b758b0571003ee" } diff --git a/src/extensions/generated/docs/extension-evidence.json b/src/extensions/generated/docs/extension-evidence.json index 80431e38d..3ed78129a 100644 --- a/src/extensions/generated/docs/extension-evidence.json +++ b/src/extensions/generated/docs/extension-evidence.json @@ -1347,7 +1347,7 @@ "current-source-status": "requires-exact-candidate-ci", "kind": "exact-sha-ci" }, - "source-digest": "sha256:d30cde83f40642f68c2dd7f2b1fe1bf1a9ca05122a7a6232fca42d624a06fed2", + "source-digest": "sha256:acdd1d9a53679f99619bdcf31729cf137eb45ece987422ff97d8e115cd880b1d", "source-digest-inputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", @@ -1364,6 +1364,7 @@ "src/extensions/external/pg_ivm/source.toml", "src/extensions/external/pg_textsearch/source.toml", "src/extensions/external/pg_uuidv7/source.toml", + "src/extensions/external/pgmq/source.toml", "src/extensions/external/pgtap/source.toml", "src/extensions/external/postgis/dependencies/geos/source.toml", "src/extensions/external/postgis/dependencies/json-c/source.toml", @@ -1390,6 +1391,14 @@ "src/extensions/external/pg_textsearch/upstream-license-data.json", "src/extensions/external/pg_uuidv7/moon.yml", "src/extensions/external/pg_uuidv7/upstream-license-data.json", + "src/extensions/external/pgmq/moon.yml", + "src/extensions/external/pgmq/recipe.toml", + "src/extensions/external/pgmq/targets/mobile.toml", + "src/extensions/external/pgmq/targets/native.toml", + "src/extensions/external/pgmq/targets/wasix.toml", + "src/extensions/external/pgmq/tests/smoke.sql", + "src/extensions/external/pgmq/tests/upstream.toml", + "src/extensions/external/pgmq/upstream-license-data.json", "src/extensions/external/pgtap/moon.yml", "src/extensions/external/pgtap/recipe.toml", "src/extensions/external/pgtap/targets/native-static-registry.toml", diff --git a/src/extensions/generated/docs/extensions.json b/src/extensions/generated/docs/extensions.json index 0f3e37d36..fc11656c5 100644 --- a/src/extensions/generated/docs/extensions.json +++ b/src/extensions/generated/docs/extensions.json @@ -978,6 +978,52 @@ }, "version": "1.4" }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/pgmq.tar.zst", + "blocker": "PGMQ 1.12.0 is pinned for native desktop, mobile, and WASIX qualification, but exact-commit package, lifecycle, restart, and dump/restore evidence has not yet passed; keep it out of public catalogs until that CI evidence exists.", + "dependencies": [], + "desktop-release-ready": false, + "display-name": "PGMQ", + "family": "External PGXS", + "id": "pgmq", + "mobile-release-ready": false, + "native-dependencies": [], + "packaged": false, + "preload-required": false, + "promoted": false, + "public": false, + "restart-required": false, + "smoke": { + "direct": "not-run", + "dump-restore": "not-run", + "restart": "not-run", + "server": "not-run" + }, + "sql-name": "pgmq", + "stable": false, + "support": { + "mobile": { + "android": "candidate", + "ios": "candidate" + }, + "native": { + "broker": "candidate", + "direct": "candidate", + "server": "candidate" + }, + "wasix": { + "direct": "candidate", + "server": "candidate" + } + }, + "target-status": { + "mobile": "candidate", + "native": "candidate", + "wasix": "candidate" + }, + "version": "1.12.0" + }, { "activation": "CREATE EXTENSION", "archive": "extensions/pgtap.tar.zst", diff --git a/src/extensions/generated/extensions.build-plan.json b/src/extensions/generated/extensions.build-plan.json index d69ddb6b3..38e460ddb 100644 --- a/src/extensions/generated/extensions.build-plan.json +++ b/src/extensions/generated/extensions.build-plan.json @@ -1034,6 +1034,39 @@ "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pgcrypto.test.js" ] }, + { + "id": "pgmq", + "sql-name": "pgmq", + "display-name": "PGMQ", + "source-kind": "oliphaunt-other-extension", + "build-kind": "pgxs-sql-only", + "source-dir": "target/oliphaunt-sources/checkouts/pgmq/pgmq-extension", + "archive": "extensions/pgmq.tar.zst", + "control-file": "target/oliphaunt-sources/checkouts/pgmq/pgmq-extension/pgmq.control", + "stable": false, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pgmq", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "not-run", + "server": "not-run", + "restart": "not-run", + "dump-restore": "not-run" + }, + "tests": [ + "src/extensions/external/pgmq/tests/smoke.sql" + ] + }, { "id": "pgtap", "sql-name": "pgtap", diff --git a/src/extensions/generated/extensions.catalog.json b/src/extensions/generated/extensions.catalog.json index 0da538986..df123fc58 100644 --- a/src/extensions/generated/extensions.catalog.json +++ b/src/extensions/generated/extensions.catalog.json @@ -1658,6 +1658,62 @@ }, "notes": [] }, + { + "id": "pgmq", + "sql-name": "pgmq", + "rust-constant": "PGMQ", + "display-name": "PGMQ", + "source-kind": "oliphaunt-other-extension", + "upstream-import-name": "pgmq", + "upstream-import-path": "pgmq-extension", + "package-export": "./pgmq", + "tags": [ + "postgres extension", + "qualification candidate" + ], + "control-file": "target/oliphaunt-sources/checkouts/pgmq/pgmq-extension/pgmq.control", + "control": { + "default-version": "1.12.0", + "module-pathname": "$libdir/pgmq", + "requires": [], + "relocatable": "false", + "schema": "pgmq" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pgmq", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "not-run", + "server": "not-run", + "restart": "not-run", + "dump-restore": "not-run" + }, + "tests": [ + "src/extensions/external/pgmq/tests/smoke.sql" + ], + "promotion": { + "configured": true, + "requested": true, + "packaged": false, + "promoted": false, + "stable": false, + "archive": "extensions/pgmq.tar.zst", + "blocker": "PGMQ 1.12.0 is pinned for native desktop, mobile, and WASIX qualification, but exact-commit package, lifecycle, restart, and dump/restore evidence has not yet passed; keep it out of public catalogs until that CI evidence exists." + }, + "notes": [ + "promotion blocker: PGMQ 1.12.0 is pinned for native desktop, mobile, and WASIX qualification, but exact-commit package, lifecycle, restart, and dump/restore evidence has not yet passed; keep it out of public catalogs until that CI evidence exists." + ] + }, { "id": "pgtap", "sql-name": "pgtap", diff --git a/src/extensions/generated/pgxs-build.tsv b/src/extensions/generated/pgxs-build.tsv index b6452c276..297f34daf 100644 --- a/src/extensions/generated/pgxs-build.tsv +++ b/src/extensions/generated/pgxs-build.tsv @@ -3,5 +3,6 @@ pg_hashids pg_hashids target/oliphaunt-sources/checkouts/pg_hashids pg_hashids.s pg_ivm pg_ivm target/oliphaunt-sources/checkouts/pg_ivm pg_ivm.so extensions/pg_ivm.tar.zst true - pg_textsearch pg_textsearch target/oliphaunt-sources/checkouts/pg_textsearch pg_textsearch.so extensions/pg_textsearch.tar.zst true - pg_uuidv7 pg_uuidv7 target/oliphaunt-sources/checkouts/pg_uuidv7 pg_uuidv7.so extensions/pg_uuidv7.tar.zst true - +pgmq pgmq target/oliphaunt-sources/checkouts/pgmq/pgmq-extension - extensions/pgmq.tar.zst false - pgtap pgtap target/oliphaunt-sources/checkouts/pgtap - extensions/pgtap.tar.zst true - vector vector target/oliphaunt-sources/checkouts/pgvector vector.so extensions/vector.tar.zst true - diff --git a/src/extensions/moon.yml b/src/extensions/moon.yml index 08dc4d70f..56db1c88b 100644 --- a/src/extensions/moon.yml +++ b/src/extensions/moon.yml @@ -82,6 +82,8 @@ dependsOn: scope: "build" - id: "oliphaunt-extension-pg-ivm" scope: "build" + - id: "extension-pgmq" + scope: "build" - id: "oliphaunt-extension-pg-textsearch" scope: "build" - id: "oliphaunt-extension-pg-uuidv7" @@ -146,6 +148,7 @@ tasks: - "oliphaunt-extension-uuid-ossp:check" - "oliphaunt-extension-pg-hashids:check" - "oliphaunt-extension-pg-ivm:check" + - "extension-pgmq:check" - "oliphaunt-extension-pg-textsearch:check" - "oliphaunt-extension-pg-uuidv7:check" - "oliphaunt-extension-pgtap:check" diff --git a/src/extensions/tools/check-extension-model.py b/src/extensions/tools/check-extension-model.py index 15acd1f43..dce5d9324 100755 --- a/src/extensions/tools/check-extension-model.py +++ b/src/extensions/tools/check-extension-model.py @@ -777,6 +777,11 @@ def validate_pg_textsearch_mobile_version_flag() -> None: def validate_external_source_pins(build_by_sql_name: dict[str, dict], source_names: set[str]) -> None: + source_catalog_by_sql_name = { + row.get("sql-name", row.get("id")): row + for row in read_json(SOURCE_CATALOG).get("extensions", []) + if isinstance(row, dict) + } for source_path in sorted(EXTERNAL_ROOT.glob("*/source.toml")): extension_dir = source_path.parent sql_name = validate_sql_name(extension_dir.name, f"{rel(source_path)} directory") @@ -790,6 +795,10 @@ def validate_external_source_pins(build_by_sql_name: dict[str, dict], source_nam generated_source_dir = generated.get("source-dir") if isinstance(generated_source_dir, str) and generated_source_dir: expected_checkout = f"target/oliphaunt-sources/checkouts/{name}" + source_catalog_row = source_catalog_by_sql_name.get(sql_name) or {} + upstream_import_path = source_catalog_row.get("upstream-import-path") + if isinstance(upstream_import_path, str) and upstream_import_path: + expected_checkout = f"{expected_checkout}/{upstream_import_path}" if generated_source_dir != expected_checkout: fail( f"{rel(source_path)} source name {name!r} implies checkout " diff --git a/src/runtimes/liboliphaunt/wasix/.release-semantic-inputs.json b/src/runtimes/liboliphaunt/wasix/.release-semantic-inputs.json index 65323c692..4e5b18ee6 100644 --- a/src/runtimes/liboliphaunt/wasix/.release-semantic-inputs.json +++ b/src/runtimes/liboliphaunt/wasix/.release-semantic-inputs.json @@ -166,7 +166,7 @@ }, { "path": "tools/xtask/src/extension_catalog.rs", - "sha256": "c67f61826704e8ff71265af85f8552b1a134fba1252b46921d10dea6d51e854a" + "sha256": "1c0b472c2103246843becd05400907ead9081fc8740ae34c15fef9a502503e62" }, { "path": "tools/xtask/src/fs_utils.rs", @@ -178,7 +178,7 @@ }, { "path": "tools/xtask/src/postgres_guard.rs", - "sha256": "0ccadf939d2e52a8a3f1a3445e6e1d401085ead7f788085c443e6563b0e19064" + "sha256": "da19f4d401306cbdec5f0638cadfb27065a0c935c173e030433c2c4a2afb1f55" }, { "path": "tools/xtask/src/release_workspace.rs", @@ -212,19 +212,19 @@ }, { "path": "src/extensions/generated/docs/extension-evidence.json", - "sha256": "1b9ae33dc2ca239985c4cd5c1571e2669f2ea905acf45b6d81e4e31261e7f7bb" + "sha256": "805de8c49c80c5d380f7420a3aba1540e540f1fb87fe88838f27f47330c68e83" }, { "path": "src/extensions/generated/docs/extensions.json", - "sha256": "e0741112aeb1d1a187ba3fda06a06302026048eceef08b389f2e55a94343cd69" + "sha256": "bdab70f9bcbdf4189356fd11e650de3051867356a555448f6dc8b70e847776f8" }, { "path": "src/extensions/generated/extensions.build-plan.json", - "sha256": "24dceceb89d3f91da6c66bc309c4767408b71908bafd32027d784b2a5f975ca5" + "sha256": "0e03beee22c5ea838e65875bfceb9854c01e188b7680c310c4eab3cedb4bd058" }, { "path": "src/extensions/generated/extensions.catalog.json", - "sha256": "79ba39ba702d247fd975089cc812fbb4c3624ebd325f0fe1f6255dd575efddb7" + "sha256": "c9685962d0dc9b54a0454dbba559303367c7df08918e0842d01bbf580d1f0cf1" }, { "path": "src/extensions/generated/mobile/static-extensions.tsv", @@ -236,7 +236,7 @@ }, { "path": "src/extensions/generated/pgxs-build.tsv", - "sha256": "89e3dedea42a4921cea9a180138cc0092808718ed6cc55e66858b074e005c29b" + "sha256": "486c94c8eb0c71ff59941eaa39f3806dd4d5ba7375044685402c23d046ac94aa" }, { "path": "src/extensions/generated/sdk/js.json", @@ -377,5 +377,5 @@ ] } ], - "sha256": "763a5c1635efc9c5275c839eb4870379d19077cb6426f89cc690e9ae1890bbad" + "sha256": "da6923976b2ee188ec14f56dbbf09138c9cedff8bd9ece7198a03bc3963b7fd1" } diff --git a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 index e02e39f54..1519258a7 100644 --- a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 +++ b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 @@ -1 +1 @@ -178f0d74e7367e8dfe4eb44c32fb7ed62f0c17f30001b56b6d7f83617f11b5c5 +cee4523b1808009e1a7ef41b5895f2a462b1a3cf7f66d97112fd390d968dd03d diff --git a/src/sdks/rust/tests/sdk_extensions.rs b/src/sdks/rust/tests/sdk_extensions.rs index 28f25d8b1..4146319d2 100644 --- a/src/sdks/rust/tests/sdk_extensions.rs +++ b/src/sdks/rust/tests/sdk_extensions.rs @@ -468,7 +468,7 @@ fn target_specific_release_readiness_can_diverge_from_wasix_support() { } #[test] -fn pg18_blocked_extensions_remain_out_of_release_ready_catalog() { +fn deferred_extensions_remain_out_of_release_ready_catalog() { let catalog = generated_wasm_extension_catalog(); let extensions = catalog["extensions"] .as_array() @@ -490,40 +490,49 @@ fn pg18_blocked_extensions_remain_out_of_release_ready_catalog() { .collect::>(); assert_eq!( blocked_ids, - BTreeSet::from(["age".to_owned()]), - "every PG18.4 non-stable extension needs an explicit blocker before release-ready parity can move" + BTreeSet::from(["age".to_owned(), "pgmq".to_owned()]), + "every non-stable extension needs an explicit blocker before release-ready parity can move" ); - let (id, sql_name, requested, packaged, blocker) = - ("age", "age", false, false, "ExecInitExtraTupleSlot"); - let extension = extensions - .iter() - .find(|extension| extension["id"].as_str() == Some(id)) - .unwrap_or_else(|| panic!("generated wasm extension catalog must contain {id}")); - assert_eq!(extension["sql-name"].as_str(), Some(sql_name)); - assert_eq!( - extension["promotion"]["requested"].as_bool(), - Some(requested), - "{id} build request state must match its current PG18.4 blocker status" - ); - assert_eq!( - extension["promotion"]["stable"].as_bool(), - Some(false), - "{id} must not be treated as PG18.4 release-ready until its blocker is resolved" - ); - assert_eq!( - extension["promotion"]["packaged"].as_bool(), - Some(packaged), - "{id} packaged state must match the generated WASIX artifact evidence" - ); - assert!( - extension["promotion"]["blocker"] - .as_str() - .unwrap_or_default() - .contains(blocker), - "{id} must record the concrete PG18.4 blocker" - ); - assert_eq!(Extension::by_release_ready_sql_name(sql_name), None); + for (id, sql_name, requested, packaged, blocker) in [ + ("age", "age", false, false, "ExecInitExtraTupleSlot"), + ( + "pgmq", + "pgmq", + true, + false, + "exact-commit package, lifecycle, restart, and dump/restore evidence has not yet passed", + ), + ] { + let extension = extensions + .iter() + .find(|extension| extension["id"].as_str() == Some(id)) + .unwrap_or_else(|| panic!("generated wasm extension catalog must contain {id}")); + assert_eq!(extension["sql-name"].as_str(), Some(sql_name)); + assert_eq!( + extension["promotion"]["requested"].as_bool(), + Some(requested), + "{id} build request state must match its current qualification status" + ); + assert_eq!( + extension["promotion"]["stable"].as_bool(), + Some(false), + "{id} must not be treated as release-ready until its blocker is resolved" + ); + assert_eq!( + extension["promotion"]["packaged"].as_bool(), + Some(packaged), + "{id} packaged state must match the generated WASIX artifact evidence" + ); + assert!( + extension["promotion"]["blocker"] + .as_str() + .unwrap_or_default() + .contains(blocker), + "{id} must record its concrete qualification blocker" + ); + assert_eq!(Extension::by_release_ready_sql_name(sql_name), None); + } } #[test] diff --git a/tools/graph/synthetic/affected.toml b/tools/graph/synthetic/affected.toml index e5aaa5902..1acc5cbe6 100644 --- a/tools/graph/synthetic/affected.toml +++ b/tools/graph/synthetic/affected.toml @@ -58,6 +58,7 @@ moon_projects = [ "oliphaunt-extension-pg-ivm", "oliphaunt-extension-pg-textsearch", "oliphaunt-extension-pg-uuidv7", + "extension-pgmq", "oliphaunt-extension-pgtap", "oliphaunt-extension-postgis", "oliphaunt-extension-vector", @@ -114,6 +115,7 @@ moon_projects = [ "oliphaunt-extension-pg-visibility", "oliphaunt-extension-pg-walinspect", "oliphaunt-extension-pgcrypto", + "extension-pgmq", "oliphaunt-extension-pgtap", "oliphaunt-extension-postgis", "oliphaunt-extension-seg", @@ -175,6 +177,7 @@ moon_projects = [ "oliphaunt-extension-pg-visibility", "oliphaunt-extension-pg-walinspect", "oliphaunt-extension-pgcrypto", + "extension-pgmq", "oliphaunt-extension-pgtap", "oliphaunt-extension-postgis", "oliphaunt-extension-seg", @@ -231,6 +234,7 @@ moon_projects = [ "oliphaunt-extension-pg-visibility", "oliphaunt-extension-pg-walinspect", "oliphaunt-extension-pgcrypto", + "extension-pgmq", "oliphaunt-extension-pgtap", "oliphaunt-extension-postgis", "oliphaunt-extension-seg", @@ -289,6 +293,7 @@ moon_projects = [ "oliphaunt-extension-pg-visibility", "oliphaunt-extension-pg-walinspect", "oliphaunt-extension-pgcrypto", + "extension-pgmq", "oliphaunt-extension-pgtap", "oliphaunt-extension-postgis", "oliphaunt-extension-seg", @@ -351,6 +356,7 @@ moon_projects = [ "oliphaunt-extension-pg-visibility", "oliphaunt-extension-pg-walinspect", "oliphaunt-extension-pgcrypto", + "extension-pgmq", "oliphaunt-extension-pgtap", "oliphaunt-extension-postgis", "oliphaunt-extension-seg", @@ -406,6 +412,7 @@ moon_projects = [ "oliphaunt-extension-pg-visibility", "oliphaunt-extension-pg-walinspect", "oliphaunt-extension-pgcrypto", + "extension-pgmq", "oliphaunt-extension-pgtap", "oliphaunt-extension-postgis", "oliphaunt-extension-seg", @@ -516,6 +523,7 @@ moon_projects = [ "oliphaunt-extension-pg-visibility", "oliphaunt-extension-pg-walinspect", "oliphaunt-extension-pgcrypto", + "extension-pgmq", "oliphaunt-extension-pgtap", "oliphaunt-extension-postgis", "oliphaunt-extension-seg", diff --git a/tools/release/check-extension-publication-legal-clearance.test.mjs b/tools/release/check-extension-publication-legal-clearance.test.mjs index 955697016..0f8d333df 100644 --- a/tools/release/check-extension-publication-legal-clearance.test.mjs +++ b/tools/release/check-extension-publication-legal-clearance.test.mjs @@ -80,13 +80,19 @@ profiles = ["native-desktop-v1"] return { product, reason, root }; } -test("the live tree has no publication blockers and keeps the legal gate active", () => { +test("the live tree keeps deferred PGMQ out of publication and the legal gate active", () => { assert.match( readFileSync("tools/release/release-metadata-check.mjs", "utf8"), /check-extension-publication-legal-clearance[.]mjs/u, ); - assert.deepEqual(extensionQualificationCandidates(), []); - assert.deepEqual(declaredExtensionPublicationBlockers(), []); + assert.deepEqual( + extensionQualificationCandidates().map(({ sqlName }) => sqlName), + ["pgmq"], + ); + assert.deepEqual( + declaredExtensionPublicationBlockers().map(({ product }) => product), + ["oliphaunt-extension-pgmq"], + ); assert.deepEqual(activeBlockedExtensionPublications(), []); assert.doesNotThrow(() => assertExtensionPublicationLegalClearance()); @@ -94,7 +100,7 @@ test("the live tree has no publication blockers and keeps the legal gate active" "tools/release/check-extension-publication-legal-clearance.mjs", ], { encoding: "utf8" }); assert.equal(result.status, 0, result.stderr); - assert.match(result.stdout, /validated 0 deferred blocker[(]s[)]; 0 active blocked publications/u); + assert.match(result.stdout, /validated 1 deferred blocker[(]s[)]; 0 active blocked publications/u); }); test("the legal gate validates a dormant fixture blocker and fails closed if it becomes active", (t) => { diff --git a/tools/release/extension-qualification-candidates.test.mjs b/tools/release/extension-qualification-candidates.test.mjs index 99cb35284..d1278fbd8 100644 --- a/tools/release/extension-qualification-candidates.test.mjs +++ b/tools/release/extension-qualification-candidates.test.mjs @@ -22,6 +22,7 @@ import { import { loadPublicationCatalog } from "./publication-catalog.mjs"; const POSTGIS_PRODUCT = "oliphaunt-extension-postgis"; +const PGMQ_BLOCKER = "PGMQ 1.12.0 is pinned for native desktop, mobile, and WASIX qualification, but exact-commit package, lifecycle, restart, and dump/restore evidence has not yet passed; keep it out of public catalogs until that CI evidence exists."; const NATIVE_TARGETS = [ "android-arm64-v8a", "android-x86_64", @@ -104,8 +105,21 @@ profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] return { blocker, root }; } -test("the live tree has no deferred qualification candidates", () => { - assert.deepEqual(extensionQualificationCandidates(), []); +test("the live tree exposes only the exact deferred PGMQ qualification candidate", () => { + assert.deepEqual(extensionQualificationCandidates(), [ + { + schema: "oliphaunt-extension-qualification-candidate-v1", + extensionId: "pgmq", + sqlName: "pgmq", + sourceName: "pgmq", + sourceVersion: "1.12.0", + sourceCommit: "08ace4087dbf00e51704c5a3d9df2e15fd566127", + requested: true, + stable: false, + blocker: PGMQ_BLOCKER, + targetProfiles: ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"], + }, + ]); const qualificationRows = readFileSync( "src/extensions/generated/mobile/qualification-static-extensions.tsv", "utf8", @@ -216,6 +230,26 @@ test("target-scoped catalog rows preserve the public default and expose only req publicRows, ); + const androidPgmqRows = await catalogRows({ + qualificationSqlNames: ["pgmq"], + qualificationTarget: "android-arm64-v8a", + }); + const androidPgmq = androidPgmqRows.find(({ sqlName }) => sqlName === "pgmq"); + assert.deepEqual( + { + desktopPrebuilt: androidPgmq?.desktopPrebuilt, + mobilePrebuilt: androidPgmq?.mobilePrebuilt, + mobileStaticRequired: androidPgmq?.mobileStaticRequired, + stem: androidPgmq?.stem, + }, + { + desktopPrebuilt: false, + mobilePrebuilt: true, + mobileStaticRequired: false, + stem: "", + }, + ); + const androidRows = await catalogRows({ qualificationSqlNames: ["age"], qualificationTarget: "android-arm64-v8a", @@ -281,11 +315,11 @@ test("deferred catalog dependencies retain the exact qualified closure without e ); }); -test("keeps live build matrices candidate-free and PostGIS public", () => { +test("qualifies PGMQ on every native target and WASIX while keeping PostGIS public", () => { const native = extensionArtifactsNativeMatrix().include; assert.deepEqual(native.map(({ target }) => target), NATIVE_TARGETS); for (const row of native) { - assert.deepEqual(csv(row.qualification_sql_names_csv), []); + assert.deepEqual(csv(row.qualification_sql_names_csv), ["pgmq"]); assert.equal(csv(row.sql_names_csv).length, 39); assert.equal(csv(row.sql_names_csv).includes("postgis"), true); assert.equal(csv(row.extensions_csv).includes(POSTGIS_PRODUCT), true); @@ -294,7 +328,7 @@ test("keeps live build matrices candidate-free and PostGIS public", () => { const wasix = extensionArtifactsWasixMatrix().include; assert.equal(wasix.length, 1); assert.equal(wasix[0].target, "wasix-portable"); - assert.deepEqual(csv(wasix[0].qualification_sql_names_csv), []); + assert.deepEqual(csv(wasix[0].qualification_sql_names_csv), ["pgmq"]); assert.equal(csv(wasix[0].sql_names_csv).length, 39); assert.equal(csv(wasix[0].sql_names_csv).includes("postgis"), true); assert.equal(csv(wasix[0].extensions_csv).includes(POSTGIS_PRODUCT), true); diff --git a/tools/release/extension-upstream-licenses.test.mjs b/tools/release/extension-upstream-licenses.test.mjs index 792d88fd5..ad9e6fd27 100644 --- a/tools/release/extension-upstream-licenses.test.mjs +++ b/tools/release/extension-upstream-licenses.test.mjs @@ -51,6 +51,7 @@ test("every active external release has an exact upstream license contract", () "pg_ivm", "pg_textsearch", "pg_uuidv7", + "pgmq", "pgtap", "postgis", "vector", @@ -68,6 +69,7 @@ test("every active external release has an exact upstream license contract", () "pg_ivm", "pg_textsearch", "pg_uuidv7", + "pgmq", "pgtap", "pgvector", "postgis", @@ -133,7 +135,7 @@ test("each external product owns an exact self-contained legal-data closure", () .map((entry) => path.join(externalRoot, entry.name, "upstream-license-data.json")) .filter(existsSync) .sort(); - assert.equal(files.length, 7); + assert.equal(files.length, 8); for (const file of files) { const data = JSON.parse(readFileSync(file, "utf8")); const owner = path.basename(path.dirname(file)); diff --git a/tools/release/native-mobile-qualification-selection.test.mjs b/tools/release/native-mobile-qualification-selection.test.mjs new file mode 100644 index 000000000..02a5984b3 --- /dev/null +++ b/tools/release/native-mobile-qualification-selection.test.mjs @@ -0,0 +1,72 @@ +import { describe, expect, test } from 'bun:test'; + +import { + mergeMobileStaticSpecRegistries, + selectMobileStaticModuleSqlNames, +} from '../../src/extensions/artifacts/native/tools/extension-artifact-packager.mjs'; + +const HEADER = [ + 'sql-name', + 'native-module-stem', + 'source-kind', + 'source-rel', + 'mobile-static-dependencies', + 'ios-static-dependencies', + 'android-static-dependencies', + 'include-dependencies', + 'include-dirs', + 'cflags', + 'hash-source-dependencies', + 'ios-hash-source-dependencies', + 'android-hash-source-dependencies', + 'hash-dirs', + 'source-files', + 'source-recursive-dirs', +].join('\t'); + +function specRow(sqlName, stem = sqlName) { + return [sqlName, stem, 'external', `checkouts/${sqlName}`].join('\t'); +} + +function specTable(rows) { + return [`# @generated by test`, HEADER, ...rows, ''].join('\n'); +} + +describe('mixed public and qualification mobile extension selection', () => { + test('SQL-only qualification candidates do not enter static module builds', () => { + const rows = [ + { + sqlName: 'amcheck', + stem: 'amcheck', + mobilePrebuilt: true, + mobileStaticRequired: true, + }, + { + sqlName: 'pgmq', + stem: '', + mobilePrebuilt: true, + mobileStaticRequired: false, + }, + ]; + + expect(selectMobileStaticModuleSqlNames(rows, ['amcheck', 'pgmq'])).toEqual(['amcheck']); + }); + + test('public and qualification static registries remain available together', () => { + const merged = mergeMobileStaticSpecRegistries( + specTable([specRow('amcheck')]), + specTable([specRow('candidate_module')]), + ); + + expect(merged).toContain(specRow('amcheck')); + expect(merged).toContain(specRow('candidate_module')); + expect(merged.indexOf(specRow('amcheck'))).toBeLessThan(merged.indexOf(specRow('candidate_module'))); + }); + + test('qualification registries cannot shadow public extension specs', () => { + expect(() => mergeMobileStaticSpecRegistries( + specTable([specRow('amcheck')]), + specTable([specRow('amcheck')]), + )).toThrow("overlaps public SQL name 'amcheck'"); + }); +}); diff --git a/tools/xtask/src/asset_checks.rs b/tools/xtask/src/asset_checks.rs index f97007a86..9f843359c 100644 --- a/tools/xtask/src/asset_checks.rs +++ b/tools/xtask/src/asset_checks.rs @@ -893,29 +893,20 @@ pub(crate) fn ensure_supported_aot_target(target: &str) -> Result<()> { ) } -pub(crate) fn verify_generated_extension_surface() -> Result<()> { - let manifest_path = Path::new(GENERATED_ASSETS_DIR).join("manifest.json"); - let manifest_text = fs::read_to_string(&manifest_path) - .with_context(|| format!("read {}", manifest_path.display()))?; - let manifest: AssetManifestOut = - serde_json::from_str(&manifest_text).context("parse committed asset manifest")?; - if skip_extensions_for_perf_probe() && manifest.extensions.is_empty() { - println!("core-only asset manifest detected; skipping generated extension surface guard"); - return Ok(()); - } - let catalog_text = fs::read_to_string("src/extensions/generated/extensions.catalog.json") - .context("read src/extensions/generated/extensions.catalog.json")?; - let catalog: serde_json::Value = - serde_json::from_str(&catalog_text).context("parse generated extension catalog")?; - let generated = fs::read_to_string( - "src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/generated_extensions.rs", - ) - .context( - "read src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/generated_extensions.rs", - )?; +struct GeneratedExtensionCatalogSurface { + runtime_constants: BTreeMap, + packaged_constants: BTreeMap, + promoted_constants: BTreeMap, + qualification_sql_names: BTreeSet, +} +fn generated_extension_catalog_surface( + catalog: &serde_json::Value, +) -> Result { + let mut runtime_constants = BTreeMap::new(); let mut packaged_constants = BTreeMap::new(); let mut promoted_constants = BTreeMap::new(); + let mut qualification_sql_names = BTreeSet::new(); for entry in catalog .get("extensions") .and_then(|value| value.as_array()) @@ -937,35 +928,86 @@ pub(crate) fn verify_generated_extension_surface() -> Result<()> { .pointer("/promotion/packaged") .and_then(|value| value.as_bool()) .unwrap_or(false); + let promoted = entry + .pointer("/promotion/promoted") + .and_then(|value| value.as_bool()) + .unwrap_or(false); + let stable = entry + .pointer("/promotion/stable") + .and_then(|value| value.as_bool()) + .unwrap_or(false); let has_archive = entry .pointer("/promotion/archive") .and_then(|value| value.as_str()) .is_some(); - if requested && packaged && has_archive { - packaged_constants.insert(sql_name.to_owned(), rust_constant.to_owned()); + if requested && has_archive { + runtime_constants.insert(sql_name.to_owned(), rust_constant.to_owned()); + if packaged { + packaged_constants.insert(sql_name.to_owned(), rust_constant.to_owned()); + } else { + let blocker = entry + .pointer("/promotion/blocker") + .and_then(|value| value.as_str()) + .unwrap_or(""); + ensure!( + !promoted && !stable && !blocker.is_empty(), + "unpackaged extension {sql_name} may enter the WASIX qualification runtime only while non-stable, non-promoted, and publication-blocked" + ); + qualification_sql_names.insert(sql_name.to_owned()); + } } - let promoted = entry - .pointer("/promotion/promoted") - .and_then(|value| value.as_bool()) - .unwrap_or(false); if promoted { promoted_constants.insert(sql_name.to_owned(), rust_constant.to_owned()); } } + Ok(GeneratedExtensionCatalogSurface { + runtime_constants, + packaged_constants, + promoted_constants, + qualification_sql_names, + }) +} + +pub(crate) fn verify_generated_extension_surface() -> Result<()> { + let manifest_path = Path::new(GENERATED_ASSETS_DIR).join("manifest.json"); + let manifest_text = fs::read_to_string(&manifest_path) + .with_context(|| format!("read {}", manifest_path.display()))?; + let manifest: AssetManifestOut = + serde_json::from_str(&manifest_text).context("parse committed asset manifest")?; + if skip_extensions_for_perf_probe() && manifest.extensions.is_empty() { + println!("core-only asset manifest detected; skipping generated extension surface guard"); + return Ok(()); + } + let catalog_text = fs::read_to_string("src/extensions/generated/extensions.catalog.json") + .context("read src/extensions/generated/extensions.catalog.json")?; + let catalog: serde_json::Value = + serde_json::from_str(&catalog_text).context("parse generated extension catalog")?; + let generated = fs::read_to_string( + "src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/generated_extensions.rs", + ) + .context( + "read src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/generated_extensions.rs", + )?; + + let surface = generated_extension_catalog_surface(&catalog)?; let manifest_packaged_sql_names = manifest .extensions .iter() .map(|extension| extension.sql_name.clone()) .collect::>(); - let catalog_packaged_sql_names = packaged_constants.keys().cloned().collect::>(); - if manifest_packaged_sql_names != catalog_packaged_sql_names { + let catalog_runtime_sql_names = surface + .runtime_constants + .keys() + .cloned() + .collect::>(); + if manifest_packaged_sql_names != catalog_runtime_sql_names { bail!( - "packaged extension catalog and asset manifest disagree: manifest-only={:?} catalog-only={:?}", + "WASIX runtime extension catalog and asset manifest disagree: manifest-only={:?} catalog-only={:?}", manifest_packaged_sql_names - .difference(&catalog_packaged_sql_names) + .difference(&catalog_runtime_sql_names) .collect::>(), - catalog_packaged_sql_names + catalog_runtime_sql_names .difference(&manifest_packaged_sql_names) .collect::>() ); @@ -977,7 +1019,11 @@ pub(crate) fn verify_generated_extension_surface() -> Result<()> { .filter(|extension| extension.smoke_status.promoted) .map(|extension| extension.sql_name.clone()) .collect::>(); - let catalog_sql_names = promoted_constants.keys().cloned().collect::>(); + let catalog_sql_names = surface + .promoted_constants + .keys() + .cloned() + .collect::>(); if manifest_promoted_sql_names != catalog_sql_names { bail!( "promoted extension catalog and asset manifest disagree: manifest-only={:?} catalog-only={:?}", @@ -991,12 +1037,37 @@ pub(crate) fn verify_generated_extension_surface() -> Result<()> { } for extension in &manifest.extensions { - let rust_constant = packaged_constants.get(&extension.sql_name).ok_or_else(|| { - anyhow!( - "extension {} missing from packaged catalog", + let rust_constant = surface + .runtime_constants + .get(&extension.sql_name) + .ok_or_else(|| { + anyhow!( + "extension {} missing from WASIX runtime catalog", + extension.sql_name + ) + })?; + if surface + .qualification_sql_names + .contains(&extension.sql_name) + { + ensure!( + !extension.stable && !extension.smoke_status.promoted, + "qualification extension {} leaked stable or promoted state into the WASIX runtime manifest", extension.sql_name - ) - })?; + ); + let public_constant = format!("pub const {rust_constant}: Extension ="); + ensure!( + !generated.contains(&public_constant), + "qualification extension {} leaked into the public generated extension API", + extension.sql_name + ); + continue; + } + ensure!( + surface.packaged_constants.contains_key(&extension.sql_name), + "extension {} is neither public packaged nor a publication-blocked qualification candidate", + extension.sql_name + ); let candidate_const = format!("CANDIDATE_{rust_constant}"); for (needle, description) in [ ( @@ -1046,6 +1117,82 @@ pub(crate) fn verify_generated_extension_surface() -> Result<()> { Ok(()) } +#[cfg(test)] +mod generated_extension_surface_tests { + use super::generated_extension_catalog_surface; + + #[test] + fn accepts_publication_blocked_runtime_qualification_candidates() { + let catalog = serde_json::json!({ + "extensions": [ + { + "sql-name": "public_ext", + "rust-constant": "PUBLIC_EXT", + "promotion": { + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/public_ext.tar.zst" + } + }, + { + "sql-name": "candidate_ext", + "rust-constant": "CANDIDATE_EXT", + "promotion": { + "requested": true, + "packaged": false, + "promoted": false, + "stable": false, + "archive": "extensions/candidate_ext.tar.zst", + "blocker": "awaiting exact-target qualification" + } + } + ] + }); + let surface = generated_extension_catalog_surface(&catalog).expect("catalog surface"); + assert_eq!( + surface + .runtime_constants + .keys() + .cloned() + .collect::>(), + ["candidate_ext", "public_ext"] + ); + assert_eq!( + surface + .packaged_constants + .keys() + .cloned() + .collect::>(), + ["public_ext"] + ); + assert!(surface.qualification_sql_names.contains("candidate_ext")); + } + + #[test] + fn rejects_unblocked_unpublished_runtime_candidates() { + let catalog = serde_json::json!({ + "extensions": [{ + "sql-name": "candidate_ext", + "rust-constant": "CANDIDATE_EXT", + "promotion": { + "requested": true, + "packaged": false, + "promoted": false, + "stable": false, + "archive": "extensions/candidate_ext.tar.zst" + } + }] + }); + let error = generated_extension_catalog_surface(&catalog) + .err() + .expect("missing blocker must fail") + .to_string(); + assert!(error.contains("publication-blocked"), "{error}"); + } +} + fn verify_generated_extension_surface_if_available() -> Result<()> { let manifest_path = Path::new(GENERATED_ASSETS_DIR).join("manifest.json"); if !manifest_path.exists() { diff --git a/tools/xtask/src/extension_catalog.rs b/tools/xtask/src/extension_catalog.rs index ce2a47f8b..ff5f38dd1 100644 --- a/tools/xtask/src/extension_catalog.rs +++ b/tools/xtask/src/extension_catalog.rs @@ -1166,6 +1166,18 @@ fn validate_catalog(catalog: &ExtensionCatalog) -> Result<()> { "duplicate SQL extension name {}", extension.sql_name ); + if let Some(source_path) = extension.upstream_import_path.as_deref() { + let source_path = Path::new(source_path); + ensure!( + !source_path.as_os_str().is_empty() + && !source_path.is_absolute() + && source_path + .components() + .all(|component| matches!(component, std::path::Component::Normal(_))), + "{} upstream-import-path must be a non-empty normalized relative path", + extension.id + ); + } ensure!( !extension.promotion.promoted || extension.promotion.stable, "{} cannot be promoted without stable=true", @@ -1459,7 +1471,27 @@ fn build_kind( } fn extension_source_dir(extension: &ExtensionCatalogEntry) -> String { - extension_source_dir_for(&extension.id, &extension.source_kind) + extension_source_dir_with_import_path( + &extension.id, + &extension.source_kind, + extension.upstream_import_path.as_deref(), + ) +} + +fn extension_source_dir_with_import_path( + extension_id: &str, + source_kind: &str, + upstream_import_path: Option<&str>, +) -> String { + let root = extension_source_dir_for(extension_id, source_kind); + upstream_import_path + .map(|source_path| { + Path::new(&root) + .join(source_path) + .to_string_lossy() + .replace('\\', "/") + }) + .unwrap_or(root) } fn pgxs_make_args(extension: &ExtensionCatalogEntry) -> Vec { @@ -1896,6 +1928,18 @@ mod tests { )); } + #[test] + fn external_source_dir_supports_a_nested_upstream_extension_root() { + assert_eq!( + extension_source_dir_with_import_path( + "pgmq", + "oliphaunt-other-extension", + Some("pgmq-extension"), + ), + "target/oliphaunt-sources/checkouts/pgmq/pgmq-extension" + ); + } + #[test] fn generated_catalog_versions_are_merged_from_non_generated_source_metadata() -> Result<()> { assert_ne!(SOURCE_CATALOG_PATH, CATALOG_PATH); diff --git a/tools/xtask/src/postgres_guard.rs b/tools/xtask/src/postgres_guard.rs index 5083644e4..32befa9b5 100644 --- a/tools/xtask/src/postgres_guard.rs +++ b/tools/xtask/src/postgres_guard.rs @@ -1491,17 +1491,19 @@ fn source_pin_for_checkout_dir<'a>( manifest: &'a SourcesManifest, source_dir: &str, ) -> Option<&'a SourcePin> { - let expected = normalize_manifest_path(Path::new(source_dir)); manifest.sources.iter().find(|source| { source_checkout_path(source.name.as_str()) - .map(|path| normalize_manifest_path(&path)) - .as_deref() - == Some(expected.as_str()) + .is_some_and(|checkout| source_dir_is_within_checkout(source_dir, &checkout)) }) } -fn normalize_manifest_path(path: &Path) -> String { - path.to_string_lossy().replace('\\', "/") +fn source_dir_is_within_checkout(source_dir: &str, checkout: &Path) -> bool { + let Ok(relative) = Path::new(source_dir).strip_prefix(checkout) else { + return false; + }; + relative + .components() + .all(|component| matches!(component, std::path::Component::Normal(_))) } fn ensure_pg18_experiment_patch_disposition() -> Result<()> { @@ -1721,3 +1723,37 @@ fn check_rust_host_runtime_abi_surface(postgres_mod: &str) -> Result<()> { } Ok(()) } + +#[cfg(test)] +mod tests { + use super::source_dir_is_within_checkout; + use std::path::Path; + + #[test] + fn pgxs_source_dirs_may_use_safe_subdirectories_of_pinned_checkouts() { + let checkout = Path::new("target/oliphaunt-sources/checkouts/pgmq"); + + assert!(source_dir_is_within_checkout( + "target/oliphaunt-sources/checkouts/pgmq", + checkout, + )); + assert!(source_dir_is_within_checkout( + "target/oliphaunt-sources/checkouts/pgmq/pgmq-extension", + checkout, + )); + } + + #[test] + fn pgxs_source_dirs_cannot_escape_or_prefix_match_pinned_checkouts() { + let checkout = Path::new("target/oliphaunt-sources/checkouts/pgmq"); + + assert!(!source_dir_is_within_checkout( + "target/oliphaunt-sources/checkouts/pgmq-other", + checkout, + )); + assert!(!source_dir_is_within_checkout( + "target/oliphaunt-sources/checkouts/pgmq/../other", + checkout, + )); + } +}