Files
Python3-Win9x/scripts/vc6-probe-pythoncore-frontier.sh
T

377 lines
14 KiB
Bash
Executable File

#!/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