Skip to content

Commit 528358e

Browse files
ondrejmirtesclaude
andcommitted
Generate the expected turbo extension version on autoloader dump
TurboExtensionEnabler no longer carries the expected version as a hand-maintained constant. build/generate-turbo-manifest.php writes it into vendor/turbo-extension-version.php from the same git computation the extension's Makefile bakes into the binary (the short SHA of the last commit touching turbo-ext/src), so a binary built from a checkout matches that checkout by construction. The phar compilation dumps the autoloader too and packs the file, so the phar carries the version of the commit it was compiled from. `make build-turbo` regenerates the file, so a rebuild after a turbo-ext/src commit needs no extra dump. The `bump-turbo` target, the follow-up "Bump expected turbo version" commits and the CI check of the constant are gone; the workflows compare the built binaries against `php build/turbo-extension-version.php`, and the compiler job checks the version packed in the phar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013qRnM7saDyd5yKXxJAnR7o
1 parent a9d6b5e commit 528358e

17 files changed

Lines changed: 245 additions & 136 deletions

‎.github/workflows/phar.yml‎

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,16 @@ jobs:
120120
working-directory: "compiler"
121121
run: "../tmp/phpstan.phar list"
122122

123+
# The autoloader dump generates the version TurboExtensionEnabler
124+
# activates from git; without it in the phar, no turbo binary would
125+
# ever activate.
126+
- name: "Check the PHAR carries the expected turbo extension version"
127+
run: |
128+
BAKED="$(php -r 'echo require "phar://" . getcwd() . "/tmp/phpstan.phar/vendor/turbo-extension-version.php";')"
129+
EXPECTED="$(php build/turbo-extension-version.php)"
130+
echo "PHAR expects: $BAKED, last commit touching turbo-ext/src: $EXPECTED"
131+
[ "$BAKED" = "$EXPECTED" ]
132+
123133
- name: "Delete PHAR"
124134
run: "rm tmp/phpstan.phar"
125135

@@ -244,7 +254,7 @@ jobs:
244254
echo "composer.lock: $INSTALLED, native engine ported against: $SUPPORTED_PHP_PARSER_VERSION"
245255
if [ "$INSTALLED" != "$SUPPORTED_PHP_PARSER_VERSION" ]; then
246256
echo "::error::nikic/php-parser was updated to $INSTALLED but turbo-ext/src/parser/ was ported against $SUPPORTED_PHP_PARSER_VERSION."
247-
echo "::error::Update procedure: diff ParserAbstract.php helpers between the two versions (grammar tables need nothing — they are read at run time), run turbo-ext/bin/generate-parser-actions.php and port any flagged closure bodies via action-overrides/, run turbo-ext/tests/parser-corpus.php and turbo-ext/tests/parser-upstream-corpus.php until byte-identical, run the full suite, then bump SUPPORTED_PHP_PARSER_VERSION here and the extension version pin."
257+
echo "::error::Update procedure: diff ParserAbstract.php helpers between the two versions (grammar tables need nothing — they are read at run time), run turbo-ext/bin/generate-parser-actions.php and port any flagged closure bodies via action-overrides/, run turbo-ext/tests/parser-corpus.php and turbo-ext/tests/parser-upstream-corpus.php until byte-identical, run the full suite, then bump SUPPORTED_PHP_PARSER_VERSION here."
248258
exit 1
249259
fi
250260
@@ -253,17 +263,8 @@ jobs:
253263
php turbo-ext/bin/generate-parser-actions.php
254264
git diff --exit-code turbo-ext/src/parser/
255265
256-
- name: "Check TurboExtensionEnabler::EXPECTED_EXTENSION_VERSION against the last commit touching turbo-ext/src"
266+
- name: "Check turbo-ext/VERSION.txt is not committed"
257267
run: |
258-
EXPECTED_SHA="$(git log -1 --format=%H -- turbo-ext/src | cut -c1-7)"
259-
ENABLER="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)"
260-
echo "last commit touching turbo-ext/src: $EXPECTED_SHA"
261-
echo "TurboExtensionEnabler::EXPECTED_EXTENSION_VERSION: $ENABLER"
262-
if [ "$ENABLER" != "$EXPECTED_SHA" ]; then
263-
echo "::error::TurboExtensionEnabler::EXPECTED_EXTENSION_VERSION must be the short SHA of the last commit touching turbo-ext/src/."
264-
echo "::error::After changing the native side, verify the implementations still match and add a follow-up commit setting the constant to that commit's short SHA (the binary bakes its version from git at build time). Compute it only after the change lands on the target branch — a pull request commit gets a new SHA when rebased."
265-
exit 1
266-
fi
267268
# VERSION.txt is generated into the phpstan/turbo-ext subsplit by
268269
# subsplit-turbo-ext.yml; a copy committed here would go stale and
269270
# silently win over git in phpize builds.
@@ -328,7 +329,7 @@ jobs:
328329
- name: "Verify the built extension reports the expected version"
329330
run: |
330331
REPORTED="$(php -d extension="$RUNNER_TEMP/turbo-ext/modules/phpstan_turbo.so" -r 'echo phpversion("phpstan_turbo");')"
331-
EXPECTED="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)"
332+
EXPECTED="$(php build/turbo-extension-version.php)"
332333
echo "built extension reports: $REPORTED, enabler expects: $EXPECTED"
333334
[ "$REPORTED" = "$EXPECTED" ]
334335
@@ -646,7 +647,7 @@ jobs:
646647
if: env.ORIGIN_RUN_ID == ''
647648
run: |
648649
REPORTED="$(php -d extension="$PWD/turbo-ext/phpstan_turbo.so" -r 'echo phpversion("phpstan_turbo");')"
649-
EXPECTED="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)"
650+
EXPECTED="$(php build/turbo-extension-version.php)"
650651
echo "core + extension report: $REPORTED, enabler expects: $EXPECTED"
651652
[ "$REPORTED" = "$EXPECTED" ]
652653
@@ -759,7 +760,7 @@ jobs:
759760
run: |
760761
docker exec -i alpine-build sh -e <<'EOF'
761762
REPORTED="$(php -d extension="$PWD/turbo-ext/phpstan_turbo.so" -r 'echo phpversion("phpstan_turbo");')"
762-
EXPECTED="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)"
763+
EXPECTED="$(php build/turbo-extension-version.php)"
763764
echo "core + extension report: $REPORTED, enabler expects: $EXPECTED"
764765
[ "$REPORTED" = "$EXPECTED" ]
765766
EOF
@@ -954,7 +955,7 @@ jobs:
954955
- name: "Verify the built extension reports the expected version"
955956
run: |
956957
REPORTED="$(php -d extension="$PWD/turbo-ext/phpstan_turbo.so" -r 'echo phpversion("phpstan_turbo");')"
957-
EXPECTED="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)"
958+
EXPECTED="$(php build/turbo-extension-version.php)"
958959
echo "built extension reports: $REPORTED, enabler expects: $EXPECTED"
959960
[ "$REPORTED" = "$EXPECTED" ]
960961
# the loader rejects a ts-mismatched binary, so php loading it above
@@ -1070,7 +1071,7 @@ jobs:
10701071
run: |
10711072
docker exec -i alpine-build sh -e <<'EOF'
10721073
REPORTED="$(php -d extension="$PWD/turbo-ext/phpstan_turbo.so" -r 'echo phpversion("phpstan_turbo");')"
1073-
EXPECTED="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)"
1074+
EXPECTED="$(php build/turbo-extension-version.php)"
10741075
echo "built extension reports: $REPORTED, enabler expects: $EXPECTED"
10751076
[ "$REPORTED" = "$EXPECTED" ]
10761077
[ "$(php -r 'echo (int) ((bool) PHP_ZTS);')" = "$PHP_ZTS" ]
@@ -1669,7 +1670,7 @@ jobs:
16691670
cp "turbo-ext/$CORE" ext-out/
16701671
fi
16711672
REPORTED="$(php -d extension="$(cygpath -w "$PWD/ext-out/php_phpstan_turbo.dll")" -r 'echo phpversion("phpstan_turbo");')"
1672-
EXPECTED="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)"
1673+
EXPECTED="$(php build/turbo-extension-version.php)"
16731674
echo "built extension reports: $REPORTED, enabler expects: $EXPECTED"
16741675
[ "$REPORTED" = "$EXPECTED" ]
16751676
# the loader rejects a ts-mismatched DLL, so php loading it above
@@ -1980,7 +1981,7 @@ jobs:
19801981
MATRIX_TS: ${{ matrix.ts }}
19811982
run: |
19821983
REPORTED="$(php -d extension="$TURBO_DLL" -r 'echo phpversion("phpstan_turbo");')"
1983-
EXPECTED="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)"
1984+
EXPECTED="$(php build/turbo-extension-version.php)"
19841985
echo "extension reports: $REPORTED, enabler expects: $EXPECTED"
19851986
[ "$REPORTED" = "$EXPECTED" ]
19861987
# the loader rejects a ts-mismatched binary, so php loading it above
@@ -2090,7 +2091,7 @@ jobs:
20902091
run: |
20912092
docker exec -i alpine-test sh -e <<'EOF'
20922093
REPORTED="$(php -d extension=/work/turbo-ext/phpstan_turbo.so -r 'echo phpversion("phpstan_turbo");')"
2093-
EXPECTED="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)"
2094+
EXPECTED="$(php build/turbo-extension-version.php)"
20942095
echo "extension reports: $REPORTED, enabler expects: $EXPECTED"
20952096
[ "$REPORTED" = "$EXPECTED" ]
20962097
[ "$(php -r 'echo (int) ((bool) PHP_ZTS);')" = "$PHP_ZTS" ]
@@ -2644,11 +2645,11 @@ jobs:
26442645
commit:
26452646
name: "Commit PHAR"
26462647
if: "github.repository_owner == 'phpstan' && (github.ref == 'refs/heads/2.3.x' || startsWith(github.ref, 'refs/tags/'))"
2647-
# Gated on every job that produces the committed bits or the version
2648-
# constant they are staged by: the compile jobs build the staged
2649-
# binaries (or carry over ones compiled from identical build inputs,
2650-
# see turbo-origins) and check the version they report; turbo-version
2651-
# guards the version constant the staging logic compares. The jobs
2648+
# Gated on every job that produces the committed bits or checks them
2649+
# against their sources: the compile jobs build the staged binaries (or
2650+
# carry over ones compiled from identical build inputs, see
2651+
# turbo-origins) and check the version they report; turbo-version
2652+
# guards the parity of the native and PHP implementations. The jobs
26522653
# that only test those bits are deliberately not gates: the
26532654
# "Run with Turbo Extension" jobs (turbo-differential,
26542655
# turbo-differential-musl, turbo-run, turbo-docker-run), the shared-core
@@ -2747,7 +2748,7 @@ jobs:
27472748
- name: "Check turbo extension version and binary set"
27482749
id: turbo-difference
27492750
run: |
2750-
EXPECTED="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" phpstan-src/src/Turbo/TurboExtensionEnabler.php)"
2751+
EXPECTED="$(php phpstan-src/build/turbo-extension-version.php)"
27512752
CURRENT="$(cat phpstan-dist/turbo-ext/.version 2>/dev/null || echo none)"
27522753
echo "enabler expects: $EXPECTED, dist carries: $CURRENT"
27532754
RESULT=same

‎.typos.toml‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,6 @@ extend-exclude = [
44
]
55
ignore-hidden = false
66

7-
[default]
8-
# TurboExtensionEnabler::EXPECTED_EXTENSION_VERSION is a git short SHA, not a
9-
# word. It changes on every turbo-ext/src commit, and a hex SHA occasionally
10-
# spells a dictionary word fragment - '873ede9' reads as "ede" -> "edge".
11-
extend-ignore-re = ["EXPECTED_EXTENSION_VERSION = '[0-9a-f]+'"]
12-
137
[default.extend-identifiers]
148
# Known typos
159
NonRemoveableTypeTrait = "NonRemoveableTypeTrait"

‎CLAUDE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ Every threshold, fan-out cap, recursion bound, or collection-size limit must be
262262

263263
### Classes marked with `#[ShadowedByTurboExtension]` have a native C++ mirror
264264

265-
Classes carrying the `PHPStan\Turbo\ShadowedByTurboExtension` attribute are reimplemented natively by the phpstan_turbo extension; the attribute names the `.cpp` file implementing it (`build/generate-turbo-manifest.php` derives the full map into `vendor/turbo-shadowed-classes.json` on composer dump-autoload). The two implementations must behave identically — analysis output is bit-for-bit identical with the extension on or off. When editing such a class, always check whether the `.cpp` mirror in `turbo-ext/` needs the same change; if it does, port it following `turbo-ext/CLAUDE.md` (method parity check, differential smoke test, full test suite with the extension loaded). Any commit touching `turbo-ext/src/` requires the follow-up commit bumping `TurboExtensionEnabler::EXPECTED_EXTENSION_VERSION` to that commit's short SHA — run `make bump-turbo`.
265+
Classes carrying the `PHPStan\Turbo\ShadowedByTurboExtension` attribute are reimplemented natively by the phpstan_turbo extension; the attribute names the `.cpp` file implementing it (`build/generate-turbo-manifest.php` derives the full map into `vendor/turbo-shadowed-classes.json` on composer dump-autoload). The two implementations must behave identically — analysis output is bit-for-bit identical with the extension on or off. When editing such a class, always check whether the `.cpp` mirror in `turbo-ext/` needs the same change; if it does, port it following `turbo-ext/CLAUDE.md` (method parity check, differential smoke test, full test suite with the extension loaded). The extension version is the short SHA of the last commit touching `turbo-ext/src/`, baked into the binary at build time and written into `vendor/turbo-extension-version.php` on composer dump-autoload (and by `make build-turbo`) — there is nothing to bump.
266266

267267
New introduced classes in these categories should be shadowed with a native C++ mirror in the same commit:
268268

‎Makefile‎

Lines changed: 9 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -232,49 +232,22 @@ TURBO_JOBS ?= $(shell getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)
232232
build-turbo:
233233
$(MAKE) -C turbo-ext -j$(TURBO_JOBS)
234234
@# The binary bakes its version from `git log -- turbo-ext/src`, and
235-
@# TurboExtensionEnabler activates only the version it expects — a build
236-
@# from a shallow clone ("dev"), or one made between a turbo-ext/src
237-
@# commit and its `make bump-turbo`, loads but stays inactive, which
238-
@# otherwise only shows up as "nothing got faster".
235+
@# TurboExtensionEnabler activates only the version the last autoloader
236+
@# dump wrote into vendor/turbo-extension-version.php from the same git
237+
@# log — regenerate it, so that a build after a turbo-ext/src commit is
238+
@# expected without another `composer dump-autoload`.
239+
@php build/generate-turbo-manifest.php > /dev/null
240+
@# A build from a checkout without git history ("dev") still loads but
241+
@# stays inactive, which otherwise only shows up as "nothing got faster".
239242
@# -n: the machine's php.ini usually loads the extension already, and
240243
@# the "Module is already loaded" warning that a second -d then prints
241244
@# goes to stdout, i.e. into the version being read. An ini-less probe
242245
@# also guarantees the version reported is the binary just built.
243246
@BUILT="$$(php -n -d display_errors=stderr -d extension="$(CURDIR)/turbo-ext/phpstan_turbo.so" -r 'echo phpversion("phpstan_turbo");' 2>/dev/null)"; \
244-
EXPECTED="$$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)"; \
247+
EXPECTED="$$(php -r 'echo require "vendor/turbo-extension-version.php";')"; \
245248
echo "built $(CURDIR)/turbo-ext/phpstan_turbo.so$${BUILT:+ (version $$BUILT)}"; \
246249
if [ -n "$$BUILT" ] && [ "$$BUILT" != "$$EXPECTED" ]; then \
247-
echo "warning: TurboExtensionEnabler expects $$EXPECTED, so this build would load but stay inactive (run 'make bump-turbo' once the turbo-ext/src commit is in)"; \
248-
fi
249-
250-
# The expected turbo version is the short SHA of the last commit touching
251-
# turbo-ext/src (enforced by the phar.yml turbo-version job), so the bump can
252-
# never be part of the commit it points at. Refreshes an unpushed bump commit
253-
# in place — after a rebase gives the watched commit a new SHA, rerunning
254-
# this amends instead of stacking a second bump commit.
255-
.PHONY: bump-turbo
256-
bump-turbo:
257-
@if [ -n "$$(git status --porcelain -- turbo-ext/src)" ]; then \
258-
echo "turbo-ext/src has uncommitted changes — commit them first, the expected version is the short SHA of the last commit touching turbo-ext/src"; \
259-
exit 1; \
260-
fi; \
261-
if ! git diff --quiet HEAD -- src/Turbo/TurboExtensionEnabler.php; then \
262-
echo "src/Turbo/TurboExtensionEnabler.php has uncommitted changes — commit or stash them first"; \
263-
exit 1; \
264-
fi; \
265-
EXPECTED="$$(git log -1 --format=%H -- turbo-ext/src | cut -c1-7)"; \
266-
CURRENT="$$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)"; \
267-
if [ "$$EXPECTED" = "$$CURRENT" ]; then \
268-
echo "TurboExtensionEnabler::EXPECTED_EXTENSION_VERSION is already $$EXPECTED"; \
269-
exit 0; \
270-
fi; \
271-
sed -i '' -e "s/EXPECTED_EXTENSION_VERSION = '$$CURRENT'/EXPECTED_EXTENSION_VERSION = '$$EXPECTED'/" src/Turbo/TurboExtensionEnabler.php; \
272-
if [ "$$(git log -1 --format=%s)" = "Bump expected turbo version" ] && [ -z "$$(git branch -r --contains HEAD)" ]; then \
273-
git commit --amend --no-edit -- src/Turbo/TurboExtensionEnabler.php; \
274-
echo "amended the unpushed bump commit: $$CURRENT -> $$EXPECTED"; \
275-
else \
276-
git commit -m "Bump expected turbo version" -- src/Turbo/TurboExtensionEnabler.php; \
277-
echo "bumped: $$CURRENT -> $$EXPECTED"; \
250+
echo "warning: TurboExtensionEnabler expects $${EXPECTED:-no version (no git history)}, so this build would load but stay inactive"; \
278251
fi
279252

280253
# Static analysis of the native extension's hand-written C++ sources. The
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Build;
4+
5+
use function fclose;
6+
use function is_resource;
7+
use function preg_match;
8+
use function proc_close;
9+
use function proc_open;
10+
use function sprintf;
11+
use function stream_get_contents;
12+
use function substr;
13+
use function trim;
14+
use function var_export;
15+
16+
/**
17+
* The phpstan_turbo version this checkout of PHPStan expects: the short SHA
18+
* of the last commit touching turbo-ext/src — the same computation the
19+
* extension's Makefile bakes into the binary, so a binary built from this
20+
* checkout matches by construction. build/generate-turbo-manifest.php writes
21+
* it into vendor/turbo-extension-version.php on every autoloader dump (the
22+
* phar carries that file too); build/turbo-extension-version.php prints it
23+
* for the workflows.
24+
*/
25+
final class TurboExtensionVersion
26+
{
27+
28+
/**
29+
* Null without a usable git checkout — the enabler then expects nothing,
30+
* and the extension stays inactive.
31+
*/
32+
public static function resolve(string $root): ?string
33+
{
34+
// an argument list rather than a command line: no shell, so no
35+
// quoting and no /dev/null that would not exist on Windows
36+
$process = proc_open(
37+
['git', '-C', $root, 'log', '-1', '--format=%H', '--', 'turbo-ext/src'],
38+
[1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
39+
$pipes,
40+
);
41+
if (!is_resource($process)) {
42+
return null;
43+
}
44+
45+
$output = stream_get_contents($pipes[1]);
46+
fclose($pipes[1]);
47+
fclose($pipes[2]);
48+
if (proc_close($process) !== 0 || $output === false) {
49+
return null;
50+
}
51+
52+
$sha = trim($output);
53+
if (preg_match('~^[0-9a-f]{40,}$~', $sha) !== 1) {
54+
return null;
55+
}
56+
57+
return substr($sha, 0, 7);
58+
}
59+
60+
public static function render(?string $version): string
61+
{
62+
return sprintf(
63+
<<<'PHP'
64+
<?php declare(strict_types = 1);
65+
66+
// turbo-extension-version.php @generated by build/generate-turbo-manifest.php
67+
// — do not edit. The phpstan_turbo version PHPStan\Turbo\TurboExtensionEnabler
68+
// activates: the short SHA of the last commit touching turbo-ext/src (null
69+
// without a git checkout to read it from).
70+
71+
return %s;
72+
73+
PHP,
74+
var_export($version, true),
75+
);
76+
}
77+
78+
}

0 commit comments

Comments
 (0)