Pymsbuild build backend

Figure I ought to make some kind of announcement, though I’m not really expecting this to be of broad interest.

I’ve finally bitten the bullet and made a PEP 517 backend that does what I need. Quite literally, it covers my needs, and likely nobody else’s :smiley:

PyPI page is here: https://pypi.org/project/pymsbuild/

Firstly, it only works on Windows and always requires Visual Studio to be installed (possibly just Build Tools, but I haven’t tried that yet). This is because it generates an MSBuild project to do most of the heavy lifting. [1]

The downside of this is that it’s somewhat overkill for plain Python packages. That’s fine, the only one of those I’ve done recently is this package :wink:

The upside is you get much better incremental builds for native packages, including (as I shake the bugs out) in-tree builds (which are essentially “installs” into the source tree, as all build artifacts naturally live in separate directories and you have to work harder to mix them up with your sources).

Secondly, you have to specify metadata manually and precisely - there’s no validation built in at all. Even the metadata version is something you have to specify! I trust myself to get this right (a.k.a. clean up my own mess when I get it wrong), and it was much simpler to code!

But probably the biggest feature (for me) is that you can just specify files and where they should go in the final package! My biggest annoyance with all the rest is trying to coerce them to put certain files in certain places, so now it’s easy to make thing go where I want (under my top-level package directory).

I don’t really see this as competing with any other build tools, and I’ll likely keep using those too (or at least setuptools, which tends to annoy me less than flit), and it won’t build up a huge userbase I’m sure. But I’m already finding it a very useful escape hatch for complex package builds (my current project has multiple Cython modules in a PEP 420 namespace package with adjacent type stubs).

1: Although, MSBuild now runs anywhere .NET Core runs, so it could become cross-platform. But I’ll do that when I need it :wink:

6 Likes

(It’s okay to revive my own zombie thread, right?)

I’ve spent a few hours over the last week fleshing out pymsbuild with some features that I either needed or wanted to experiment with, and I feel like it’s getting close to being declared 1.0. So I thought I’d summarise some of the unique highlights.

As a reminder, pymsbuild isn’t a workflow tool, it’s just a build backend. The entry point is the pyproject.toml, and if your frontend knows how to build from that, it’ll be fine.

Everything’s documented on the PyPI page or GitHub. These are just teasers to see if something sparks your interest.

DLL packing

A Windows-centric name, because I can, but it works with gcc on Ubuntu (and probably elsewhere, but I only got it going tonight and haven’t tried it anywhere else yet :smiley: ). Basically, this takes a “normal” package definition and instead of making a directory of files, it makes an extension module that contains all the files and an importlib loader to load them (or to redirect nested extension modules to a separate file).

Two-stage build

An essential for me, this lets you run the build step for the package and the packing step into a wheel or sdist separately. Specify a layout directory and the build will run up to that point. Then you can add/remove/modify anything in there (e.g. adding an SBOM or doing code signing). Run the pymsbuild pack command on that directory and it’ll finish the job.

Wildcards and flattening

Boy this was some fun code to write. Use as complex a recursive wildcard as you like, it’ll find all the files it can relative to your packages source directory (hopefully called src), track them relative to the sdist root, relocate them in the same structure to your output/wheel, optionally collapse the tree structure (or replace separators with a different character), and there are some clever renaming tricks if you know the MSBuild %(Name)%(Extension) style syntax.

Complete project override

Sometimes you just know better than the tool. All the package-type elements (Package, PydFile) generate MSBuild projects that properly handle “normal” packages (including incremental build, separated temporary directories, multitargeting and cross-compilation), but you can also just store an MSBuild project in the repo/sdist and trigger it as part of the build. The project= argument suppresses complex generation and gives you the simplicity of a static build.[1]

Encrypted packages

Did I mention you can use AES encryption on the DLL packed packages? On Windows, at least. I’m not sure why you’d bother really, but it was a bit of fun to code up. And if you’ve got a way to hide the key from people, maybe it’s of some value to you?

Override the config path

I have a lot of projects that live together, for many good and bad reasons. But my tools don’t stop me from doing it. The default config file name is _msbuild.py, but you can pass a different name on the command line. When you make an sdist, it gets renamed back to the default, so that the sdist builds without needing the override.

Linux support

Turns out, dotnet run supports exactly the same syntax and tasks as MSBuild on Windows, so if you’ve installed the .NET SDK you can also use this to build your packages on Linux. Still kinda experimental, as I haven’t really used it a lot so don’t know how much it’s broken, but every time I do use it I un-break it a little bit more!

Cython

I use Cython a lot. There are special types you can pull into your package definition to automatically use Cython on source files.

If anything, it’s a decent example of extensibility by overriding existing types and adding the MSBuild targets files that do the real work. But I also use Cython a lot, so this has been essential.

Dynamic metadata and packages

Yeah, I know, people will hate this. But it turns out to be fairly unavoidable, so I’ve exposed it in the safest way I can that is still useful.

When building an sdist, your init_METADATA function will get called to let you change anything in your METADATA dict. Wheels don’t get this though, they’ll have exactly the same metadata as the sdist did. Mostly meant for messing with version numbers, though it can also update the BuildSdistRequires and BuildWheelRequires fields to add in dynamic build dependencies.

When building an sdist or wheel, your init_PACKAGE function gets called to let you modify the actual package contents, and you get the intended wheel tag so you know what the target is. MSBuild has enough conditional compilation stuff that this isn’t all that useful, but occasionally it’s a livesaver to get to change the package around based on the target version/platform.

Summary

That all was the summary! But yeah, I’m still not heavily pushing this as a competitor to any other backends (not even at work, where I probably could gain some real traction if I wanted). It’s mostly scratching my own itches, but there’s a chance someone else has those as well, so here’s the info to let you know if it might help you.

With a bit of luck (and free time), it should hit 1.0 before the end of the year.


  1. Okay, that’s a bit tongue in cheek. Nothing about MSBuild is “simple” :stuck_out_tongue: but it can be pretty static. ↩︎

5 Likes

Could you clarify what you mean by a “static build” here?

Also is it easy to use pymsbuild with mingw? (Or have I misunderstood the level at which pymsbuild is used so that this is not even a relevant question?)

A static project file/build definition, rather than regenerating it from the definition in the _msbuild.py file. Nothing to do with a statically linked Python runtime, in case that’s what you’re thinking.

Right now… maybe? But it could be. If you set PYMSBUILD_PLATFORM env var to linux_x86_64 it ought to pick up the set of targets that will use gcc rather than MSVC, though you’ll also need to override the wheel tag, ABI tag and target extension to get back to Windows markers.

But it’s definitely possible to add a platform tag that will choose mingw compatible settings by default. And MSBuild is really just a task runner that happens to come with a VS-centric set of default tasks, so there’s nothing stopping it from being used here apart from someone writing the more specific tasks.

(In fact, it wouldn’t surprise me if someone has already made a set of MSBuild target files for C++ projects using mingw. If so, those can probably be more or less dropped in.)