Skip to content

Instantly share code, notes, and snippets.

@LalatenduMohanty
Created January 13, 2026 16:46
Show Gist options
  • Select an option

  • Save LalatenduMohanty/0c690b745566cdd5a479506aa40da4d2 to your computer and use it in GitHub Desktop.

Select an option

Save LalatenduMohanty/0c690b745566cdd5a479506aa40da4d2 to your computer and use it in GitHub Desktop.
This document describes the new `patchsettings` module, which introduces a declarative, YAML-based configuration system for applying patches and modifications to source distributions (sdists) and wheel metadata during the build process.

Design Document: Patch Settings Module

Author: Christian Heimes (tiran)
Branch: tiran:fromager:patchsettings
Commit: 0879376fa0a4c1c4dd761fdbb85c3118332af8d5
Date: January 12, 2026

Summary

This document describes the new patchsettings module, which introduces a declarative, YAML-based configuration system for applying patches and modifications to source distributions (sdists) and wheel metadata during the build process.

Motivation

Current State

Fromager currently supports patching packages through:

  1. Patch files (.patch files in patches/<package>-<version>/)
  2. project_override settings in YAML configuration for pyproject.toml modifications
  3. Plugin hooks for programmatic customization

Problems with Current Approach

  1. Patch files are inflexible: .patch files require exact line matching and are brittle across versions
  2. Limited declarative options: project_override only handles pyproject.toml [build-system] modifications
  3. Plugin overhead: Simple modifications require writing Python code in a plugin module
  4. Version targeting is coarse: Patches are either version-specific (directory-based) or apply to all versions
  5. No metadata patching: No declarative way to modify wheel METADATA (install requirements)

Goals

  • Provide a declarative, YAML-based approach for common patching operations
  • Support version-conditional patches using specifier sets (e.g., >=1.2, <2.0)
  • Enable sdist modifications (source files, pyproject.toml, PKG-INFO)
  • Enable dist-info metadata modifications (wheel METADATA, dependencies)
  • Reduce the need for custom plugin code for common scenarios

Architecture

Module Location

src/fromager/patchsettings.py

Class Hierarchy

PatchBase (abstract base)
├── SdistPatchBase (abstract base for sdist operations)
│   ├── PatchReplaceLine      - regex line replacement
│   ├── PatchDeleteLine       - regex line deletion
│   ├── PatchPyProjectBuildSystem - pyproject.toml [build-system] modifications
│   └── FixPkgInfoVersion     - PKG-INFO metadata version fix
│
└── DistInfoMetadataPatchBase (abstract base for wheel metadata operations)
    ├── RemoveInstallRequires - remove install dependencies
    └── PinInstallRequiresToBuildVersion - pin dep to build env version

Discriminated Union Pattern

The module uses Pydantic's discriminated union pattern with the op field as the discriminator:

Patch = typing.Annotated[
    PatchReplaceLine
    | PatchDeleteLine
    | PatchPyProjectBuildSystem
    | FixPkgInfoVersion
    | RemoveInstallRequires
    | PinInstallRequiresToBuildVersion,
    pydantic.Field(..., discriminator="op"),
]

This allows YAML configurations to specify the operation type via the op key, with Pydantic automatically selecting the correct model class.

Detailed Design

Base Classes

PatchBase

Common fields for all patch operations:

Field Type Description
step ClassVar Build phase: "sdist" or "dist-info-metadata"
op str Operation discriminator (e.g., "replace-line")
title str Human-readable description
when_version SpecifierSet | None Version specifier for conditional application
ignore_missing bool Don't fail if operation doesn't modify anything

SdistPatchBase

Base for operations on unpacked source distributions. Defines execute() method signature:

def execute(
    self,
    *,
    ctx: context.WorkContext,
    req: Requirement,
    version: Version,
    sdist_root_dir: pathlib.Path,
) -> None:

DistInfoMetadataPatchBase

Base for operations on wheel metadata. Defines execute() method signature:

def execute(
    self,
    *,
    ctx: context.WorkContext,
    req: Requirement,
    version: Version,
    wheel_root_dir: pathlib.Path,
    build_env: build_environment.BuildEnvironment,
) -> None:

Patch Operations

1. PatchReplaceLine (op: replace-line)

Regex-based line replacement in source files.

Fields:

  • files: List of file glob patterns
  • search: Regex pattern to match
  • replace: Replacement string (supports backreferences)

Use Case: Comment out problematic requirements, fix version strings

2. PatchDeleteLine (op: delete-line)

Regex-based line deletion from source files.

Fields:

  • files: List of file glob patterns
  • search: Regex pattern to match

Use Case: Remove unwanted dependencies from requirements files

3. PatchPyProjectBuildSystem (op: pyproject-build-system)

Modify pyproject.toml [build-system] section.

Fields:

  • update_build_requires: List of requirements to add/update
  • remove_build_requires: List of package names to remove

Replaces: Existing project_override setting in package settings

Use Case: Add missing build dependencies, remove incompatible build tools

4. FixPkgInfoVersion (op: fix-pkg-info)

Fix PKG-INFO metadata version in sdist.

Fields:

  • metadata_version: Target metadata version (default: "2.4")

Use Case: Fix sdists with invalid or outdated metadata versions

5. RemoveInstallRequires (op: remove-install-requires)

Remove an install dependency from wheel METADATA.

Fields:

  • requirement: Requirement specifier to remove
  • name_only: If true, match only by package name (ignore version specifiers)

Use Case: Remove optional/problematic runtime dependencies

6. PinInstallRequiresToBuildVersion (op: pin-install-requires-to-build)

Pin an install requirement to the version in the build environment.

Fields:

  • requirement: Requirement to pin

Use Case: Ensure wheel uses exact versions of build-time dependencies

Custom Pydantic Types

SpecifierSetType

Pydantic-aware wrapper for packaging.specifiers.SpecifierSet:

class SpecifierSetType(SpecifierSet):
    @classmethod
    def __get_pydantic_core_schema__(cls, source_type, handler) -> CoreSchema:
        return core_schema.with_info_plain_validator_function(
            lambda v, _: SpecifierSet(v),
            serialization=core_schema.plain_serializer_function_ser_schema(
                str, when_used="json"
            ),
        )

RequirementType

Pydantic-aware wrapper for packaging.requirements.Requirement:

class RequirementType(Requirement):
    @classmethod
    def __get_pydantic_core_schema__(cls, source_type, handler) -> CoreSchema:
        return core_schema.with_info_plain_validator_function(
            lambda v, _: Requirement(v),
            serialization=core_schema.plain_serializer_function_ser_schema(
                str, when_used="json"
            ),
        )

Configuration Format

Example YAML

patch:
  - title: Comment out 'foo' requirement for version >= 1.2
    op: replace-line
    files:
      - 'requirements.txt'
    search: '^(foo.*)$'
    replace: '# \1'
    when_version: '>=1.2'
    ignore_missing: true

  - title: Remove 'bar' from constraints.txt
    op: delete-line
    files:
      - 'constraints.txt'
    search: 'bar.*'

  - title: Remove 'somepackage' installation requirement
    op: remove-install-requires
    requirement: somepackage

  - title: Fix PKG-INFO metadata version
    op: fix-pkg-info
    metadata_version: '2.4'
    when_version: '<1.0'

  - title: Add missing setuptools to pyproject.toml
    op: pyproject-build-system
    update_build_requires:
      - setuptools

  - title: Remove Ray installation requirement
    op: remove-install-requires
    requirement: ray
    name_only: true

  - title: Update Torch install requirement to version in build env
    op: pin-install-requires-to-build
    requirement: torch

Root Model

class Root(pydantic.BaseModel):
    model_config = MODEL_CONFIG
    patch: pydantic.RootModel[list[Patch]]

Integration Points

With Existing Components

Component Integration
packagesettings.py Could embed Patches as a field in PackageSettings
sources.py:prepare_new_source() Execute sdist step patches after unpacking
wheels.py:add_extra_metadata_to_wheels() Execute dist-info-metadata step patches
pyproject.py PatchPyProjectBuildSystem supersedes project_override

Build Pipeline Flow

1. Download sdist
2. Unpack sdist
3. Apply .patch files (existing)
4. Apply sdist-step patch settings (NEW)
   - PatchReplaceLine
   - PatchDeleteLine  
   - PatchPyProjectBuildSystem
   - FixPkgInfoVersion
5. Build wheel
6. Apply dist-info-metadata-step patch settings (NEW)
   - RemoveInstallRequires
   - PinInstallRequiresToBuildVersion
7. Pack wheel

Implementation Status

The commit introduces the data models only. The following work remains:

TODO

  1. Execution Logic: Implement execute() methods for each patch class
  2. Integration: Add patch execution calls to sources.py and wheels.py
  3. Settings Integration: Add patch field to PackageSettings model
  4. Deprecation: Plan migration from project_override to PatchPyProjectBuildSystem
  5. Documentation: Add user documentation and examples
  6. Tests: Unit tests for each patch operation
  7. Validation: File existence checks, regex compilation validation

Comparison with Existing Features

Feature project_override patchsettings
Scope pyproject.toml only Files, pyproject, PKG-INFO, wheel metadata
Version targeting None when_version specifier sets
Failure handling None ignore_missing option
Discoverability Limited title field for documentation
Extensibility Fixed Discriminated union pattern

Security Considerations

  • Regex DoS: Complex regex patterns could cause performance issues
  • Path traversal: File patterns should be validated to stay within sdist root
  • Build environment access: PinInstallRequiresToBuildVersion requires build env access

Future Extensions

  1. Additional operations:

    • add-line - append lines to files
    • update-install-requires - modify version specifiers
    • add-entry-point - add console scripts
  2. Conditional operations:

    • when_python - Python version conditions
    • when_platform - Platform-specific patches
  3. Operation chaining:

    • Reference previous operation results
    • Conditional execution based on prior outcomes

References

@LalatenduMohanty
Copy link
Author

  • Code Duplication with packagesettings.py . Recommendation extract MODEL_CONFIG to a shared module (e.g., src/fromager/settings_base.py).
  • PackageVersion vs SpecifierSetType Inconsistency. packagesettings.py already has a PackageVersion Pydantic type, but patchsettings.py creates new SpecifierSetType and RequirementType wrappers with a different validation pattern.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment