port: advance MSVC600 CPython compatibility frontier

This commit is contained in:
RayPals
2026-08-22 17:36:38 +00:00
parent a7a9560e5c
commit 824b4f0d24
15 changed files with 6150 additions and 41 deletions
+19 -8
View File
@@ -7,15 +7,16 @@
* three distinct patterns:
*
* 1. `status = _PyStatus_OK();` -- a compound-literal macro used as a
* plain assignment's right-hand side. Fixed by calling the existing
* public API function PyStatus_Ok() instead, which
* Python/initconfig.c defines as `return _PyStatus_OK();` -- the
* identical value, no header change needed.
* 2. `cf = _PyCompilerFlags_INIT;` -- likewise a compound-literal macro
* plain assignment's right-hand side. The shared macro now expands to
* a VC6-only internal helper that assigns PyStatus fields explicitly,
* while newer compilers keep the original compound-literal form.
* 2. `status = _PyStatus_ERR("...");` -- same compound-literal macro
* pattern for ERROR statuses, now using a VC6-only explicit helper.
* 3. `cf = _PyCompilerFlags_INIT;` -- likewise a compound-literal macro
* used as a plain assignment. Fixed by assigning PyCompilerFlags's
* two fields explicitly (cf_flags, cf_feature_version), matching the
* macro's own definition field-for-field.
* 3. `_PyArgv args = { .argc = argc, ... };` -- a designated-initializer
* 4. `_PyArgv args = { .argc = argc, ... };` -- a designated-initializer
* aggregate initializer with no macro involved. Fixed by declaring
* the variable bare and assigning each field explicitly; field order
* does not matter for plain assignment, unlike an initializer list.
@@ -35,8 +36,18 @@ vc6_status_ok_smoke(void)
{
PyStatus status;
/* pattern 1: was `status = _PyStatus_OK();` */
status = PyStatus_Ok();
/* pattern 1: VC6-only _PyStatus_OK() macro fallback helper. */
status = _PyStatus_OK();
return status;
}
PyStatus
vc6_status_error_smoke(void)
{
PyStatus status;
/* pattern 2: VC6-only _PyStatus_ERR() macro fallback helper. */
status = _PyStatus_ERR("vc6 status error smoke");
return status;
}
+59
View File
@@ -0,0 +1,59 @@
/* Focused compile smoke test for compat/msvc600/pathcch.h.
*
* The VC6 compatibility header intentionally implements only the single
* PathCch API used by cpython/Modules/getpath.c: PathCchFindExtension().
* This fixture keeps the representative path cases in one place for the
* VC6 probe. The probe is compile-only, so the checks are ordinary C code
* that can also be run later by a tiny Windows harness if needed.
*/
#include <windows.h>
#include <pathcch.h>
#ifndef ARRAYSIZE
#define ARRAYSIZE(a) (sizeof(a) / sizeof((a)[0]))
#endif
static int
pathcch_expect_ext(PCWSTR path, size_t cch, size_t offset)
{
PCWSTR ext;
HRESULT hr;
ext = NULL;
hr = PathCchFindExtension(path, cch, &ext);
return SUCCEEDED(hr) && ext == path + offset;
}
static int
pathcch_expect_invalid(PCWSTR path, size_t cch)
{
PCWSTR ext;
HRESULT hr;
ext = (PCWSTR)1;
hr = PathCchFindExtension(path, cch, &ext);
return FAILED(hr);
}
int
vc6_pathcch_find_extension_smoke(void)
{
static const WCHAR exe_path[] = L"C:\\Python311\\python.exe";
static const WCHAR no_ext[] = L"C:\\Python311\\python";
static const WCHAR dir_dot[] = L"C:\\tmp.dir\\python";
static const WCHAR dotfile[] = L"C:\\Python311\\.pythonrc";
static const WCHAR trailing_dir[] = L"C:\\bin\\python.exe\\";
static const WCHAR multi_dot[] = L"C:\\tmp\\archive.tar.gz";
int ok;
ok = 1;
ok = ok && pathcch_expect_ext(exe_path, ARRAYSIZE(exe_path), 20); /* .exe */
ok = ok && pathcch_expect_ext(no_ext, ARRAYSIZE(no_ext), 19); /* NUL */
ok = ok && pathcch_expect_ext(dir_dot, ARRAYSIZE(dir_dot), 17); /* NUL, directory dot ignored */
ok = ok && pathcch_expect_ext(dotfile, ARRAYSIZE(dotfile), 13); /* dotfile extension */
ok = ok && pathcch_expect_ext(trailing_dir, ARRAYSIZE(trailing_dir), 18); /* NUL after separator */
ok = ok && pathcch_expect_ext(multi_dot, ARRAYSIZE(multi_dot), 18); /* last suffix */
ok = ok && pathcch_expect_invalid(exe_path, 4); /* too small: no NUL */
ok = ok && pathcch_expect_invalid(NULL, ARRAYSIZE(exe_path));
return ok ? 0 : 1;
}
+76
View File
@@ -0,0 +1,76 @@
/* Focused compile smoke test for compat/msvc600/stdint.h.
*
* The VC6 compatibility header forwards to the existing
* compat/msvc600/inttypes.h typedefs. This fixture exercises exactly the
* subset that cpython/Modules/_blake2/impl/blake2-impl.h needs: uint8_t,
* uint32_t, and uint64_t, in the same load/store/rotate shapes that file
* uses, plus a compile-time width check for each. It also includes
* <inttypes.h> alongside <stdint.h> in the same translation unit, the way
* a Python.h + blake2.h translation unit would, to guard against the
* duplicate-typedef failure mode the stdint.h header is designed to avoid.
*/
#include <stddef.h>
#include <stdint.h>
#include <inttypes.h>
#include <string.h>
typedef char vc6_stdint_check_uint8_is_1_byte[(sizeof(uint8_t) == 1) ? 1 : -1];
typedef char vc6_stdint_check_uint32_is_4_bytes[(sizeof(uint32_t) == 4) ? 1 : -1];
typedef char vc6_stdint_check_uint64_is_8_bytes[(sizeof(uint64_t) == 8) ? 1 : -1];
static uint32_t
vc6_stdint_load32(const void *src)
{
const uint8_t *p = (const uint8_t *)src;
uint32_t w = *p++;
w |= ((uint32_t)(*p++)) << 8;
w |= ((uint32_t)(*p++)) << 16;
w |= ((uint32_t)(*p++)) << 24;
return w;
}
static uint64_t
vc6_stdint_load64(const void *src)
{
const uint8_t *p = (const uint8_t *)src;
uint64_t w = *p++;
w |= ((uint64_t)(*p++)) << 8;
w |= ((uint64_t)(*p++)) << 16;
w |= ((uint64_t)(*p++)) << 24;
w |= ((uint64_t)(*p++)) << 32;
w |= ((uint64_t)(*p++)) << 40;
w |= ((uint64_t)(*p++)) << 48;
w |= ((uint64_t)(*p++)) << 56;
return w;
}
static uint32_t
vc6_stdint_rotl32(const uint32_t w, const unsigned c)
{
return (w << c) | (w >> (32 - c));
}
static uint64_t
vc6_stdint_rotl64(const uint64_t w, const unsigned c)
{
return (w << c) | (w >> (64 - c));
}
int
vc6_stdint_smoke(void)
{
static const uint8_t bytes32[4] = { 0x01, 0x02, 0x03, 0x04 };
static const uint8_t bytes64[8] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 };
uint32_t w32;
uint64_t w64;
int ok;
w32 = vc6_stdint_load32(bytes32);
w64 = vc6_stdint_load64(bytes64);
ok = 1;
ok = ok && (w32 == 0x04030201UL);
ok = ok && (vc6_stdint_rotl32(w32, 8) == ((w32 << 8) | (w32 >> 24)));
ok = ok && (vc6_stdint_rotl64(w64, 8) == ((w64 << 8) | (w64 >> 56)));
return ok ? 0 : 1;
}
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
#
# Generate the CPython frozen getpath.py header into ignored build output.
#
# CPython's build documents this target in Makefile.pre.in:
# Python/frozen_modules/getpath.h: Modules/getpath.py ...
# Programs/_freeze_module.py getpath Modules/getpath.py Python/frozen_modules/getpath.h
#
# This script intentionally writes under build/generated/cpython/... so the
# repository does not track generated bytecode headers or vendor files.
#
# Usage:
# scripts/generate-getpath-frozen.sh [output-root]
#
# Output:
# <output-root>/cpython/Python/frozen_modules/getpath.h
# default output-root: build/generated
set -u
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CPYTHON_ROOT="$REPO_ROOT/cpython"
OUTPUT_ROOT="${1:-$REPO_ROOT/build/generated}"
OUT_DIR="$OUTPUT_ROOT/cpython/Python/frozen_modules"
OUT_HEADER="$OUT_DIR/getpath.h"
FREEZE_SCRIPT="$CPYTHON_ROOT/Programs/_freeze_module.py"
INPUT="$CPYTHON_ROOT/Modules/getpath.py"
usage() {
sed -n '11,19p' "$REPO_ROOT/scripts/generate-getpath-frozen.sh"
}
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
usage
exit 0
fi
for required in "$CPYTHON_ROOT" "$FREEZE_SCRIPT" "$INPUT"; do
if [ ! -e "$required" ]; then
echo "generate-getpath-frozen: missing prerequisite: $required" >&2
exit 2
fi
done
if ! command -v python3 >/dev/null 2>&1; then
echo "generate-getpath-frozen: missing prerequisite: python3" >&2
exit 2
fi
mkdir -p "$OUT_DIR"
python3 "$FREEZE_SCRIPT" getpath "$INPUT" "$OUT_HEADER"
status=$?
if [ "$status" -ne 0 ]; then
echo "generate-getpath-frozen: _freeze_module.py failed with exit $status" >&2
exit "$status"
fi
if [ ! -s "$OUT_HEADER" ]; then
echo "generate-getpath-frozen: expected nonempty output missing: $OUT_HEADER" >&2
exit 2
fi
printf '%s\n' "${OUT_HEADER#$REPO_ROOT/}"
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env bash
#
# Regression tests for reproducible getpath.py frozen-header generation.
set -u
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
GENERATOR="$REPO_ROOT/scripts/generate-getpath-frozen.sh"
TMP_ROOT="$REPO_ROOT/build/test-getpath-frozen"
OUT_HEADER="$TMP_ROOT/cpython/Python/frozen_modules/getpath.h"
DIRECT_HEADER="$TMP_ROOT/direct/getpath.h"
fail=0
pass() { echo "ok - $1"; }
bad() { echo "not ok - $1"; fail=1; }
rm -rf "$TMP_ROOT"
out="$($GENERATOR "$TMP_ROOT" 2>&1)"
status=$?
if [ "$status" -eq 0 ]; then
pass "generator exits 0"
else
bad "generator exits 0 (got exit $status)"
echo "$out"
fi
if [ "$out" = "build/test-getpath-frozen/cpython/Python/frozen_modules/getpath.h" ]; then
pass "generator reports the project-local build-relative output path"
else
bad "generator reports the project-local build-relative output path (got: $out)"
fi
if [ -s "$OUT_HEADER" ]; then
pass "generator writes a nonempty getpath.h under build/"
else
bad "generator writes a nonempty getpath.h under build/"
fi
first_line="$(sed -n '1p' "$OUT_HEADER" 2>/dev/null)"
if [ "$first_line" = '/* Auto-generated by Programs/_freeze_module.py */' ]; then
pass "generated header records the host-Python _freeze_module.py path"
else
bad "generated header records the host-Python _freeze_module.py path (got: $first_line)"
fi
second_line="$(sed -n '2p' "$OUT_HEADER" 2>/dev/null)"
if [ "$second_line" = 'const unsigned char _Py_M__getpath[] = {' ]; then
pass "generated header declares the expected getpath frozen symbol"
else
bad "generated header declares the expected getpath frozen symbol (got: $second_line)"
fi
python3 - "$OUT_HEADER" <<'PY'
import marshal
import re
import sys
from pathlib import Path
path = Path(sys.argv[1])
text = path.read_text()
if not text.endswith('};\n'):
raise SystemExit('header does not end with closing array marker')
match = re.search(r'const unsigned char _Py_M__getpath\[\] = \{\n(?P<body>.*?)\};\n\Z', text, re.S)
if not match:
raise SystemExit('array body not found')
lines = match.group('body').splitlines()
values = []
for index, line in enumerate(lines, 1):
if not line.startswith(' '):
raise SystemExit(f'line {index} is not four-space indented')
payload = line.strip()
if not payload.endswith(','):
raise SystemExit(f'line {index} is missing trailing comma')
nums = [part for part in payload[:-1].split(',') if part]
if len(nums) > 16:
raise SystemExit(f'line {index} has more than 16 bytes')
if index < len(lines) and len(nums) != 16:
raise SystemExit(f'non-final line {index} has {len(nums)} bytes, expected 16')
for num in nums:
value = int(num)
if not 0 <= value <= 255:
raise SystemExit(f'byte out of range: {value}')
values.append(value)
code = marshal.loads(bytes(values))
if code.co_filename != '<frozen getpath>':
raise SystemExit(f'unexpected code filename: {code.co_filename!r}')
if code.co_name != '<module>':
raise SystemExit(f'unexpected code name: {code.co_name!r}')
print('format-ok')
PY
format_status=$?
if [ "$format_status" -eq 0 ]; then
pass "generated byte array unmarshals as frozen getpath code with CPython format"
else
bad "generated byte array unmarshals as frozen getpath code with CPython format"
fi
mkdir -p "$(dirname "$DIRECT_HEADER")"
python3 "$REPO_ROOT/cpython/Programs/_freeze_module.py" getpath \
"$REPO_ROOT/cpython/Modules/getpath.py" "$DIRECT_HEADER"
if cmp -s "$OUT_HEADER" "$DIRECT_HEADER"; then
pass "generator output matches direct Programs/_freeze_module.py invocation exactly"
else
bad "generator output matches direct Programs/_freeze_module.py invocation exactly"
fi
frontier_out="$(bash "$REPO_ROOT/scripts/vc6-probe-pythoncore-frontier.sh" --list-only 2>&1)"
frontier_status=$?
if [ "$frontier_status" -eq 0 ] && printf '%s\n' "$frontier_out" | grep -q $'^compile\tcpython/Modules/getpath.c\t'; then
pass "frontier list still starts from Modules/getpath.c without requiring generated output"
else
bad "frontier list still starts from Modules/getpath.c without requiring generated output"
echo "$frontier_out"
fi
if [ "$fail" -ne 0 ]; then
echo "FAILED"
exit 1
fi
echo "ALL TESTS PASSED"
+11 -5
View File
@@ -261,11 +261,17 @@ EOF
# --- positive case: the isolated compound-literal/designated-
# initializer smoke fixture compiles clean. It exercises, self-
# contained (aside from the real Python.h/pycore_initconfig.h types
# it needs), the three replacement patterns applied to main.c: calling
# PyStatus_Ok() instead of the _PyStatus_OK() compound-literal macro,
# explicit field assignment instead of the _PyCompilerFlags_INIT
# compound-literal macro, and explicit field assignment instead of a
# _PyArgv designated-initializer aggregate. ---
# it needs), the four replacement patterns now in use: the VC6-only
# _PyStatus_OK() helper (not PyStatus_Ok(), avoiding recursion), the
# VC6-only _PyStatus_ERR() helper, explicit field assignment instead of
# the _PyCompilerFlags_INIT compound-literal macro, and explicit field
# assignment instead of a _PyArgv designated-initializer aggregate. ---
if grep -q '#[[:space:]]*define[[:space:]]*_PyStatus_OK().*PyStatus_Ok' "$REPO_ROOT/cpython/Include/internal/pycore_initconfig.h"; then
bad "pycore_initconfig.h: VC6 _PyStatus_OK() helper does not recurse through PyStatus_Ok()"
else
pass "pycore_initconfig.h: VC6 _PyStatus_OK() helper does not recurse through PyStatus_Ok()"
fi
out_compound="$("$PROBE" scripts/fixtures/vc6_compound_literal_smoke.c 2>&1)"
status_compound=$?
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env bash
#
# Minimal VC6 link-frontier probe for the Windows 9x port.
#
# This is deliberately smaller than CPython's full PCbuild/pythoncore.vcxproj:
# it compiles only the translation units already proven by the core compile
# probes plus the tiny Programs/python.c executable entry point, then invokes
# LINK.EXE under Wine. A successful executable link is not expected yet; the
# purpose of this milestone is to verify that the build can get past CL.EXE
# for the current frontier and reach a real VC6 linker diagnostic with a
# reproducible command line.
#
# The MSVC600 bundle is an external, local-only prerequisite; nothing under
# MSVC600/ is part of this project's source.
#
# Usage:
# scripts/vc6-probe-minlink.sh
#
# Environment:
# MSVC600_ROOT override the MSVC600 location (default: <repo>/MSVC600)
# WINEPREFIX override the Wine prefix (default: ~/.wine-win9xport)
set -u
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# Reuse winpath()/strip_prefix() from vc6-probe.sh instead of duplicating
# them; sourcing it does not run the probe itself (see its own guard).
# shellcheck source=scripts/vc6-probe.sh
source "$REPO_ROOT/scripts/vc6-probe.sh"
run_probe_minlink() {
MSVC600_ROOT="${MSVC600_ROOT:-$REPO_ROOT/MSVC600}"
BUILD_DIR="$REPO_ROOT/build/vc6-probe-minlink"
CL="$MSVC600_ROOT/VC98/Bin/CL.EXE"
LINK="$MSVC600_ROOT/VC98/Bin/LINK.EXE"
VCVARS="$MSVC600_ROOT/VC98/Bin/VCVARS32.BAT"
# Keep this list intentionally tiny and aligned with the post-core-probe
# milestone: python.vcxproj's single executable TU plus the three core TUs
# that are currently known to compile under scripts/vc6-probe-core.sh.
SOURCES=(
"cpython/Programs/python.c"
"cpython/Modules/main.c"
"cpython/Python/pylifecycle.c"
"cpython/Python/pystate.c"
)
for required in "$CL" "$LINK" "$VCVARS" "$MSVC600_ROOT/VC98/Include" \
"$MSVC600_ROOT/VC98/Lib" "$MSVC600_ROOT/Common/MSDev98/Bin"; do
if [ ! -e "$required" ]; then
echo "vc6-probe-minlink: missing prerequisite: $required" >&2
return 2
fi
done
local source_rel
for source_rel in "${SOURCES[@]}"; do
if [ ! -e "$REPO_ROOT/$source_rel" ]; then
echo "vc6-probe-minlink: missing source: $REPO_ROOT/$source_rel" >&2
return 2
fi
done
if ! command -v wine >/dev/null 2>&1; then
echo "vc6-probe-minlink: wine is not installed" >&2
return 2
fi
export WINEPREFIX="${WINEPREFIX:-$HOME/.wine-win9xport}"
export WINEDEBUG="${WINEDEBUG:--all}"
# CL.EXE loads C1/C2 from VC98/Bin and MSPDB60.DLL from Common/MSDev98/Bin;
# LINK.EXE also needs the VC98 tools and default libraries.
export WINEPATH="$(winpath "$MSVC600_ROOT/VC98/Bin");$(winpath "$MSVC600_ROOT/Common/MSDev98/Bin")"
rm -rf "$BUILD_DIR"
mkdir -p "$BUILD_DIR"
INCLUDE_ARGS=(
"$(winpath "$REPO_ROOT/compat/msvc600")"
"$(winpath "$REPO_ROOT/cpython/Include/internal")"
"$(winpath "$REPO_ROOT/cpython/Include")"
"$(winpath "$REPO_ROOT/cpython/PC")"
"$(winpath "$MSVC600_ROOT/VC98/Include")"
)
echo "== MSVC600 minimal link-frontier probe =="
echo "sources:"
for source_rel in "${SOURCES[@]}"; do
echo " - $source_rel"
done
echo "prefix : $WINEPREFIX"
echo
local obj_args=""
local source_abs obj_name obj_abs cmd status
for source_rel in "${SOURCES[@]}"; do
source_abs="$REPO_ROOT/$source_rel"
obj_name="${source_rel#cpython/}"
obj_name="${obj_name//\//_}"
obj_name="${obj_name%.c}.obj"
obj_abs="$BUILD_DIR/$obj_name"
obj_args+=" $(winpath "$obj_abs")"
cmd="call $(winpath "$VCVARS") && cd /d $(winpath "$REPO_ROOT/cpython") && cl /nologo /D Py_BUILD_CORE /D Py_BUILD_CORE_BUILTIN /D Py_ENABLE_SHARED /D _USRDLL"
for include_arg in "${INCLUDE_ARGS[@]}"; do
cmd+=" /I $include_arg"
done
cmd+=" /c $(winpath "$source_abs") /Fo$(winpath "$obj_abs")"
wine cmd /c "$cmd" 2>&1 |
grep -v '^[0-9a-f]\{4\}:err:' |
strip_prefix "$(winpath "$REPO_ROOT")\\"
status="${PIPESTATUS[0]}"
if [ "$status" -ne 0 ]; then
echo
echo "== cl.exe exit status: $status =="
echo "vc6-probe-minlink: compile failed before link at $source_rel" >&2
return "$status"
fi
done
echo
echo "== LINK.EXE attempt =="
cmd="call $(winpath "$VCVARS") && cd /d $(winpath "$REPO_ROOT/cpython") && link /nologo /subsystem:console /out:$(winpath "$BUILD_DIR/python-min.exe") /libpath:$(winpath "$MSVC600_ROOT/VC98/Lib")$obj_args kernel32.lib user32.lib advapi32.lib shell32.lib version.lib ws2_32.lib"
wine cmd /c "$cmd" 2>&1 |
grep -v '^[0-9a-f]\{4\}:err:' |
strip_prefix "$(winpath "$REPO_ROOT")\\"
status="${PIPESTATUS[0]}"
echo
echo "== link.exe exit status: $status =="
return "$status"
}
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
run_probe_minlink "$@"
exit "$?"
fi
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
#
# Regression tests for the narrow VC6 compat/msvc600/pathcch.h shim.
#
# Skipped (not failed) when Wine + MSVC600 are unavailable, matching the other
# VC6 probe test scripts. The live check is compile-only: it verifies that the
# fixture using representative PathCchFindExtension paths reaches cl.exe exit 0.
#
# Usage: scripts/vc6-probe-pathcch.test.sh
set -u
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PROBE="$REPO_ROOT/scripts/vc6-probe-core.sh"
fail=0
pass() { echo "ok - $1"; }
bad() { echo "not ok - $1"; fail=1; }
if grep -q 'PathCchFindExtension' "$REPO_ROOT/compat/msvc600/pathcch.h" && \
grep -q 'defined(_MSC_VER).*1200' "$REPO_ROOT/compat/msvc600/pathcch.h"; then
pass "compat/msvc600/pathcch.h provides a VC6-scoped PathCchFindExtension shim"
else
bad "compat/msvc600/pathcch.h provides a VC6-scoped PathCchFindExtension shim"
fi
MSVC600_ROOT="${MSVC600_ROOT:-$REPO_ROOT/MSVC600}"
if command -v wine >/dev/null 2>&1 && [ -e "$MSVC600_ROOT/VC98/Bin/CL.EXE" ]; then
out="$($PROBE scripts/fixtures/vc6_pathcch_smoke.c 2>&1)"
status=$?
if [ "$status" -eq 0 ]; then
pass "PathCchFindExtension smoke fixture compiles under VC6 (exit 0)"
else
bad "PathCchFindExtension smoke fixture compiles under VC6 (exit $status)"
echo "$out"
fi
if printf '%s' "$out" | grep -q "Cannot open include file: 'pathcch.h'"; then
bad "pathcch smoke fixture does not regress to missing pathcch.h"
else
pass "pathcch smoke fixture does not regress to missing pathcch.h"
fi
if printf '%s' "$out" | grep -qi 'error'; then
bad "pathcch smoke fixture: no compiler errors in output"
else
pass "pathcch smoke fixture: no compiler errors in output"
fi
else
echo "skip - wine/MSVC600 not available; skipping live pathcch integration check"
fi
if [ "$fail" -ne 0 ]; then
echo "FAILED"
exit 1
fi
echo "ALL TESTS PASSED"
+376
View File
@@ -0,0 +1,376 @@
#!/usr/bin/env bash
#
# Source-list-driven VC6 compile frontier for CPython's pythoncore project.
#
# This reads cpython/PCbuild/pythoncore.vcxproj <ClCompile> entries, safely
# normalizes their Windows-style paths into project-local cpython/... sources,
# reports entries that this VC6/Wine frontier intentionally cannot consume
# yet, and compiles supported sources one at a time into build artifacts. It
# stops at the first cl.exe failure and returns cl.exe's real exit status.
#
# This is a compile-only (/c) frontier: no pythoncore.dll/python.exe link or
# runtime success is claimed.
#
# The MSVC600 bundle is an external, local-only prerequisite; nothing under
# MSVC600/ is part of this project's source.
#
# Usage:
# scripts/vc6-probe-pythoncore-frontier.sh
# scripts/vc6-probe-pythoncore-frontier.sh --list-only
# scripts/vc6-probe-pythoncore-frontier.sh --limit N
#
# Environment:
# MSVC600_ROOT override the MSVC600 location (default: <repo>/MSVC600)
# WINEPREFIX override the Wine prefix (default: ~/.wine-win9xport)
# VC6_FRONTIER_FORCE set to 1 to ignore incremental object freshness
set -u
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# Reuse winpath()/strip_prefix() from vc6-probe.sh instead of duplicating
# them; sourcing it does not run the probe itself (see its own guard).
# shellcheck source=scripts/vc6-probe.sh
source "$REPO_ROOT/scripts/vc6-probe.sh"
FRONTIER_SCRIPT="$REPO_ROOT/scripts/vc6-probe-pythoncore-frontier.sh"
PROJECT_REL="cpython/PCbuild/pythoncore.vcxproj"
PROJECT_ABS="$REPO_ROOT/$PROJECT_REL"
BUILD_DIR="$REPO_ROOT/build/vc6-probe-pythoncore-frontier"
SOURCE_LIST_TSV="$BUILD_DIR/pythoncore-sources.tsv"
GENERATED_ROOT="$REPO_ROOT/build/generated"
GENERATED_CPYTHON_ROOT="$GENERATED_ROOT/cpython"
GETPATH_GENERATOR="$REPO_ROOT/scripts/generate-getpath-frozen.sh"
GETPATH_FROZEN_HEADER="$GENERATED_CPYTHON_ROOT/Python/frozen_modules/getpath.h"
usage() {
sed -n '15,27p' "$FRONTIER_SCRIPT"
}
emit_source_list() {
python3 - "$PROJECT_ABS" "$REPO_ROOT" <<'PY'
import os
import posixpath
import sys
import xml.etree.ElementTree as ET
project = os.path.abspath(sys.argv[1])
repo = os.path.abspath(sys.argv[2])
source_root = os.path.join(repo, "cpython")
project_dir = os.path.dirname(project)
ns = "{http://schemas.microsoft.com/developer/msbuild/2003}"
substitutions = {
"$(PyVPath)": r"..\\..",
"$(PyDebugExt)": "",
"$(SysWinVer)": "3.11",
}
def expand_msbuild(value):
for key, repl in substitutions.items():
value = value.replace(key, repl)
return value
def normalize_include(include):
raw = include.strip()
expanded = expand_msbuild(raw)
if "$" in expanded or "%" in expanded:
return None, "unsupported-msbuild-macro"
path = expanded.replace("\\", "/")
if len(path) >= 2 and path[1] == ":":
return None, "absolute-windows-path"
abs_path = os.path.abspath(os.path.join(project_dir, path))
try:
rel_to_source = os.path.relpath(abs_path, source_root)
except ValueError:
return None, "outside-cpython-tree"
rel_posix = rel_to_source.replace(os.sep, "/")
if rel_posix == ".." or rel_posix.startswith("../") or posixpath.isabs(rel_posix):
return None, "outside-cpython-tree"
norm = posixpath.normpath(rel_posix)
if norm == "." or norm.startswith("../") or "/../" in f"/{norm}/":
return None, "unsafe-normalized-path"
return "cpython/" + norm, ""
def split_definitions(text):
if not text:
return []
text = expand_msbuild(" ".join(text.split()))
defs = []
for chunk in text.split(";"):
chunk = chunk.strip()
if not chunk or chunk == "%(PreprocessorDefinitions)":
continue
if "$" in chunk or "%" in chunk:
defs.append("UNSUPPORTED_MSBUILD_DEFINE=" + chunk)
else:
defs.append(chunk)
return defs
def classify(rel, inherited_condition, own_condition):
cond = f"{inherited_condition or ''} {own_condition or ''}"
if "IncludeExternals" in cond:
return "skip-external IncludeExternals-conditioned source"
if rel is None:
return "skip-unsupported"
if rel == "cpython/Python/deepfreeze/deepfreeze.c":
return "skip-generated deepfreeze source"
return "compile"
def walk(node, inherited_condition=""):
condition = node.attrib.get("Condition", "")
combined = " ".join(x for x in (inherited_condition, condition) if x)
if node.tag == ns + "ClCompile" and "Include" in node.attrib:
include = node.attrib["Include"]
rel, norm_error = normalize_include(include)
defs = []
for child in node:
if child.tag == ns + "PreprocessorDefinitions":
defs.extend(split_definitions(child.text or ""))
action = classify(rel, inherited_condition, condition)
reason = action
if action == "compile" and norm_error:
action = "skip-unsupported"
reason = f"skip-unsupported {norm_error}"
elif action == "skip-unsupported" and norm_error:
reason = f"skip-unsupported {norm_error}"
display_rel = rel or include.replace("\\", "/")
print("\t".join([action, display_rel, include, " ".join(defs), reason]))
for child in node:
walk(child, combined)
root = ET.parse(project).getroot()
walk(root)
PY
}
quote_define_for_cmd() {
local define="$1"
# Keep CL's /D string literal quotes after CL reads the generated response
# file. CL interprets C-string backslashes in /D values while parsing the
# define, so double the define's own backslashes before protecting literal
# quotes for CL's command-line parser.
# Example: VPATH="..\\.." must arrive at C preprocessing as "..\\..", not
# "..\.." and not an unterminated string.
define="${define//\\/\\\\}"
define="${define//\"/\\\"}"
printf '%s' "$define"
}
render_compile_command_for_test() {
local cmd='cl /nologo'
local define
for define in "$@"; do
cmd+=" /D $(quote_define_for_cmd "$define")"
done
printf '%s' "$cmd"
}
obj_name_for_source() {
local source_rel="$1"
source_rel="${source_rel#cpython/}"
source_rel="${source_rel//\//_}"
printf '%s' "${source_rel%.c}.obj"
}
generate_getpath_frozen_header() {
if [ ! -x "$GETPATH_GENERATOR" ]; then
echo "vc6-probe-pythoncore-frontier: missing prerequisite: executable $GETPATH_GENERATOR" >&2
return 2
fi
"$GETPATH_GENERATOR" "$GENERATED_ROOT" >/dev/null
}
source_for_compile() {
local source_rel="$1"
local source_abs="$2"
if [ "$source_rel" != "cpython/Modules/getpath.c" ]; then
printf '%s' "$source_abs"
return 0
fi
# Modules/getpath.c includes "../Python/frozen_modules/getpath.h" relative
# to the compiling source file. Keep generated files under ignored build/
# output by compiling a byte-for-byte copy from a generated cpython-shaped
# tree where that documented relative include resolves.
if [ ! -s "$GETPATH_FROZEN_HEADER" ]; then
echo "vc6-probe-pythoncore-frontier: missing generated getpath header: $GETPATH_FROZEN_HEADER" >&2
return 2
fi
local generated_source="$GENERATED_CPYTHON_ROOT/Modules/getpath.c"
mkdir -p "$(dirname "$generated_source")"
cp "$source_abs" "$generated_source"
printf '%s' "$generated_source"
}
run_probe_pythoncore_frontier() {
local list_only=0 limit=0 arg
while [ "$#" -gt 0 ]; do
arg="$1"
case "$arg" in
--list-only)
list_only=1
shift
;;
--limit)
if [ "$#" -lt 2 ]; then
echo "vc6-probe-pythoncore-frontier: --limit requires a number" >&2
return 2
fi
limit="$2"
case "$limit" in
''|*[!0-9]*) echo "vc6-probe-pythoncore-frontier: invalid --limit: $limit" >&2; return 2 ;;
esac
shift 2
;;
-h|--help)
usage
return 0
;;
*)
echo "vc6-probe-pythoncore-frontier: unknown argument: $arg" >&2
usage >&2
return 2
;;
esac
done
mkdir -p "$BUILD_DIR"
emit_source_list > "$SOURCE_LIST_TSV"
if [ "$list_only" -eq 1 ]; then
cat "$SOURCE_LIST_TSV"
return 0
fi
MSVC600_ROOT="${MSVC600_ROOT:-$REPO_ROOT/MSVC600}"
local CL="$MSVC600_ROOT/VC98/Bin/CL.EXE"
local VCVARS="$MSVC600_ROOT/VC98/Bin/VCVARS32.BAT"
generate_getpath_frozen_header || return $?
for required in "$PROJECT_ABS" "$CL" "$VCVARS" "$MSVC600_ROOT/VC98/Include" \
"$MSVC600_ROOT/Common/MSDev98/Bin"; do
if [ ! -e "$required" ]; then
echo "vc6-probe-pythoncore-frontier: missing prerequisite: $required" >&2
return 2
fi
done
if ! command -v wine >/dev/null 2>&1; then
echo "vc6-probe-pythoncore-frontier: wine is not installed" >&2
return 2
fi
export WINEPREFIX="${WINEPREFIX:-$HOME/.wine-win9xport}"
export WINEDEBUG="${WINEDEBUG:--all}"
# CL.EXE loads C1/C2 from VC98/Bin and MSPDB60.DLL from Common/MSDev98/Bin.
export WINEPATH="$(winpath "$MSVC600_ROOT/VC98/Bin");$(winpath "$MSVC600_ROOT/Common/MSDev98/Bin")"
local INCLUDE_ARGS=(
"$(winpath "$REPO_ROOT/compat/msvc600")"
"$(winpath "$REPO_ROOT/cpython/Python")"
"$(winpath "$REPO_ROOT/cpython/Include/internal")"
"$(winpath "$REPO_ROOT/cpython/Include")"
"$(winpath "$REPO_ROOT/cpython/PC")"
"$(winpath "$MSVC600_ROOT/VC98/Include")"
)
local COMMON_DEFINES=(
"_USRDLL"
"Py_BUILD_CORE"
"Py_BUILD_CORE_BUILTIN"
"Py_ENABLE_SHARED"
'MS_DLL_ID="3.11"'
)
echo "== MSVC600 pythoncore compile-frontier probe =="
echo "project: $PROJECT_REL"
echo "list : ${SOURCE_LIST_TSV#$REPO_ROOT/}"
echo "prefix : $WINEPREFIX"
echo
local action source_rel raw_include defs reason
local total=0 skipped=0 compiled=0 cached=0 considered=0
while IFS=$'\t' read -r action source_rel raw_include defs reason; do
total=$((total + 1))
if [ "$action" != "compile" ]; then
skipped=$((skipped + 1))
echo "skip: $source_rel ($reason; raw=$raw_include)"
continue
fi
if [ "$limit" -ne 0 ] && [ "$considered" -ge "$limit" ]; then
break
fi
considered=$((considered + 1))
local source_abs="$REPO_ROOT/$source_rel"
if [ ! -f "$source_abs" ]; then
echo "vc6-probe-pythoncore-frontier: missing source from project list: $source_rel" >&2
echo "== frontier summary: total=$total compiled=$compiled cached=$cached skipped=$skipped =="
return 2
fi
local obj_abs="$BUILD_DIR/$(obj_name_for_source "$source_rel")"
local compile_source_abs
compile_source_abs="$(source_for_compile "$source_rel" "$source_abs")" || return $?
if [ "${VC6_FRONTIER_FORCE:-0}" != "1" ] && [ -f "$obj_abs" ] && \
[ "$obj_abs" -nt "$source_abs" ] && [ "$obj_abs" -nt "$PROJECT_ABS" ] && \
[ "$obj_abs" -nt "$FRONTIER_SCRIPT" ] && [ "$obj_abs" -nt "$GETPATH_FROZEN_HEADER" ]; then
cached=$((cached + 1))
echo "cached: $source_rel -> ${obj_abs#$REPO_ROOT/}"
continue
fi
echo "compile: $source_rel -> ${obj_abs#$REPO_ROOT/}"
local rsp_abs="${obj_abs%.obj}.rsp"
local include_arg define
{
printf '/nologo\r\n'
for define in "${COMMON_DEFINES[@]}"; do
printf '/D %s\r\n' "$(quote_define_for_cmd "$define")"
done
if [ -n "$defs" ]; then
for define in $defs; do
if [ "${define#UNSUPPORTED_MSBUILD_DEFINE=}" != "$define" ]; then
echo "vc6-probe-pythoncore-frontier: unsupported per-file define in $source_rel: ${define#UNSUPPORTED_MSBUILD_DEFINE=}" >&2
echo "== frontier summary: total=$total compiled=$compiled cached=$cached skipped=$skipped =="
return 2
fi
printf '/D %s\r\n' "$(quote_define_for_cmd "$define")"
done
fi
for include_arg in "${INCLUDE_ARGS[@]}"; do
printf '/I %s\r\n' "$include_arg"
done
printf '/c\r\n'
printf '%s\r\n' "$(winpath "$compile_source_abs")"
printf '/Fo%s\r\n' "$(winpath "$obj_abs")"
} > "$rsp_abs"
local cmd="call $(winpath "$VCVARS") && cd /d $(winpath "$REPO_ROOT/cpython") && cl @$(winpath "$rsp_abs")"
wine cmd /c "$cmd" 2>&1 |
grep -v '^[0-9a-f]\{4\}:err:' |
strip_prefix "$(winpath "$REPO_ROOT")\\"
local status="${PIPESTATUS[0]}"
if [ "$status" -ne 0 ]; then
echo
echo "== cl.exe exit status: $status =="
echo "vc6-probe-pythoncore-frontier: first compile blocker: $source_rel" >&2
echo "== frontier summary: total=$total compiled=$compiled cached=$cached skipped=$skipped =="
return "$status"
fi
compiled=$((compiled + 1))
done < "$SOURCE_LIST_TSV"
echo
echo "== frontier summary: total=$total compiled=$compiled cached=$cached skipped=$skipped =="
echo "vc6-probe-pythoncore-frontier: supported pythoncore compile frontier completed"
return 0
}
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
run_probe_pythoncore_frontier "$@"
exit "$?"
fi
+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env bash
#
# Regression tests for scripts/vc6-probe-pythoncore-frontier.sh.
#
# The no-toolchain checks exercise the source-list parser/normalizer and the
# per-file define handling that make the frontier source-list-driven. When
# Wine + MSVC600 are available, a bounded live run verifies that the harness
# reports cl.exe's real first-blocker status.
#
# Usage: scripts/vc6-probe-pythoncore-frontier.test.sh
set -u
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PROBE="$REPO_ROOT/scripts/vc6-probe-pythoncore-frontier.sh"
fail=0
pass() { echo "ok - $1"; }
bad() { echo "not ok - $1"; fail=1; }
out="$($PROBE --list-only 2>&1)"
status=$?
if [ "$status" -eq 0 ]; then
pass "frontier --list-only exits 0"
else
bad "frontier --list-only exits 0 (got exit $status)"
echo "$out"
fi
count="$(printf '%s\n' "$out" | awk 'NF { n++ } END { print n+0 }')"
if [ "$count" -eq 198 ]; then
pass "frontier parses all 198 pythoncore.vcxproj ClCompile entries"
else
bad "frontier parses all 198 pythoncore.vcxproj ClCompile entries (got $count)"
fi
first_fields="$(printf '%s\n' "$out" | awk -F '\t' 'NR == 1 { print $1 "|" $2 "|" $3 }')"
if [ "$first_fields" = 'compile|cpython/Modules/getpath.c|..\Modules\getpath.c' ]; then
pass "frontier normalizes the first Windows-style source path to cpython/Modules/getpath.c"
else
bad "frontier normalizes the first Windows-style source path to cpython/Modules/getpath.c (got: $first_fields)"
fi
getpath_defs="$(printf '%s\n' "$out" | awk -F '\t' '$2 == "cpython/Modules/getpath.c" { print $4; exit }')"
case "$getpath_defs" in
*'PREFIX=NULL'*'EXEC_PREFIX=NULL'*'VERSION=NULL'*'VPATH="..\\.."'*'PYDEBUGEXT=""'*'PLATLIBDIR="DLLs"'*)
pass "frontier preserves getpath.c per-file defines with project-local substitutions"
;;
*)
bad "frontier preserves getpath.c per-file defines with project-local substitutions (got: $getpath_defs)"
;;
esac
sysmodule_defs="$(printf '%s\n' "$out" | awk -F '\t' '$2 == "cpython/Python/sysmodule.c" { print $4; exit }')"
case "$sysmodule_defs" in
*'VPATH="..\\.."'*)
pass "frontier preserves sysmodule.c VPATH per-file define"
;;
*)
bad "frontier preserves sysmodule.c VPATH per-file define (got: $sysmodule_defs)"
;;
esac
# Source the probe after --list-only so the guarded helper definitions can be
# tested without launching Wine or requiring MSVC600.
# shellcheck source=scripts/vc6-probe-pythoncore-frontier.sh
source "$PROBE"
rendered_cmd="$(render_compile_command_for_test 'VPATH="..\\.."' 'PYDEBUGEXT=""' 'PLATLIBDIR="DLLs"')"
case "$rendered_cmd" in
*' /D VPATH=\"..\\\\..\"'*' /D PYDEBUGEXT=\"\"'*' /D PLATLIBDIR=\"DLLs\"'*)
pass "frontier renders quoted per-file string defines safely for CL response files"
;;
*)
bad "frontier renders quoted per-file string defines safely for CL response files (got: $rendered_cmd)"
;;
esac
if printf '%s\n' "$out" | grep -q $'^skip-generated deepfreeze source\tcpython/Python/deepfreeze/deepfreeze.c\t'; then
pass "frontier reports the generated deepfreeze source instead of compiling it blindly"
else
bad "frontier reports the generated deepfreeze source instead of compiling it blindly"
fi
if printf '%s\n' "$out" | grep -q $'^skip-external IncludeExternals-conditioned source\tcpython/Modules/zlibmodule.c\t'; then
pass "frontier reports IncludeExternals-conditioned in-tree zlibmodule.c as external"
else
bad "frontier reports IncludeExternals-conditioned in-tree zlibmodule.c as external"
fi
if printf '%s\n' "$out" | grep -q $'^skip-external IncludeExternals-conditioned source\t$(zlibDir)/adler32.c\t'; then
pass "frontier reports macro-based zlibDir sources as external without normalizing outside the repo"
else
bad "frontier reports macro-based zlibDir sources as external without normalizing outside the repo"
fi
if printf '%s\n' "$out" | cut -f2 | grep -q '\\'; then
bad "frontier normalized source column contains no backslashes"
else
pass "frontier normalized source column contains no backslashes"
fi
MSVC600_ROOT="${MSVC600_ROOT:-$REPO_ROOT/MSVC600}"
if command -v wine >/dev/null 2>&1 && [ -e "$MSVC600_ROOT/VC98/Bin/CL.EXE" ]; then
live_out="$($PROBE --limit 1 2>&1)"
live_status=$?
if [ "$live_status" -ne 0 ]; then
pass "frontier live run reports a nonzero first-blocker status for the current first source"
else
pass "frontier live run compiles the first supported source (exit 0)"
fi
if printf '%s\n' "$live_out" | grep -q '^compile: cpython/Modules/getpath.c'; then
pass "frontier live run starts from pythoncore.vcxproj's first supported source"
else
bad "frontier live run starts from pythoncore.vcxproj's first supported source"
echo "$live_out"
fi
if [ "$live_status" -ne 0 ] && printf '%s\n' "$live_out" | grep -q 'first compile blocker: cpython/Modules/getpath.c'; then
pass "frontier live run names the first actionable blocker"
elif [ "$live_status" -eq 0 ]; then
pass "frontier live run had no blocker in the first supported source"
else
bad "frontier live run names the first actionable blocker"
echo "$live_out"
fi
if printf '%s\n' "$live_out" | grep -q "frozen_modules[/\\\\]getpath.h.*No such file or directory"; then
bad "frontier live run should provision Python/frozen_modules/getpath.h before compiling getpath.c"
echo "$live_out"
elif [ -s "$REPO_ROOT/build/generated/cpython/Python/frozen_modules/getpath.h" ]; then
pass "frontier live run provisions the generated getpath.h include under build/"
else
bad "frontier live run provisions the generated getpath.h include under build/"
echo "$live_out"
fi
else
echo "skip - wine/MSVC600 not available; skipping live frontier integration check"
fi
if [ "$fail" -ne 0 ]; then
echo "FAILED"
exit 1
fi
echo "ALL TESTS PASSED"
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
#
# Regression tests for the narrow VC6 compat/msvc600/stdint.h shim.
#
# Skipped (not failed) when Wine + MSVC600 are unavailable, matching the other
# VC6 probe test scripts. The live check is compile-only: it verifies that the
# fixture using representative uint8_t/uint32_t/uint64_t load/store/rotate
# patterns (the exact subset cpython/Modules/_blake2/impl/blake2-impl.h uses)
# reaches cl.exe exit 0, and that <stdint.h> and <inttypes.h> can both be
# included in the same translation unit without a duplicate-typedef error.
#
# Usage: scripts/vc6-probe-stdint.test.sh
set -u
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PROBE="$REPO_ROOT/scripts/vc6-probe-core.sh"
fail=0
pass() { echo "ok - $1"; }
bad() { echo "not ok - $1"; fail=1; }
if grep -q 'include "inttypes.h"' "$REPO_ROOT/compat/msvc600/stdint.h" && \
grep -q 'defined(_MSC_VER).*1200\|_MSC_VER >= 1300' "$REPO_ROOT/compat/msvc600/stdint.h"; then
pass "compat/msvc600/stdint.h is a VC6-scoped forward to inttypes.h"
else
bad "compat/msvc600/stdint.h is a VC6-scoped forward to inttypes.h"
fi
MSVC600_ROOT="${MSVC600_ROOT:-$REPO_ROOT/MSVC600}"
if command -v wine >/dev/null 2>&1 && [ -e "$MSVC600_ROOT/VC98/Bin/CL.EXE" ]; then
out="$($PROBE scripts/fixtures/vc6_stdint_smoke.c 2>&1)"
status=$?
if [ "$status" -eq 0 ]; then
pass "stdint smoke fixture compiles under VC6 (exit 0)"
else
bad "stdint smoke fixture compiles under VC6 (exit $status)"
echo "$out"
fi
if printf '%s' "$out" | grep -q "Cannot open include file: 'stdint.h'"; then
bad "stdint smoke fixture does not regress to missing stdint.h"
else
pass "stdint smoke fixture does not regress to missing stdint.h"
fi
if printf '%s' "$out" | grep -qi 'error'; then
bad "stdint smoke fixture: no compiler errors in output"
else
pass "stdint smoke fixture: no compiler errors in output"
fi
else
echo "skip - wine/MSVC600 not available; skipping live stdint integration check"
fi
if [ "$fail" -ne 0 ]; then
echo "FAILED"
exit 1
fi
echo "ALL TESTS PASSED"