Files
CoolProp/dev/test_wheel_contents.py
Ian Bell 70d03056e0 Modernize Python build system to use scikit-build-core (#2632)
* Modernize Python build system to use scikit-build-core

This commit replaces the old setuptools-based build system with a modern
scikit-build-core + CMake build system for the Python bindings.

Key changes:
- Replace setup.py with pyproject.toml using scikit-build-core backend
- Create new CMakeLists.txt for Cython module compilation
- Add FindCython.cmake helper module
- Update README from .rst to .md format
- Enable incremental builds with proper CMake dependency tracking
- Support Python 3.8-3.14 with proper Cython directives

Benefits:
- Incremental builds work correctly (only rebuild changed files)
- Modern PEP 517/518 compliant build system
- Build artifacts cached in build/{wheel_tag} directories
- Better integration with pip and modern Python tooling
- No more need for custom _py_backend build hooks

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add build-time file generation and ignore generated files

This commit adds the missing build-time steps from setup.py:
- Header generation from JSON files (generate_headers.py)
- Cython constants module generation (generate_constants_module.py)
- Copying headers, fmtlib, and BibTeX file to package directory

Also updates .gitignore to ignore:
- wrappers/Python/CoolProp/include/ (generated during build)
- wrappers/Python/CoolProp/CoolPropBibTeXLibrary.bib (copied during build)

Includes test script to verify wheel contents match between old and new build approaches.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Set CMAKE_POSITION_INDEPENDENT_CODE for shared library build

Enable -fPIC flag for all targets to ensure proper shared library compilation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Remove deprecated buildbot configuration

The buildbot system is deprecated and no longer in use.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Update extract_version.py to use .version file instead of setup.py

With the migration to scikit-build-core, version information is now stored
in the .version file and read by pyproject.toml. Updated the script to:

- Rename replace_setup_py() to replace_version_file()
- Update .version file instead of modifying setup.py
- Change --replace-setup-py flag to --replace-version

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Update build scripts and documentation for scikit-build-core

Changes:
- Updated documentation to show modern pip-based installation
- Updated manylinux build script to use pip wheel instead of setup.py
- Updated conda metadata generator to use pip install
- Removed deprecated PyPI preparation script (replaced by `python -m build --sdist`)

All build infrastructure now uses the new scikit-build-core build system.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Update manylinux Docker script for scikit-build-core

Removed SETUP_PY_ARGS since cmake options are no longer passed via
setup.py arguments. The new build system uses CMake directly via
scikit-build-core. Also fixed typo and updated install_root path.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Remove deprecated _py_backend custom build backend

The _py_backend was a custom setuptools build backend wrapper used
with the old setup.py build system. It's no longer needed with
scikit-build-core.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add uv package manager documentation

Added section on using uv (Astral's fast Python package manager) to
install and work with CoolProp. Includes examples for:
- Installing in current environment
- Creating new projects with CoolProp
- Running scripts with automatic environment management
- Development installations from source

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix sdist packaging and update scikit-build-core config

Changes:
- Updated pyproject.toml to use newer scikit-build-core config syntax
  - cmake.minimum-version → cmake.version
  - cmake.verbose → build.verbose
- Added sdist.include to ensure .version file is in source distributions
- Added .version to MANIFEST.in for completeness

This fixes the issue where building a wheel from an sdist would fail
due to missing .version file. Now sdist → wheel builds work correctly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Rename setup.py to deprecated_setup.py

The old setuptools-based build system has been fully replaced with
scikit-build-core. Renaming setup.py to deprecated_setup.py to:
- Clearly indicate it's no longer the primary build method
- Keep it available for reference and backward compatibility
- Prevent accidental use of the old build system

Users should now use: pip install .

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Update GitHub Actions workflows for new build system

Changes:
- Replace --replace-setup-py with --replace-version flag
- Update sdist build to use 'python -m build --sdist' instead of deprecated prepare_pypi.py
- Workflows now work with scikit-build-core build system

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix cibuildwheel to build from repository root

With scikit-build-core, the pyproject.toml is at the repository root,
not in wrappers/Python/. Updated cibuildwheel configuration:
- Changed package-dir from ./wrappers/Python/ to .
- Removed redundant CIBW_BEFORE_BUILD (dependencies are in pyproject.toml)
- Build dependencies are now automatically installed by pip from pyproject.toml

This fixes the "Multiple top-level packages discovered" error.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Set macOS deployment target to 11.0 in pyproject.toml

Configure MACOSX_DEPLOYMENT_TARGET=11.0 (Big Sur) in cibuildwheel config.
This matches the setting in GitHub Actions and ensures wheels are built
with consistent compatibility for macOS 11.0 and later.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Remove old pyproject.toml from wrappers/Python

This file was used by the old setuptools build system. With
scikit-build-core, the main pyproject.toml at the repository root
is now used for all Python packaging configuration.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Remove redundant PyPy skip selector from cibuildwheel

The 'pp*' skip selector was causing a warning because PyPy isn't enabled
in the build matrix anyway. Since we explicitly specify only CPython
versions in the build directive (cp38-*, cp39-*, etc.), the pp* skip is
unnecessary.

Fixes warning: "Invalid skip selector: 'pp*'. This selector matches a
group that wasn't enabled."

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Update fmtlib from 11.1.3 to 12.0.0

Updated the fmtlib submodule to the latest stable release (12.0.0).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Remove pdsim

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-11 13:07:10 -04:00

276 lines
8.7 KiB
Python

#!/usr/bin/env python
"""
Test script to compare wheel contents between old setup.py and new scikit-build-core builds.
This script:
1. Builds a wheel using the old setup.py approach
2. Builds a wheel using the new scikit-build-core approach
3. Extracts and compares the contents
4. Reports any differences
5. Exits with code 0 if identical, 1 if different
"""
import sys
import os
import tempfile
import subprocess
import zipfile
import shutil
import filecmp
import difflib
from pathlib import Path
from typing import Tuple, List, Set
# Get the repository root
REPO_ROOT = Path(__file__).parent.parent.absolute()
def run_command(cmd: List[str], cwd: Path, description: str) -> subprocess.CompletedProcess:
"""Run a command and handle errors."""
print(f" Running: {' '.join(cmd)}")
try:
result = subprocess.run(
cmd,
cwd=cwd,
capture_output=True,
text=True,
check=True
)
return result
except subprocess.CalledProcessError as e:
print(f"ERROR: {description} failed!")
print(f" Command: {' '.join(cmd)}")
print(f" Return code: {e.returncode}")
print(f" stdout: {e.stdout}")
print(f" stderr: {e.stderr}")
sys.exit(1)
def build_old_wheel(tmpdir: Path) -> Path:
"""Build wheel using old setup.py approach."""
print("\n" + "="*80)
print("Building wheel with OLD setup.py approach...")
print("="*80)
# Change to wrappers/Python directory
build_dir = REPO_ROOT / "wrappers" / "Python"
# Clean any previous builds
dist_dir = build_dir / "dist"
if dist_dir.exists():
shutil.rmtree(dist_dir)
# Build the wheel
run_command(
[sys.executable, "setup.py", "bdist_wheel"],
cwd=build_dir,
description="Old wheel build"
)
# Find the built wheel
wheels = list((build_dir / "dist").glob("*.whl"))
if not wheels:
print("ERROR: No wheel file found in dist/")
sys.exit(1)
wheel_path = wheels[0]
print(f" Built wheel: {wheel_path.name}")
# Copy to temp directory
dest = tmpdir / wheel_path.name
shutil.copy2(wheel_path, dest)
return dest
def build_new_wheel(tmpdir: Path) -> Path:
"""Build wheel using new scikit-build-core approach."""
print("\n" + "="*80)
print("Building wheel with NEW scikit-build-core approach...")
print("="*80)
# Build from repository root
run_command(
[sys.executable, "-m", "pip", "wheel", "--no-deps", "--no-build-isolation",
"-w", str(tmpdir), str(REPO_ROOT)],
cwd=REPO_ROOT,
description="New wheel build"
)
# Find the built wheel
wheels = list(tmpdir.glob("*.whl"))
if not wheels:
print("ERROR: No wheel file found")
sys.exit(1)
wheel_path = wheels[0]
print(f" Built wheel: {wheel_path.name}")
return wheel_path
def extract_wheel(wheel_path: Path, extract_dir: Path) -> Set[str]:
"""Extract wheel and return set of relative file paths."""
print(f"\n Extracting {wheel_path.name}...")
with zipfile.ZipFile(wheel_path, 'r') as zf:
zf.extractall(extract_dir)
# Get all files recursively
files = set()
for root, dirs, filenames in os.walk(extract_dir):
for filename in filenames:
filepath = Path(root) / filename
relpath = filepath.relative_to(extract_dir)
files.add(str(relpath))
print(f" Extracted {len(files)} files")
return files
def compare_file_contents(file1: Path, file2: Path) -> Tuple[bool, str]:
"""Compare two files. Returns (identical, diff_text)."""
# Binary comparison first
if filecmp.cmp(file1, file2, shallow=False):
return True, ""
# If different, try to generate a useful diff
try:
with open(file1, 'r', encoding='utf-8', errors='ignore') as f1:
lines1 = f1.readlines()
with open(file2, 'r', encoding='utf-8', errors='ignore') as f2:
lines2 = f2.readlines()
diff = list(difflib.unified_diff(
lines1, lines2,
fromfile=str(file1),
tofile=str(file2),
lineterm='',
n=3
))
if diff:
return False, '\n'.join(diff[:50]) # Limit diff output
except:
pass
return False, "(binary files differ)"
def compare_wheels(old_dir: Path, new_dir: Path, old_files: Set[str], new_files: Set[str]) -> bool:
"""Compare extracted wheel contents. Returns True if identical."""
print("\n" + "="*80)
print("Comparing wheel contents...")
print("="*80)
# Find differences in file lists
only_in_old = old_files - new_files
only_in_new = new_files - old_files
common_files = old_files & new_files
# Filter out metadata files and irrelevant files that are expected to differ
def is_ignorable(path: str) -> bool:
parts = Path(path).parts
filename = Path(path).name
return (len(parts) > 0 and
(parts[0].endswith('.dist-info') or
parts[0].endswith('.egg-info') or
path.endswith('RECORD') or
path.endswith('WHEEL') or
filename == '.DS_Store' or
filename == '.gitignore' or
path.startswith('_py_backend/')))
only_in_old = {f for f in only_in_old if not is_ignorable(f)}
only_in_new = {f for f in only_in_new if not is_ignorable(f)}
all_identical = True
# Report files only in old
if only_in_old:
print(f"\n⚠️ Files ONLY in OLD wheel ({len(only_in_old)}):")
for f in sorted(only_in_old)[:20]: # Limit output
print(f" - {f}")
if len(only_in_old) > 20:
print(f" ... and {len(only_in_old) - 20} more")
all_identical = False
# Report files only in new
if only_in_new:
print(f"\n⚠️ Files ONLY in NEW wheel ({len(only_in_new)}):")
for f in sorted(only_in_new)[:20]: # Limit output
print(f" - {f}")
if len(only_in_new) > 20:
print(f" ... and {len(only_in_new) - 20} more")
all_identical = False
# Compare common files (excluding binary .so files which are expected to differ)
non_binary_files = [f for f in common_files
if not is_ignorable(f) and not f.endswith('.so')]
print(f"\n Comparing {len(non_binary_files)} common non-binary files...")
print(f" (Skipping {len([f for f in common_files if f.endswith('.so')])} binary .so files)")
different_files = []
for relpath in sorted(non_binary_files):
file1 = old_dir / relpath
file2 = new_dir / relpath
identical, diff_text = compare_file_contents(file1, file2)
if not identical:
different_files.append((relpath, diff_text))
if different_files:
print(f"\n⚠️ Files with DIFFERENT contents ({len(different_files)}):")
for relpath, diff_text in different_files[:10]: # Limit output
print(f"\n File: {relpath}")
if diff_text:
print(" Diff preview:")
for line in diff_text.split('\n')[:20]:
print(f" {line}")
if len(different_files) > 10:
print(f" ... and {len(different_files) - 10} more files differ")
all_identical = False
# Summary
print("\n" + "="*80)
if all_identical:
print("✅ SUCCESS: Wheel contents are IDENTICAL!")
else:
print("❌ FAILURE: Wheel contents DIFFER!")
print(f" - Files only in old: {len(only_in_old)}")
print(f" - Files only in new: {len(only_in_new)}")
print(f" - Files with different content: {len(different_files)}")
print("="*80)
return all_identical
def main():
print("="*80)
print("CoolProp Wheel Comparison Test")
print("="*80)
print(f"Repository: {REPO_ROOT}")
with tempfile.TemporaryDirectory() as tmpdir_str:
tmpdir = Path(tmpdir_str)
# Create subdirectories
old_build = tmpdir / "old_build"
new_build = tmpdir / "new_build"
old_extract = tmpdir / "old_extract"
new_extract = tmpdir / "new_extract"
for d in [old_build, new_build, old_extract, new_extract]:
d.mkdir()
# Build both wheels
old_wheel = build_old_wheel(old_build)
new_wheel = build_new_wheel(new_build)
# Extract both wheels
old_files = extract_wheel(old_wheel, old_extract)
new_files = extract_wheel(new_wheel, new_extract)
# Compare contents
identical = compare_wheels(old_extract, new_extract, old_files, new_files)
# Exit with appropriate code
sys.exit(0 if identical else 1)
if __name__ == "__main__":
main()