Compare commits

..

20 Commits

Author SHA1 Message Date
Jerry Ma
996cca12c0 [v3] - fix(windows): resolve doctor-installed perl and quote perl path (#1174) 2026-07-08 13:59:05 +08:00
crazywhalecc
5451945972 Use escapeshellarg instead 2026-07-08 13:21:52 +08:00
crazywhalecc
fc1673e37f Merge branch 'refs/heads/v3' into pr/win-openssl-perl-path
# Conflicts:
#	src/Package/Library/openssl.php
2026-07-08 13:19:36 +08:00
Marc
8034ae37ea v3 weekly build fix (#1203) 2026-07-08 11:40:37 +07:00
Marc
c9f24b980d [v3] Enable SQLite column metadata (#1205) 2026-07-08 11:39:22 +07:00
crazywhalecc
facc7bc5a7 test swoole 2026-07-08 11:43:35 +08:00
crazywhalecc
160d7f5537 fix: disable undefined behavior sanitizer in build flags 2026-07-08 11:41:41 +08:00
Jerry Ma
afa5f535e9 Merge branch 'v3' into v3c/fix-builds 2026-07-08 10:06:19 +08:00
Jerry Ma
0dde151632 Merge branch 'v3' into fix/v3-sqlite-column-metadata 2026-07-08 10:00:57 +08:00
Jerry Ma
e906edb332 Test bot separate (#1206) 2026-07-08 10:00:41 +08:00
crazywhalecc
7d6c082c86 random test 2026-07-07 22:38:39 +08:00
crazywhalecc
db4f7c6a0c feat: separate workflow for posting test bot comments 2026-07-07 22:37:23 +08:00
Jan Jakeš
ca0167c83e Enable SQLite column metadata
Build Unix SQLite with SQLITE_ENABLE_COLUMN_METADATA so the metadata APIs are available on macOS and Linux.

Remove the pdo_sqlite configure override that forced PHP to treat sqlite3_column_table_name() as unavailable, and add sanity checks for both SQLite compile options and PDO column metadata.
2026-07-07 16:25:00 +02:00
crazywhalecc
331da67049 Fix gen-ext-test-matrix AND bug (should be OR) 2026-07-07 15:48:35 +08:00
crazywhalecc
cc448d463b simdjson test 2026-07-07 15:42:34 +08:00
crazywhalecc
dc55881b78 libde265 fix build 2026-07-07 15:21:19 +08:00
crazywhalecc
a59473e4ab ext-test-matrix generation fix 2026-07-07 15:03:31 +08:00
crazywhalecc
c6a227f4de libde265 test 2026-07-07 14:50:32 +08:00
Luther Monson
6c59456c02 fix(windows): consolidate to single canonical strawberry-perl path
Per review: spc's pkg config, doctor, and MSVCToolchain all use
PKG_ROOT_PATH\strawberry-perl. The arch-suffixed strawberry-perl-{arch}-win
path was stale and shouldn't be juggled alongside it. Validate against the
one canonical pkg-root path, falling back to WindowsUtil::findCommand.
2026-05-27 22:30:04 -07:00
Luther Monson
0e8a806640 fix(windows): resolve doctor-installed perl and quote perl path
Two related fixes for OpenSSL builds on Windows:

1. spc doctor installs Strawberry Perl under `pkgroot/strawberry-perl/`,
   but the validate() check only looks at the arch-specific native path
   (`strawberry-perl-x86_64-win`). When a user runs `spc doctor` to
   install perl, the subsequent build fails because the doctor's install
   layout isn't on the lookup list. Add it as a fallback before falling
   through to WindowsUtil::findCommand.

2. Wrap the resolved perl path in quote() when building the Configure
   command. WindowsUtil::findCommand can return a path containing spaces
   (e.g. `C:\Program Files\...`), which then splits the cmd.exe
   invocation. Quoting matches how every other interpolated path in this
   command is handled.
2026-05-27 22:07:57 -07:00
13 changed files with 188 additions and 65 deletions

View File

@@ -0,0 +1,48 @@
name: Post Test Bot Comment
on:
workflow_run:
workflows: ["v3 Tests"]
types: [completed]
permissions:
pull-requests: write
actions: read
jobs:
post-comment:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Download bot output
id: download
uses: actions/download-artifact@v4
continue-on-error: true
with:
name: bot-output
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Post/Update comment
if: steps.download.outcome == 'success'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
COMMENT_BODY=$(jq -r '.comment_body' bot-output.json)
PR_NUMBER=$(jq -r '.pr_number' bot-output.json)
MARKER="<!-- spc-test-bot -->"
# Find existing bot comment id
EXISTING_ID=$(gh api \
"repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
--jq "[.[] | select(.body | startswith(\"${MARKER}\")) | .id] | first // empty")
if [ -n "$EXISTING_ID" ]; then
gh api --method PATCH \
"repos/${{ github.repository }}/issues/comments/${EXISTING_ID}" \
-f body="$COMMENT_BODY"
else
gh pr comment "$PR_NUMBER" \
--repo "${{ github.repository }}" \
--body "$COMMENT_BODY"
fi

View File

@@ -131,7 +131,6 @@ jobs:
if: needs.check-gate.outputs.enabled == 'true'
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
outputs:
need_test: ${{ steps.bot.outputs.need_test }}
@@ -168,23 +167,17 @@ jobs:
echo "php_versions=$(echo "$BOT_JSON" | jq -c '.php_versions')" >> "$GITHUB_OUTPUT"
echo "tier2=$(echo "$BOT_JSON" | jq -r '.tier2')" >> "$GITHUB_OUTPUT"
COMMENT_BODY=$(echo "$BOT_JSON" | jq -r '.comment_body')
MARKER="<!-- spc-test-bot -->"
# Save JSON for the comment-posting workflow
jq -n --argjson bot "$BOT_JSON" \
--arg pr '${{ github.event.pull_request.number }}' \
'$bot + {pr_number: $pr}' > bot-output.json
# Find existing bot comment id
EXISTING_ID=$(gh api \
repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments \
--jq "[.[] | select(.body | startswith(\"$MARKER\")) | .id] | first // empty")
if [ -n "$EXISTING_ID" ]; then
gh api --method PATCH \
repos/${{ github.repository }}/issues/comments/"$EXISTING_ID" \
-f body="$COMMENT_BODY"
else
gh pr comment ${{ github.event.pull_request.number }} \
--repo ${{ github.repository }} \
--body "$COMMENT_BODY"
fi
- name: Upload bot output
uses: actions/upload-artifact@v4
with:
name: bot-output
path: bot-output.json
retention-days: 1
gen-matrix:
name: "Generate test matrix"

View File

@@ -20,7 +20,7 @@ class curl
// which requires secur32.lib for SSL/TLS functions (SslEncryptPackage, etc.).
$extra_libs = getenv('SPC_EXTRA_LIBS') ?: '';
if (!str_contains($extra_libs, 'secur32.lib')) {
putenv('SPC_EXTRA_LIBS=' . trim($extra_libs . ' secur32.lib'));
putenv('SPC_EXTRA_LIBS=' . trim("{$extra_libs} secur32.lib"));
}
}
}

View File

@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
namespace Package\Extension;
use Package\Target\php;
use StaticPHP\Attribute\Package\BeforeStage;
use StaticPHP\Attribute\Package\Extension;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Util\FileSystem;
#[Extension('pdo_sqlite')]
class pdo_sqlite
{
#[BeforeStage('php', [php::class, 'configureForUnix'], 'ext-pdo_sqlite')]
public function patchBeforeConfigure(PackageInstaller $installer): void
{
FileSystem::replaceFileRegex(
"{$installer->getTargetPackage('php')->getSourceDir()}/configure",
'/sqlite3_column_table_name=yes/',
'sqlite3_column_table_name=no'
);
}
}

View File

@@ -50,8 +50,9 @@ class simdjson extends PhpExtensionPackage
if (!str_contains((string) $extra, '-lstdc++')) {
f_putenv('SPC_COMPILER_EXTRA=' . clean_spaces($extra . ' -lstdc++'));
}
$env['CFLAGS'] .= ' -Xclang -target-feature -Xclang +evex512';
$env['CXXFLAGS'] .= ' -Xclang -target-feature -Xclang +evex512';
$env['CFLAGS'] .= ' -fno-sanitize=undefined -Xclang -target-feature -Xclang +evex512';
$env['CXXFLAGS'] .= ' -fno-sanitize=undefined -Xclang -target-feature -Xclang +evex512';
}
return $env;
}
@@ -63,8 +64,8 @@ class simdjson extends PhpExtensionPackage
return;
}
$extra_cflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') ?: '';
GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=' . trim($extra_cflags . ' -Xclang -target-feature -Xclang +evex512'));
GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=' . trim($extra_cflags . ' -fno-sanitize=undefined -Xclang -target-feature -Xclang +evex512'));
$extra_cxxflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS') ?: '';
GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS=' . trim($extra_cxxflags . ' -Xclang -target-feature -Xclang +evex512'));
GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS=' . trim($extra_cxxflags . ' -fno-sanitize=undefined -Xclang -target-feature -Xclang +evex512'));
}
}

View File

@@ -30,6 +30,7 @@ class swoole extends PhpExtensionPackage
if ($installer->getPhpExtensionPackage('swoole-hook-odbc') && $installer->getPhpExtensionPackage('pdo_odbc')?->isBuildStatic()) {
throw new WrongUsageException('swoole-hook-odbc provides pdo_odbc, if you enable odbc hook for swoole, you must remove pdo_odbc extension.');
}
// swoole-hook-pgsql conflicts with pdo_pgsql
if ($installer->getPhpExtensionPackage('swoole-hook-pgsql') && $installer->getPhpExtensionPackage('pdo_pgsql')?->isBuildStatic()) {
throw new WrongUsageException('swoole-hook-pgsql provides pdo_pgsql, if you enable pgsql hook for swoole, you must remove pdo_pgsql extension.');

View File

@@ -20,6 +20,7 @@ class libde265 extends LibraryPackage
->addConfigureArgs(
'-DENABLE_SDL=OFF',
'-DENABLE_DECODER=OFF',
'-DENABLE_SIMD=OFF',
'-DHAVE_NEON=OFF',
)
->build();

View File

@@ -42,7 +42,7 @@ class openssl
$perl = ApplicationContext::get('perl');
$cmd = cmd()->cd($lib->getSourceDir())
->exec(
"{$perl} Configure zlib VC-WIN64A " .
escapeshellarg($perl) . ' Configure zlib VC-WIN64A ' .
'no-shared ' .
'--prefix=' . quote($lib->getBuildRootPath()) . ' ' .
'--with-zlib-lib=' . quote($lib->getLibDir()) . ' ' .

View File

@@ -19,7 +19,12 @@ class sqlite
#[BuildFor('Linux')]
public function buildUnix(LibraryPackage $lib): void
{
UnixAutoconfExecutor::create($lib)->configure()->make();
UnixAutoconfExecutor::create($lib)
->appendEnv([
'CFLAGS' => '-DSQLITE_ENABLE_COLUMN_METADATA=1',
])
->configure()
->make();
$lib->patchPkgconfPrefix(['sqlite3.pc']);
}

View File

@@ -107,7 +107,12 @@ class GenExtTestMatrixCommand extends BaseCommand
// Separate into regular and virtual extensions (build-static:false excluded globally)
$all_regular = [];
$all_virtual = [];
$all_libraries = [];
foreach ($all as $pkg_name => $config) {
if (($config['type'] ?? '') === 'library') {
$all_libraries[$pkg_name] = $config;
continue;
}
if (($config['type'] ?? '') !== 'php-extension') {
continue;
}
@@ -154,10 +159,7 @@ class GenExtTestMatrixCommand extends BaseCommand
$raw,
fn ($d) => isset($pool_set[$d]) && $d !== $pkg_name
));
$os_lib_deps[$this->displayName($pkg_name)] = array_values(array_filter(
$raw,
fn ($d) => !str_starts_with($d, 'ext-')
));
$os_lib_deps[$this->displayName($pkg_name)] = $this->collectLibraryDeps($raw, $all_libraries, $os);
}
$all_ext_lib_deps[$os] = $os_lib_deps;
@@ -246,22 +248,23 @@ class GenExtTestMatrixCommand extends BaseCommand
}
}
if (!empty($filter_extensions)) {
$entries = array_values(array_filter($entries, function (array $entry) use ($filter_extensions): bool {
if (!empty($filter_extensions) || !empty($filter_libs)) {
$entries = array_values(array_filter($entries, function (array $entry) use ($filter_extensions, $filter_libs, $all_ext_lib_deps): bool {
$names = explode(',', $entry['extension']);
return count(array_intersect($names, $filter_extensions)) > 0;
}));
}
if (!empty($filter_libs)) {
$entries = array_values(array_filter($entries, function (array $entry) use ($filter_libs, $all_ext_lib_deps): bool {
$names = explode(',', $entry['extension']);
$lib_deps = $all_ext_lib_deps[$entry['os']] ?? [];
foreach ($names as $name) {
if (count(array_intersect($lib_deps[$name] ?? [], $filter_libs)) > 0) {
return true;
if (!empty($filter_extensions) && count(array_intersect($names, $filter_extensions)) > 0) {
return true;
}
if (!empty($filter_libs)) {
$lib_deps = $all_ext_lib_deps[$entry['os']] ?? [];
foreach ($names as $name) {
if (count(array_intersect($lib_deps[$name] ?? [], $filter_libs)) > 0) {
return true;
}
}
}
return false;
}));
}
@@ -300,6 +303,41 @@ class GenExtTestMatrixCommand extends BaseCommand
return str_starts_with($pkg_name, 'ext-') ? substr($pkg_name, 4) : $pkg_name;
}
/**
* Collect direct and transitive library dependencies from a package dependency list.
*
* @param string[] $deps
* @param array<string, mixed[]> $library_configs
* @param array<string, true> $seen
*
* @return string[]
*/
private function collectLibraryDeps(array $deps, array $library_configs, string $platform, array $seen = []): array
{
$collected = [];
foreach ($deps as $dep) {
if (str_starts_with($dep, 'ext-') || isset($seen[$dep])) {
continue;
}
$seen[$dep] = true;
$collected[$dep] = $dep;
if (!isset($library_configs[$dep])) {
continue;
}
$child_deps = array_merge(
$this->resolvePlatformList($library_configs[$dep], 'depends', $platform),
$this->resolvePlatformList($library_configs[$dep], 'suggests', $platform),
);
foreach ($this->collectLibraryDeps($child_deps, $library_configs, $platform, $seen) as $child_dep) {
$collected[$child_dep] = $child_dep;
}
}
return array_values($collected);
}
/**
* Split orphans into batches such that no two conflicting extensions share a batch.
* Uses a greedy graph-coloring approach.

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
$pdo = new PDO('sqlite::memory:');
$pdo->exec('CREATE TABLE spc_column_metadata_test (id INTEGER)');
$stmt = $pdo->query('SELECT id FROM spc_column_metadata_test');
if ($stmt === false) {
throw new RuntimeException('Failed to query SQLite metadata test table.');
}
$metadata = $stmt->getColumnMeta(0);
if (($metadata['table'] ?? null) !== 'spc_column_metadata_test') {
throw new RuntimeException('PDO SQLite column metadata does not include the origin table.');
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
$sqlite = new SQLite3(':memory:');
$enabled = $sqlite->querySingle("SELECT sqlite_compileoption_used('ENABLE_COLUMN_METADATA')");
if ((int) $enabled !== 1) {
throw new RuntimeException('SQLite was not built with SQLITE_ENABLE_COLUMN_METADATA.');
}

View File

@@ -176,6 +176,31 @@ class GenExtTestMatrixCommandTest extends TestCase
}
}
/**
* --for-libs must include extensions that depend on the library through other libraries.
*/
public function testForLibsFilterIncludesTransitiveLibraryDeps(): void
{
$matrix = $this->runMatrix(['--os' => 'Linux', '--for-libs' => 'libde265']);
$this->assertNotEmpty($matrix, '--for-libs=libde265 must yield at least one entry');
foreach ($matrix as $entry) {
$parts = explode(',', $entry['extension']);
$this->assertContains('imagick', $parts, "Entry {$entry['extension']} should not appear in --for-libs=libde265 results");
}
}
/**
* Multiple filters should include entries matching any changed package.
*/
public function testExtensionAndLibraryFiltersAreCombinedAsUnion(): void
{
$matrix = $this->runMatrix(['--os' => 'Linux', '--for-extensions' => 'simdjson', '--for-libs' => 'libde265']);
$this->assertNotEmpty($this->findEntriesContaining($matrix, 'simdjson'), 'simdjson entry must be included');
$this->assertNotEmpty($this->findEntriesContaining($matrix, 'imagick'), 'imagick entry must be included through libde265');
}
/**
* --tier2 must produce only Tier2 runners and no Windows entries.
*/
@@ -258,14 +283,17 @@ class GenExtTestMatrixCommandTest extends TestCase
* - ext-swoole-hook-* virtual (arg-type: none) — must be bundled with swoole
* - ext-curl simple orphan, depended on by swoole but must NOT be pulled into swoole entry
* - ext-redis simple orphan
* - ext-simdjson simple orphan used for combined filter tests
* - ext-xml depends on lib 'libxml2'
* - ext-dom depends on ext-xml (DFS chain)
* - ext-imagick depends on imagemagick -> libheif -> libde265
* - ext-linux-only restricted to Linux via os: [Linux]
*/
private static function buildFixture(): array
{
// php-extension must be a non-empty assoc array ([] fails is_assoc_array() check).
$ext = static fn (array $phpExt = ['arg-type' => 'standard'], array $topLevel = []): array => array_merge(['type' => 'php-extension', 'php-extension' => $phpExt], $topLevel);
$lib = static fn (array $topLevel = []): array => array_merge(['type' => 'library', 'artifact' => ['source' => 'custom']], $topLevel);
return [
// Isolated standalones
@@ -279,11 +307,18 @@ class GenExtTestMatrixCommandTest extends TestCase
// Simple orphans
'ext-curl' => $ext(),
'ext-redis' => $ext(),
'ext-simdjson' => $ext(),
// DFS chain: dom depends on xml; xml depends on lib 'libxml2'
'ext-xml' => $ext(['arg-type' => 'standard'], ['depends' => ['libxml2']]),
'ext-dom' => $ext(['arg-type' => 'standard'], ['depends' => ['ext-xml']]),
// Transitive library chain: imagick -> imagemagick -> libheif -> libde265
'ext-imagick' => $ext(['arg-type' => 'standard'], ['depends' => ['imagemagick']]),
'imagemagick' => $lib(['depends' => ['libheif']]),
'libheif' => $lib(['depends' => ['libde265']]),
'libde265' => $lib(),
// OS-restricted to Linux only
'ext-linux-only' => $ext(['os' => ['Linux']]),
];