Skip to main content

pymsbuild

This is a PEP 517 backend for building packages via MSBuild.

Configuration file

The file is named _msbuild.py, and is executed by running python -m pymsbuild.

The package definition specifies all the files that end up in the released packages.

from pymsbuild import *

METADATA = {
    "Metadata-Version": "2.1",
    "Name": "package",
    "Version": "1.0.0",
    "Author": "My Name",
    "Author-email": "myemail@example.com",
    "Description": File("README.md"),
    "Description-Content-Type": "text/markdown",
    "Classifier": [
        "Development Status :: 3 - Alpha",
        "Intended Audience :: Developers",
        "Programming Language :: Python :: 3.9",
    ],
}

PACKAGE = Package(
    "my_package",
    PyFile(r"my_package\*.py"),
    PydFile(
        "_accelerator",
        CSourceFile(r"win32\*.c"),
        CHeaderFile(r"win32\*.h"),
    ),
    Package(
        "subpackage",
        PyFile(r"subpackage\*.py"),
    ),
)

Usage

Rebuild the current project in-place.

python -m pymsbuild

Interactively generate the _msbuild.py file with project spec. (Or at least, it will, once implemented.)

python -m pymsbuild init

Build the project and output an sdist

python -m pymsbuild sdist

Build the project and output a wheel

python -m pymsbuild wheel

Clean any recent builds

python -m pymsbuild clean

Advanced Examples

Dynamic METADATA

Metadata may be dynamically generated, either on import or with the init_METADATA function. This function is called and must either return the metadata dict to use, or update METADATA directly.

However, if a PKG-INFO file is found adjacent to the configuration file, it will be used verbatim. Sdist generation adds this file, so all metadata is static from that point onward. init_METADATA is not called in this case.

from pymsbuild import *

METADATA = {
    "Metadata-Version": "2.1",
    "Name": "package",
    "Version": os.getenv("VERSION", "1.0.0"),
    "Author": "My Name",
    "Author-email": "myemail@example.com",
    "Description": File("README.md"),
    "Description-Content-Type": "text/markdown",
    "Classifier": [
        "Development Status :: 3 - Alpha",
        "Intended Audience :: Developers",
        "Programming Language :: Python :: 3.9",
    ],
}

def init_METADATA():
    if os.getenv("BUILD_BUILDNUMBER"):
        METADATA["Version"] = f"1.0.{os.getenv('BUILD_BUILDNUMBER', '')}"
    # Updated METADATA directly, so no need to return anything

Separate packages

Packages are just Python objects, so they may be kept in variables and used later. They also expose a members attribute, which is a list, so that members can be added or inserted later.

After the entire module is executed, the package in PACKAGE is the only one used to generate output.

P1 = Package(
    "submodule",
    PyFile(r"src\submodule\__init__.py")
)

P2 = Package(
    "submodule_2",
    PyFile(r"src\submodule_2\__init__.py")
)

PACKAGE = Package("my_package", P1)
PACKAGE.members.append(P2)

Dynamic packages

After import, if an init_PACKAGE(tag=None) function exists it will be called with the intended platform tag. It must modify or return PACKAGE. This function is called for in-place, sdist and wheel generation, however, for sdists (and any scenario that should not generate binaries), tag will be None. Otherwise, it will be a string like cp38-cp38-win32.

X64_ACCELERATOR = PydFile(
    "_my_package",
    CSourceFile(r"win32\*.c"),
    IncludeFile(r"win32\*.h"),
)

PACKAGE = Package(
    "my_package",
    PyFile(r"my_package\*.py"),
)

def init_PACKAGE(tag=None):
    if tag.endswith("-win_amd64"):
        PACKAGE.members.append(X64_ACCELERATOR)

Source offsets

If you keep your source in a src folder (recommended), provide the source= argument to Package in order to properly offset filenames. Because it is a named argument, it must be provided last.

This is important for sdist generation and in-place builds, which need to match package layout with source layout. Simply prefixing filename patterns with the additional directory is not always sufficient.

Note that this will also offset subpackages, and that subpackages may include additional source arguments. However, it only affects sources, while the package name (the first argument) determines where in the output the package will be located. In-place builds will create new folders in your source tree if it does not match the final structure.

PACKAGE = Package(
    "my_package",
    PyFile(r"my_package\__init__.py"),
    source="src",
)

Project file override

Both Package and PydFile types generate MSBuild project files and execute them as part of build, including sdists. For highly customised builds, this generation may be overridden completely by specifying the project_file named argument. All members are then ignored.

By doing this, you take full responsibility for a valid build, including providing a number of undocumented and unsupported targets.

Recommendations:

  • lock your pymsbuild dependency to a specific version in pyproject.toml
  • generate project files first and modify, rather than create new ones
  • read the pymsbuild source code, especially the targets folder
  • consider contributing/requesting your feature
PACKAGE = Package(
    "my_package",
    PydFile("_accelerator", project_file=r"src\accelerator.vcxproj")
)

Compiler/linker arguments

Rather than overriding the entire project file, there are a number of ways to inject arbitrary values into a project. These require familiarity with MSBuild files and the toolsets you are building with.

The Property element inserts a <PropertyGroup> with the value you specifiy at the position in the project the element appears.

PYD = PydFile(
    "module",
    Property("WindowsSdkVersion", "10.0.18363.0"),
    ...
)

The ItemDefinition element inserts an <ItemDefinitionGroup> with the type and metadata you specify at the position in the project the element appears.

PYD = PydFile(
    "module",
    ItemDefinition("ClCompile", PreprocessorDefinitions="Py_LIMITED_API"),
    ...
)

The ConditionalValue item may wrap any element value to add conditions or concatenate the value. This may also be used on source arguments for file elements.

    ...
    Property("Arch", ConditionalValue("x86", condition="$(Platform) == 'Win32'")),
    Property("Arch", ConditionalValue("x64", if_empty=True)),
    ...
    ItemDefinition(
        "ClCompile",
        AdditionalIncludeDirectories=
            ConditionalValue(INCLUDES + ";", prepend=True),
        ProprocessorDefinitions=
            ConditionalValue(";Py_LIMETED_API", append=True),
    ),
    ...

ConditionalValue may also be used to dynamically update values in the init_PACKAGE function, allowing you to keep the structure mostly static but insert values from the current METADATA (which is fully evaluated by the time init_PACKAGE is called).

VER = ConditionalValue("1.0.0")

PYD = PydFile(
    "module",
    Property("Version", VER),
    CSourceFile(r"src\*.c"),
    IncludeFile(r"src\*.h"),
)

def init_PACKAGE(tag):
    VER.value = METADATA["Version"]

As a last resort, the LiteralXml element inserts plain text directly into the generated file. It will be inserted as a child of the top-level Project element.

    ...
    LiteralXml("<Import Project='my_props.props' />"),
    ...

Release files for pymsbuild 0.0.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pymsbuild 0.0.5
File Size Uploaded
pymsbuild-0.0.5.tar.gz 15.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pymsbuild 0.0.5
File Interpreter ABI Platform
pymsbuild-0.0.5-py3-none-any.whl Python 3 none any Details

Total release size: 35.6 kB

Release files / pymsbuild-0.0.5.tar.gz

Download URL pymsbuild-0.0.5.tar.gz
Size 15.5 kB
Tags Source
SHA-256 checksum
How to use checksums
56ee6e226540cc183c2c8c67101b39cc5c9959f561d84d02e857cb075878502f
BLAKE2b-256 checksum
How to use checksums
f058c3694218c0e4ef3ea705f48a1f644d24df105edc30983610764cef3553be
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.1.1 pkginfo/1.5.0.1 requests/2.23.0 setuptools/41.2.0 requests-toolbelt/0.9.1 tqdm/4.46.0 CPython/3.8.3

Release files / pymsbuild-0.0.5-py3-none-any.whl

Download URL pymsbuild-0.0.5-py3-none-any.whl
Size 20.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
785b03aa188e2382601cb61e3b4e2e04e802aaba7a430ce8bf33d68080866bdc
BLAKE2b-256 checksum
How to use checksums
8b81f277a9f621bcaf9d4359f91a1800bedfb7e20db3916d96b14830d4ce66e8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.1.1 pkginfo/1.5.0.1 requests/2.23.0 setuptools/41.2.0 requests-toolbelt/0.9.1 tqdm/4.46.0 CPython/3.8.3

Release history Release notifications | RSS feed

1.2.3

2 release files

1.2.2

2 release files

1.2.1

2 release files

1.2.0

2 release files

1.1.7

2 release files

1.1.6

2 release files

1.1.5

2 release files

1.1.4

2 release files

1.1.3

2 release files

1.1.2

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.0

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

0.0.23

2 release files

0.0.22

2 release files

0.0.21

2 release files

0.0.19

2 release files

0.0.18

2 release files

0.0.17

2 release files

0.0.16

2 release files

0.0.15

2 release files

0.0.9

2 release files

0.0.8

2 release files

0.0.7

1 release file

0.0.6

3 release files

This release

0.0.5 This release

2 release files

0.0.4

2 release files

0.0.3

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page