diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6a8b68f..acc3c4a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: python-version: "3.14" - uses: astral-sh/setup-uv@v7 - - run: uvx --with tox-uv tox -e py,docs,style + - run: uvx --with tox-uv tox -e py,style - run: uv build - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bb36bd7..045b62f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -56,4 +56,4 @@ jobs: python-version: "3.14" - uses: astral-sh/setup-uv@v7 - - run: uvx --with tox-uv tox -e docs,style + - run: uvx --with tox-uv tox -e style diff --git a/.gitignore b/.gitignore index 3a2d099..282dc87 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ root/ venv*/ pp*.yml +uv.lock diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..ab3cfb4 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,74 @@ +## Architecture + +### Core Classes Hierarchy + +``` +ModuleBuilder (abstract base) +├── PythonBuilder (abstract, extends ModuleBuilder) +│ └── Cpython (concrete implementation in cpython.py) +└── External modules (in external/xcpython.py, xtkinter.py) + ├── Bdb, Bzip2, Gdbm, LibFFI, Mpdec, Openssl, Readline + ├── Sqlite, Uuid, Xz, Zlib, Zstd, TkInter + +BuildSetup (coordinates overall compilation) +├── BuildContext (handles isolation hacks on macOS) +├── Folders (manages build directory structure) +└── ModuleCollection (manages module selection and dependencies) + +PPG (Global state singleton) +├── config: Config (YAML configuration) +├── target: PlatformId (target OS/arch) +├── cpython: CPythonFamily (available versions) +└── families: dict (extensible family support) + +Config +├── Folders (build/dist/sources/logs directories) +├── target platform detection +├── YAML config merging (platform-specific overrides) + +PythonInspector +├── Inspect Python installation for portability +├── LibAutoCorrect (rewrite paths to be relative) +└── Report system and shared lib detection + +Tracker/Trackable +├── Categorizes found issues/objects by type +└── Provides detailed reports +``` + +### Key Design Patterns + +1. **Hierarchical Module Building**: External modules are compiled first, then Python itself, using the same build framework. + +2. **Environment Variable Injection**: `xenv_*` methods dynamically provide environment variables (CPATH, LDFLAGS, PATH, etc.) for compilation. + +3. **Platform Abstraction**: `PPG.target` (PlatformId) encapsulates platform logic. Compile methods named `_do__compile()` dispatch to platform-specific implementations. + +4. **Configuration Precedence**: YAML config supports platform-specific overrides (windows.ext, macos.env, etc.). Most specific setting wins. + +5. **Folder Masking (macOS)**: On macOS, `/usr/local` is temporarily masked with a RAM disk to prevent accidental dynamic linking. + +6. **Build Isolation**: All external modules compiled to a shared `build/deps/` folder, Python finds them via CPATH/LDFLAGS. + +7. **Lazy Version Fetching**: `VersionFamily` caches available versions, fetching from python.org on first access. + +8. **Telltale Detection**: Modules check for marker files (`m_telltale`) to determine if they're already available on the system (as shared libs). + +9. **Log Aggregation**: Each module logs to a separate file (`01-openssl.log`, `02-cpython.log`, etc.) under `build/logs/`. + + +## CI/CD + +### GitHub Actions + +**tests.yml** (main branch & PRs): +- Matrix test on py3.10, 3.11, 3.12, 3.13, 3.14 +- Runs: `uvx --with tox-uv tox -e py` +- Coverage upload to coveralls.io (parallel, then finish) +- Linter job: docs + style checks on 3.14 + +**release.yml** (version tags v*): +- Triggers on `v[0-9]*` tags +- Runs all tests + docs + style +- Builds distribution with `uv build` +- Publishes to PyPI via trusted publishing (OIDC) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7e54760 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,76 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What This Project Does + +portable-python is a CLI tool and Python library for compiling portable CPython binaries from source. Binaries are statically linked so they can be extracted to any folder and used without installation. Supports Linux and macOS (not Windows). + +## Common Commands + +```bash +# Run all tests (tox manages envs for py310-314, coverage, docs, style) +tox + +# Run tests for a single Python version +tox -e py313 + +# Run a single test +pytest tests/test_build.py::test_build_rc -vv + +# Lint check / auto-fix +tox -e style +tox -e reformat + +# Run tests directly (from venv) +pytest tests/ + +# CI uses: uvx --with tox-uv tox -e py +``` + +## Architecture + +**Key classes:** + +- `PPG` (versions.py) — Global singleton holding config, target platform, and version families. All modules access shared state through this. +- `BuildSetup` (__init__.py) — Coordinates overall compilation: downloads sources, builds external modules, then CPython. +- `ModuleBuilder` (__init__.py) — Abstract base for anything that gets compiled. Both external C modules and Python itself extend this. +- `PythonBuilder` (__init__.py) — Extends ModuleBuilder for Python implementations. +- `Cpython` (cpython.py) — Concrete PythonBuilder that handles CPython's configure/make/install, optimization, and finalization. +- `Config` (config.py) — Loads and merges YAML configuration (portable-python.yml) with platform-specific overrides. +- `PythonInspector` (inspector.py) — Validates portability of a built Python by checking shared library dependencies and paths. + +**External modules** (external/xcpython.py): `Bdb`, `Bzip2`, `Gdbm`, `LibFFI`, `Mpdec`, `Openssl`, `Readline`, `Sqlite`, `Uuid`, `Xz`, `Zlib`, `Zstd` — each is a ModuleBuilder subclass that compiles a C library statically before CPython is built. + +**Key patterns:** + +- Platform-specific compile logic uses `_do_linux_compile()` / `_do_macos_compile()` method dispatch. +- Environment injection: `xenv_*` methods provide CPATH, LDFLAGS, PATH etc. for compilation. +- On macOS, `/usr/local` is masked with a RAM disk (`FolderMask`) to prevent accidental dynamic linking. +- External modules compile to a shared `build/deps/` prefix; CPython finds them via CPATH/LDFLAGS. +- Telltale detection: modules check for marker files (`m_telltale`) to determine if system already has the library. +- No patches to upstream CPython source — relies solely on configure flags. + +**runez** is the foundational utility library (file ops, system info, CLI decorators, logging, Version/PythonSpec types). Check runez before reimplementing anything. + +**Additional pointers:** + +- `ModuleCollection.selected` contains only the modules chosen for a build — not all candidates. +- Build logs go to `build/logs/NN-modulename.log` (e.g. `01-openssl.log`, `02-cpython.log`). +- YAML config supports platform-specific overrides and path templates — see CONFIGURATION.md. +- See ARCHITECTURE.md for class hierarchy and design patterns, DEVELOP.md for common tasks and dependencies. + +## Testing + +- pytest with 100% code coverage target +- Tests mock `runez.run()` to avoid actual compilation — uses `--dryrun` mode +- `conftest.py` provides a `cli` fixture (from runez) and forbids HTTP calls (`GlobalHttpCalls.forbid()`) +- Sample YAML configs in `tests/sample-config*.yml` for testing configuration parsing + +## Linting + +Ruff handles all linting and formatting. Key settings in pyproject.toml: +- Line length: 140 +- McCabe complexity: max 18 +- Security checks (S rules) disabled in tests +- Numpy-style docstrings diff --git a/CONFIGURATION.md b/CONFIGURATION.md new file mode 100644 index 0000000..5dcc93d --- /dev/null +++ b/CONFIGURATION.md @@ -0,0 +1,53 @@ +# Configuration (portable-python.yml) + +portable-python is configured via a YAML file (default: `portable-python.yml` in current directory, override with `--config`). + + +## Key Sections + +### folders + +- `build`, `dist`, `sources`, `logs`, `destdir`, `ppp-marker` +- All support path templates: `{build}`, `{version}`, `{abi_suffix}` + +### cpython-modules (CSV) + +Which external modules to auto-select. +Default is configured in `DEFAULT_CONFIG` (see `src/portable_python/config.py`). +Examples: openssl, zlib, xz, sqlite, bzip2, gdbm, libffi, readline, uuid + +### cpython-configure (list) + +Extra `./configure` args for CPython. +Default includes: `--enable-optimizations`, `--with-lto`, `--with-ensurepip=upgrade` + +### cpython-clean-1st-pass (list) + +Files to remove before `compileall` — removes test files, idle, 2to3 (~94 MB savings). + +### cpython-clean-2nd-pass (list) + +Files to remove after `compileall` — removes pycaches for seldom-used libs (~1.8 MB savings). + + +## Per-module config + +Each external module can be customized with these keys (replace `{module}` with the module name, e.g. `openssl`): + +| Key | Purpose | +|-----|---------| +| `{module}-version` | Version to use | +| `{module}-url` | URL to download from | +| `{module}-src-suffix` | File extension if not in URL | +| `{module}-configure` | Custom configure args | +| `{module}-http-headers` | HTTP headers for download | +| `{module}-patches` | File patches to apply | +| `{module}-debian` | Package name on Debian (for dependency detection) | + + +## Platform-specific overrides + +Configuration supports platform-specific sections (e.g. `windows.ext`, `macos.env`, etc.). +The most specific setting wins. + +`MACOSX_DEPLOYMENT_TARGET` defaults to 13 (Ventura). diff --git a/DEVELOP.md b/DEVELOP.md index df23ae1..08ec5f1 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -3,9 +3,7 @@ Create a dev venv: ```shell -uv venv -uv pip install -r requirements.txt -r tests/requirements.txt -uv pip install -e . +uv sync ``` You can then run `portable-python` from that venv: @@ -66,3 +64,48 @@ Now inside docker, you run a build: ```shell portable-python build 3.13.2 ``` + + +# Key Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| click | <9 | CLI framework | +| pyyaml | <7 | Configuration parsing | +| requests | <3 | HTTP downloads | +| runez | <6 | Utilities (file ops, system, logging) | +| urllib3 | <3 | HTTP transport | +| pytest-cov | - | Coverage reporting (dev only) | + +**runez** is central: provides file ops (`ls_dir`, `touch`, `compress`/`decompress`), system info (platform detection), CLI decorators (`@click.group`), logging, and version handling (`Version`, `PythonSpec`). + + +# Common Tasks + +## Add a New External Module + +1. Create class in `src/portable_python/external/xcpython.py` extending `ModuleBuilder` +2. Set `m_name`, `m_telltale`, `m_debian`, `version` property +3. Implement `url` property (or override `_do_linux_compile()` / `_do_macos_compile()`) +4. Add to `Cpython.candidate_modules()` if it's a CPython sub-module +5. Add tests in `tests/test_setup.py` + +## Add a New Config Option + +1. Update `DEFAULT_CONFIG` in `src/portable_python/config.py` +2. Use `PPG.config.get_value("key")` to retrieve it in code +3. Add tests to `tests/test_setup.py` + +## Fix a Portability Issue + +1. Run `portable-python inspect ` to diagnose +2. If lib is being dynamically linked, add to module list or update isolation +3. Use `LibAutoCorrect.run()` logic (or extend it) to fix paths +4. Add test case to `tests/test_inspector.py` + +## Bump Python Support + +1. Update `pyproject.toml` classifiers and `requires-python` +2. Update `.github/workflows/tests.yml` matrix +3. Update `CPythonFamily.min_version` if needed +4. Run full test matrix with `tox` diff --git a/MANIFEST.in b/MANIFEST.in index 5863756..9d5d250 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,2 @@ include LICENSE include README.rst -include DEVELOP.md -include SECURITY.md -include requirements.txt diff --git a/README.rst b/README.rst index fb2677c..a89c81b 100644 --- a/README.rst +++ b/README.rst @@ -128,6 +128,64 @@ Note that you can use ``--dryrun`` mode to inspect what would be done without do Would tar build/3.9.7 -> dist/cpython-3.9.7-macos-x86_64.tar.gz +CLI reference +------------- + +Main entry point:: + + portable-python [OPTIONS] COMMAND [ARGS] + +**Global options**: + +- ``--config PATH``: Config file (default: ``portable-python.yml``) +- ``--quiet``: Turn off DEBUG logging +- ``--dryrun`` / ``-n``: Show what would be done +- ``--target PLATFORM``: Override detected platform (for testing) + + +**build** ```` - Build a portable Python binary:: + + portable-python build 3.13.2 -m openssl,zlib + +- ``--modules, -m CSV``: External modules to include +- ``--prefix, -p PATH``: Use ``--prefix`` (non-portable) + + +**build-report** ``[PYTHON_SPEC]`` - Show module status and what will be compiled: + +- ``--modules, -m CSV``: Specific modules to check +- Validates that modules can be built + + +**inspect** ```` - Check if a Python installation is portable:: + + portable-python inspect /usr/bin/python3 + +- ``--modules, -m MODULES``: Which modules to inspect +- ``--verbose, -v``: Show full ``.so`` report +- ``--prefix, -p``: Built with ``--prefix`` (not portable) +- ``--skip-so, -s``: Don't check all ``.so`` files + + +**list** ``[FAMILY]`` - List available versions (default: cpython):: + + portable-python list cpython + +- ``--json``: Output as JSON + + +**diagnostics** - Show system diagnostics + + +**recompress** `` `` - Re-compress existing binary tarball (for comparing compression sizes) + + +**lib-auto-correct** ```` - Auto-correct exes/libs to use relative paths: + +- ``--commit``: Actually perform changes (dryrun by default) +- ``--prefix, -p PATH``: Expected ``--prefix`` from build + + Library ------- diff --git a/pyproject.toml b/pyproject.toml index 7513e0f..ae8b1c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,81 @@ [build-system] -requires = ["setuptools", "setupmeta"] +requires = ["setuptools", "setuptools-scm"] build-backend = "setuptools.build_meta" +[tool.setuptools_scm] +local_scheme = "dirty-tag" + +[tool.setuptools.packages.find] +where = ["src"] + +[project] +name = "portable-python" +dynamic = ["version"] +description = "Portable python binaries" +readme = "README.rst" +requires-python = ">=3.10" +license = "MIT" +license-files = ["LICENSE"] +authors = [ + {name = "Zoran Simic", email = "zoran@simicweb.com"}, +] +keywords = ["python", "portable", "binary"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Operating System :: MacOS :: MacOS X", + "Operating System :: POSIX", + "Operating System :: Unix", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: Implementation :: CPython", + "Topic :: Software Development :: Build Tools", + "Topic :: System :: Installation/Setup", + "Topic :: System :: Software Distribution", + "Topic :: Utilities", +] +dependencies = [ + "click<9", + "pyyaml<7", + "requests<3", + "runez<6", + "urllib3<3", +] + +[dependency-groups] +test = [ + "pytest-cov", +] +dev = [ + { include-group = "test" }, +] + +[project.scripts] +portable-python = "portable_python.__main__:main" + +[project.urls] +Source = "https://github.com/codrsquad/portable-python" + +[tool.coverage.run] +concurrency = ["thread"] +data_file = ".tox/.coverage" +patch = ["subprocess"] + +[tool.coverage.xml] +output = ".tox/test-reports/coverage.xml" + +[tool.coverage.html] +directory = ".tox/test-reports/htmlcov" + +[tool.pytest.ini_options] +cache_dir = ".tox/.cache" + [tool.ruff] cache-dir = ".tox/.ruff_cache" line-length = 140 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 81dffc3..0000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -click<9 -pyyaml<7 -requests<3 -runez<6 -urllib3<3 diff --git a/setup.py b/setup.py deleted file mode 100644 index 7726114..0000000 --- a/setup.py +++ /dev/null @@ -1,41 +0,0 @@ -from setuptools import setup - -setup( - name="portable-python", - setup_requires=["setupmeta"], - versioning="dev", - author="Zoran Simic zoran@simicweb.com", - keywords="python, portable, binary", - url="https://github.com/codrsquad/portable-python", - python_requires=">=3.10", - entry_points={ - "console_scripts": [ - "portable-python = portable_python.__main__:main", - ], - }, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Intended Audience :: Developers", - "Operating System :: MacOS :: MacOS X", - "Operating System :: POSIX", - "Operating System :: Unix", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - "Programming Language :: Python :: Implementation :: CPython", - "Topic :: Software Development :: Build Tools", - "Topic :: System :: Installation/Setup", - "Topic :: System :: Software Distribution", - "Topic :: Utilities", - ], - project_urls={ - "Documentation": "https://github.com/codrsquad/portable-python/wiki", - "Release notes": "https://github.com/codrsquad/portable-python/wiki/Release-notes", - "Source": "https://github.com/codrsquad/portable-python", - }, -) diff --git a/tests/requirements.txt b/tests/requirements.txt deleted file mode 100644 index c75c448..0000000 --- a/tests/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -pytest-cov diff --git a/tox.ini b/tox.ini index 41e7456..651da7e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,11 @@ [tox] -envlist = py{310,311,312,313,314}, coverage, docs, style +envlist = py{310,311,312,313,314}, coverage, style skip_missing_interpreters = true [testenv] setenv = COVERAGE_FILE={toxworkdir}/.coverage.{envname} usedevelop = True -deps = -rrequirements.txt - -rtests/requirements.txt +dependency_groups = test commands = pytest {posargs:-vv --cov=src/ --cov=tests/ --cov-report=xml tests/} [testenv:coverage] @@ -30,33 +29,3 @@ deps = ruff commands = ruff check --fix ruff format -# Old check-manifest + readme-renderer, see if there's better out there nowadays -[testenv:docs] -skip_install = True -deps = check-manifest - readme-renderer - pip - setuptools -commands = check-manifest - python setup.py check --strict --restructuredtext - -[check-manifest] -ignore = .dockerignore - Dockerfile - scripts/* - portable-python*.yml - retired/** - tests/** - tox.ini - -[coverage:run] -concurrency = - thread -patch = subprocess -[coverage:xml] -output = .tox/test-reports/coverage.xml -[coverage:html] -directory = .tox/test-reports/htmlcov - -[pytest] -cache_dir = .tox/.cache