#!/usr/bin/env bash # # Source-list-driven VC6 compile frontier for CPython's pythoncore project. # # This reads cpython/PCbuild/pythoncore.vcxproj 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: /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 "")) if rel == "cpython/Modules/binascii.c": # binascii.c has a complete local CRC32 fallback. Do not require # the external zlib headers in the minimal VC6 frontier. defs = [define for define in defs if define != "USE_ZLIB_CRC32"] 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