381 lines
14 KiB
Bash
Executable File
381 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 ""))
|
|
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
|