port: advance MSVC600 CPython compatibility frontier
This commit is contained in:
Executable
+376
@@ -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
|
||||
Reference in New Issue
Block a user