McRogueFace/tools/package.sh
John McCardle 54624b353d Untrack generated docs; add API manifest infra and pre-commit hook
Rendered/generated documentation no longer lives in git -- it's regenerated
by a pre-commit hook (tools/hooks/pre-commit, installed via `make
install-hooks`) and shipped inside release artifacts (tools/package.sh) instead.
A compact api/manifest.json (signatures + docstring hashes + carried-forward
lifecycle metadata: since/modified per object and member) is the one exception
and stays committed, refreshed by the same hook, so API changes remain
trackable by commit/tag via `git show <ref>:api/manifest.json` and
`tools/api_delta.py REF1 REF2` without needing a full rebuild at old refs.

- tools/generate_api_manifest.py: engine-side introspection, run headless;
  emits api/manifest.json (committed) and docs/generated/api_full.json
  (gitignored, full docstrings -- the docs-site generator's data source).
- tools/api_delta.py: stdlib-only diff CLI (md/json/gitea-checklist formats),
  with --site-dir to cross-reference affected docs-site pages via their mcrf
  frontmatter.
- tools/hooks/pre-commit: incremental build + frozen-docstring gate + doc
  regen + manifest refresh, gated on staged src/ changes.
- .gitignore covers the rendered docs/stubs plus personal working-notes docs
  (plans, audits, research write-ups) that were never McRogueFace-user-facing.
- docs/api-stability.md and docs/threading-model.md removed: their content is
  now the wiki's "API Stability Contract" and "Threading Model" system pages
  (ratified 2026-07-02, migration verified 2026-07-11); the repo continues to
  enforce them via tests/unit/api_surface_snapshot_test.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 20:57:31 -04:00

398 lines
13 KiB
Bash
Executable file

#!/bin/bash
#
# McRogueFace Distribution Packager
#
# Creates clean distribution packages for Windows and Linux
# Supports light (minimal stdlib) and full (complete stdlib) variants
#
# Usage:
# ./tools/package.sh windows light # Windows light build
# ./tools/package.sh windows full # Windows full build
# ./tools/package.sh linux light # Linux light build
# ./tools/package.sh linux full # Linux full build
# ./tools/package.sh all # All variants
#
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
DIST_DIR="$PROJECT_ROOT/dist"
# Version from git or default
VERSION=$(cd "$PROJECT_ROOT" && git describe --tags --always 2>/dev/null || echo "dev")
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Files and directories to exclude from distribution
EXCLUDE_PATTERNS=(
"CMakeFiles"
"CMakeCache.txt"
"cmake_install.cmake"
"Makefile"
"*.o"
"*.obj"
".git"
".gitignore"
"__pycache__"
"*.pyc"
"*.pyo"
)
# Platform-specific excludes
WINDOWS_EXCLUDES=(
"*.so"
"*.so.*"
"lib.linux-*"
)
LINUX_EXCLUDES=(
"*.dll"
"*.exe"
"*.pdb"
)
check_build_exists() {
local platform=$1
local build_dir
if [ "$platform" = "windows" ]; then
build_dir="$PROJECT_ROOT/build-windows"
if [ ! -f "$build_dir/mcrogueface.exe" ]; then
log_error "Windows build not found. Run 'make windows' first."
return 1
fi
else
build_dir="$PROJECT_ROOT/build"
if [ ! -f "$build_dir/mcrogueface" ]; then
log_error "Linux build not found. Run 'make' first."
return 1
fi
fi
return 0
}
create_stdlib_zip() {
local platform=$1
local preset=$2
local output_dir=$3
log_info "Creating stdlib zip: platform=$platform preset=$preset"
# Use Python 3 to run our stdlib packager
python3 "$SCRIPT_DIR/package_stdlib.py" \
--platform "$platform" \
--preset "$preset" \
--output "$output_dir"
}
# Copy the rendered API docs + committed manifest into <package>/docs/.
# These are generated (rendered docs live outside git as of the generated-docs
# .gitignore block) but MUST ship inside every release artifact.
copy_release_docs() {
local package_dir=$1
local docs_out="$package_dir/docs"
log_info "Bundling API documentation into docs/"
mkdir -p "$docs_out"
# "src (relative to PROJECT_ROOT) : dest (relative to <package>/docs)"
local pairs=(
"docs/API_REFERENCE_DYNAMIC.md:API_REFERENCE_DYNAMIC.md"
"docs/api_reference_dynamic.html:api_reference_dynamic.html"
"docs/mcrfpy.3:mcrfpy.3"
"stubs/mcrfpy.pyi:mcrfpy.pyi"
"docs/generated/api_full.json:generated/api_full.json"
"api/manifest.json:manifest.json"
)
local pair src dst
for pair in "${pairs[@]}"; do
src="${pair%%:*}"
dst="${pair#*:}"
if [ -f "$PROJECT_ROOT/$src" ]; then
mkdir -p "$(dirname "$docs_out/$dst")"
cp "$PROJECT_ROOT/$src" "$docs_out/$dst"
else
log_warn "Release doc missing (NOT bundled): $src"
log_warn " run 'bash tools/generate_all_docs.sh' and regenerate the manifest before packaging"
fi
done
}
package_windows() {
local preset=$1
local build_dir="$PROJECT_ROOT/build-windows"
local package_name="McRogueFace-${VERSION}-Windows-${preset}"
local package_dir="$DIST_DIR/$package_name"
log_info "Packaging Windows ($preset): $package_name"
# Check build exists
check_build_exists windows || return 1
# Clean and create package directory
rm -rf "$package_dir"
mkdir -p "$package_dir"
# Copy executable
cp "$build_dir/mcrogueface.exe" "$package_dir/"
# Copy DLLs (excluding build artifacts)
for dll in "$build_dir"/*.dll; do
[ -f "$dll" ] && cp "$dll" "$package_dir/"
done
# Copy assets
if [ -d "$build_dir/assets" ]; then
cp -r "$build_dir/assets" "$package_dir/"
fi
# Copy scripts
if [ -d "$build_dir/scripts" ]; then
cp -r "$build_dir/scripts" "$package_dir/"
fi
# Copy Python stdlib directory structure (same as Linux)
# Python home is set to <exe>/lib/Python, so stdlib must be there
mkdir -p "$package_dir/lib/Python"
# Copy the Lib directory from build (matches Linux structure)
if [ -d "$build_dir/lib/Python/Lib" ]; then
log_info "Copying Python stdlib (preset: $preset)"
cp -r "$build_dir/lib/Python/Lib" "$package_dir/lib/Python/"
# Remove test directories and other excludes to save space
rm -rf "$package_dir/lib/Python/Lib/test"
rm -rf "$package_dir/lib/Python/Lib/tests"
rm -rf "$package_dir/lib/Python/Lib/idlelib"
rm -rf "$package_dir/lib/Python/Lib/tkinter"
rm -rf "$package_dir/lib/Python/Lib/turtledemo"
rm -rf "$package_dir/lib/Python/Lib/pydoc_data"
rm -rf "$package_dir/lib/Python/Lib/lib2to3"
rm -rf "$package_dir/lib/Python/Lib/ensurepip"
rm -rf "$package_dir/lib/Python/Lib/_pyrepl"
find "$package_dir/lib/Python/Lib" -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
find "$package_dir/lib/Python/Lib" -name "*.pyc" -delete 2>/dev/null || true
find "$package_dir/lib/Python/Lib" -name "test_*.py" -delete 2>/dev/null || true
find "$package_dir/lib/Python/Lib" -name "*_test.py" -delete 2>/dev/null || true
fi
# Also copy python314.zip for backwards compatibility (some deployments use it)
if [ -f "$build_dir/python314.zip" ]; then
cp "$build_dir/python314.zip" "$package_dir/"
fi
# Bundle rendered API docs + manifest
copy_release_docs "$package_dir"
# Create the distribution archive
log_info "Creating archive: ${package_name}.zip"
(cd "$DIST_DIR" && zip -r "${package_name}.zip" "$package_name")
# Report size
local size=$(du -h "$DIST_DIR/${package_name}.zip" | cut -f1)
log_info "Created: $DIST_DIR/${package_name}.zip ($size)"
# Cleanup uncompressed directory
rm -rf "$package_dir"
}
package_linux() {
local preset=$1
local build_dir="$PROJECT_ROOT/build"
local package_name="McRogueFace-${VERSION}-Linux-${preset}"
local package_dir="$DIST_DIR/$package_name"
log_info "Packaging Linux ($preset): $package_name"
# Check build exists
check_build_exists linux || return 1
# Clean and create package directory
rm -rf "$package_dir"
mkdir -p "$package_dir"
mkdir -p "$package_dir/lib"
# Copy executable
cp "$build_dir/mcrogueface" "$package_dir/"
# Copy shared libraries from __lib (not from build dir which has artifacts)
if [ -d "$PROJECT_ROOT/__lib" ]; then
# Copy only essential runtime libraries (not test modules)
# Core libraries: libpython, libsfml-*, libtcod
# NOTE: libtcod.so.2 is the SONAME the executable records in DT_NEEDED;
# ship the real object under that exact name (not the bare libtcod.so),
# otherwise the runtime loader cannot find it on a clean machine.
for lib in libpython3.14.so.1.0 libsfml-graphics.so.2.6.1 libsfml-window.so.2.6.1 \
libsfml-system.so.2.6.1 libsfml-audio.so.2.6.1 libtcod.so.2; do
[ -f "$PROJECT_ROOT/__lib/$lib" ] && cp "$PROJECT_ROOT/__lib/$lib" "$package_dir/lib/"
done
# Create necessary symlinks
(cd "$package_dir/lib" && \
ln -sf libpython3.14.so.1.0 libpython3.14.so && \
ln -sf libsfml-graphics.so.2.6.1 libsfml-graphics.so.2.6 && \
ln -sf libsfml-graphics.so.2.6.1 libsfml-graphics.so && \
ln -sf libsfml-window.so.2.6.1 libsfml-window.so.2.6 && \
ln -sf libsfml-window.so.2.6.1 libsfml-window.so && \
ln -sf libsfml-system.so.2.6.1 libsfml-system.so.2.6 && \
ln -sf libsfml-system.so.2.6.1 libsfml-system.so && \
ln -sf libsfml-audio.so.2.6.1 libsfml-audio.so.2.6 && \
ln -sf libsfml-audio.so.2.6.1 libsfml-audio.so && \
ln -sf libtcod.so.2 libtcod.so)
# Copy Python extension modules to correct location (excluding test modules)
# Must match structure: lib/Python/lib.linux-x86_64-3.14/
local pylib_dir="$PROJECT_ROOT/__lib/Python/lib.linux-x86_64-3.14"
if [ -d "$pylib_dir" ]; then
mkdir -p "$package_dir/lib/Python/lib.linux-x86_64-3.14"
for so in "$pylib_dir"/*.so; do
local basename=$(basename "$so")
# Skip test modules
case "$basename" in
*test*|xxlimited*|_ctypes_test*|_xxtestfuzz*)
continue
;;
*)
cp "$so" "$package_dir/lib/Python/lib.linux-x86_64-3.14/"
;;
esac
done
fi
fi
# Copy assets
if [ -d "$build_dir/assets" ]; then
cp -r "$build_dir/assets" "$package_dir/"
fi
# Copy scripts
if [ -d "$build_dir/scripts" ]; then
cp -r "$build_dir/scripts" "$package_dir/"
fi
# Copy Python stdlib directory
# Python home is set to <exe>/lib/Python, stdlib must be in lib/Python/Lib/
mkdir -p "$package_dir/lib/Python"
# Copy the Lib directory from __lib/Python/Lib (filtered by preset)
if [ -d "$PROJECT_ROOT/__lib/Python/Lib" ]; then
log_info "Copying Python stdlib (preset: $preset)"
# For now, copy entire Lib - filtering happens via package_stdlib.py for zip
# TODO: Implement directory-based filtering for light preset
cp -r "$PROJECT_ROOT/__lib/Python/Lib" "$package_dir/lib/Python/"
# Remove test directories and other excludes to save space
rm -rf "$package_dir/lib/Python/Lib/test"
rm -rf "$package_dir/lib/Python/Lib/tests"
rm -rf "$package_dir/lib/Python/Lib/idlelib"
rm -rf "$package_dir/lib/Python/Lib/tkinter"
rm -rf "$package_dir/lib/Python/Lib/turtledemo"
rm -rf "$package_dir/lib/Python/Lib/pydoc_data"
rm -rf "$package_dir/lib/Python/Lib/lib2to3"
rm -rf "$package_dir/lib/Python/Lib/ensurepip"
rm -rf "$package_dir/lib/Python/Lib/_pyrepl"
find "$package_dir/lib/Python/Lib" -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
find "$package_dir/lib/Python/Lib" -name "*.pyc" -delete 2>/dev/null || true
find "$package_dir/lib/Python/Lib" -name "test_*.py" -delete 2>/dev/null || true
find "$package_dir/lib/Python/Lib" -name "*_test.py" -delete 2>/dev/null || true
fi
# Bundle rendered API docs + manifest
copy_release_docs "$package_dir"
# Create run script
cat > "$package_dir/run.sh" << 'EOF'
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export LD_LIBRARY_PATH="$SCRIPT_DIR/lib:$LD_LIBRARY_PATH"
exec "$SCRIPT_DIR/mcrogueface" "$@"
EOF
chmod +x "$package_dir/run.sh"
# Create the distribution archive
log_info "Creating archive: ${package_name}.tar.gz"
(cd "$DIST_DIR" && tar -czf "${package_name}.tar.gz" "$package_name")
# Report size
local size=$(du -h "$DIST_DIR/${package_name}.tar.gz" | cut -f1)
log_info "Created: $DIST_DIR/${package_name}.tar.gz ($size)"
# Cleanup uncompressed directory
rm -rf "$package_dir"
}
show_usage() {
echo "McRogueFace Distribution Packager"
echo ""
echo "Usage: $0 <platform> <preset>"
echo ""
echo "Platforms:"
echo " windows - Windows build (requires 'make windows' first)"
echo " linux - Linux build (requires 'make' first)"
echo " all - Build all variants"
echo ""
echo "Presets:"
echo " light - Minimal stdlib (~2-3 MB)"
echo " full - Complete stdlib (~8-10 MB)"
echo ""
echo "Examples:"
echo " $0 windows light # Small Windows package"
echo " $0 linux full # Full Linux package"
echo " $0 all # All platform/preset combinations"
}
main() {
local platform=${1:-}
local preset=${2:-full}
# Create dist directory
mkdir -p "$DIST_DIR"
case "$platform" in
windows)
package_windows "$preset"
;;
linux)
package_linux "$preset"
;;
all)
log_info "Building all distribution variants..."
package_windows light || true
package_windows full || true
package_linux light || true
package_linux full || true
log_info "All packages created in $DIST_DIR"
ls -lh "$DIST_DIR"/*.zip "$DIST_DIR"/*.tar.gz 2>/dev/null || true
;;
-h|--help|"")
show_usage
exit 0
;;
*)
log_error "Unknown platform: $platform"
show_usage
exit 1
;;
esac
}
main "$@"