Enscons Documentation#

Enscons provides a set of Builders and utility functions for SCons to build Python distribution wheels. It provides a PEP 517 build backend, compatible with popular build frontends such as Pip and Build.

Project Layout#

To get started, your project must have at least these two files in the project’s top level directory: a pyproject.toml and an SConstruct file.

pyproject.toml#

At a minimum, your pyproject.toml file must contain the build-system requirements and build-backend.

[build-system]
requires = ["enscons"]
build-backend = "enscons.api"

This tells build frontends such as pip to build the package using Enscons. Add any other build-time requirements to the requires list here. This should only list requirements needed to build the wheel. Runtime dependencies should not be specified here.

It is recommended to add other static project metadata to a [project] section in this file in accordance with the PyPA specification on Declaring Project Metadata Here’s an example showing the essential items that Enscons knows about:

[project]
name = "distribution_name"
description = "Project Description"
version = "0.0.1"
license = "MIT"
authors = [
    {name = "Author Name", email = "authoremail@example.com"}
]
readme = "README.md"
requires-python: ">=3.8"
dependencies = ["click", "requests"]
keywords = ["keyword1", "keyword2"]
classifiers = [
    "Programming Language :: Python :: 3",
]
url = "https://example.com/myproject"

# This specifies the root directory which will be added to the python path when
# installing in editable mode (pip install -e).
src_root = "."

[project.scripts]
my-script = "packagename.modulename:main"

[project.optional-dependencies]
more = ["sphinx"]

SConstruct#

This file defines how to build your package. It may be helpful to familiarize yourself with SCons by reading the SCons Crash Course but here’s a quick primer:

SCons is a build utility similar to make but which uses a Python script to declare targets and the dependency graph. This script is named SConstruct but may also be called SConstruct.py or sconstruct.py which may be helpful to enable syntax highlighting in your editor.

An SCons script defines Builders which will declare how to build targets from sources. Sources and targets are typically files or directories, and are collectively known as nodes. The nodes then form a dependency graph with the builders as edges.

A basic SConstruct file for building a Python wheel consists of an Environment, a Whl builder, a WhlFile builder, and an SDist builder.

Here’s a simple and complete example. The following sections will go over each part in more detail.

import pytoml as  toml
import enscons

metadata = toml.load(open("pyproject.toml"))["project"]
tag = "py3-none-any"

env = Environment(
    tools=["default", "packaging", enscons.generate],
    PACKAGE_METADATA=metadata,
    WHEEL_TAG=tag,
)

py_sources = env.Glob("mypackage/*.py")

purelib = env.Whl("purelib", py_sources)
whl = env.WhlFile(purelib)
sdist = env.SDist(env.FindSourceFiles() + ["PKG-INFO", "README.md"])

# Sets the default target when scons is run on the command line with no target
env.Default(whl, sdist)

Note that the Environment class is not imported. It is injected into the script’s globals by the SCons runtime.

Creating the Environment#

The SConstruct Environment object should be created as shown:

metadata = toml.load(open("pyproject.toml"))["project"]
tag = "py3-none-any"

env = Environment(
    tools=["default", "packaging", enscons.generate],
    PACKAGE_METADATA=metadata,
    WHEEL_TAG=tag,
)

The tools parameter defines which plugins are loaded into this environment. We’ll want to add the default and packaging tools provided by SCons, as well as the enscons tool, which loads the enscons builders.

Additional keyword arguments set the Environment’s construction environment variables. (this is different than the system process’s environment). Two variables are required:

The PACKAGE_METADATA should typically come from the pyproject.toml file’s [project] section as shown, but doesn’t have to. Most project metadata keys are accepted and incorporated into the wheel’s metadata.

The wheel tag should be set to the compatibility tag of the wheel. See WHEEL_TAG for more information.

More environment variables may be defined, and are described in the [Environment Variables] section.

Environment Builders#

env.Whl(category, source, root=None)#

Copies files into a temporary directory which holds the contents of the wheel.

Parameters:
  • category (str) – “purelib”, “platlib”, “headers”, “data”, etc.

  • source – files belonging to category

  • root – relative to root directory, i.e. “.”, “src”

Returns:

A list of file nodes for each file added

This should be called at least once to add members into the wheel file. Typically, this would be called once to add all Python sources to the wheel. It may be called more than once to add additional sources to the wheel.

For pure Python wheels, you’ll typically want to use a WHEEL_TAG ending in “none-any” in the environment, and set the category here to “purelib”. Source files passed in will be copied into the root of the wheel.

For wheels with a binary compiled or platform-specific component, you’ll want to use “platlib” along with a platform-specific WHEEL_TAG. Source files passed in will be copied into the root of the wheel.

Any other category will add files into the corresponding subdirectory of the WHEEL_DATA_PATH directory (e.g. projectname-0.0.1.data/category)

Returns a list of nodes for the files added to the wheel (both source files and metadata files). The returned nodes should later be passed in to env.WhlFile() as the list of sources.

root determines the directory for which the sources are relative to. Source files are copied to the target directory along with all path components between root and the file itself.

env.WhlFile([target=None, ]source=None)#

Build the wheel archive from the given sources. If a single positional parameter is given, it is taken to be the source parameter.

Parameters:
  • source – A list of file nodes to add to the wheel archive. Typically this is a list of nodes as returned from env.Whl() (or the concatenated list from all calls)

  • target (str) – The path to the wheel file being created. By default this is WHEEL_FILE

Returns:

A file node for the resulting wheel file.

Calling this also adds the build target “bdist_wheel”.

env.SDist(target=None, source=None)#

Creates a source distribution

Parameters:
  • target – The path to the source distribution output

  • source – A list of source files to include

Returns:

The source dist node

n.b. Only explicitly named sources are added to the source distribution. To make this easier, the env.FindSourceFiles() function is convenient starting point. It discovers all files that are sources to some other SCons build target.

You will need to explicitly add any other metadata files you want to include, such as PKG-INFO, pyproject.toml, SConstruct, etc. (PKG-INFO is automatically generated by enscons when named as a source)

e.g.

sdist = env.SDist(env.FindSourceFiles() + [
    "PKG-INFO", "README.md", "pyproject.toml", "SConstruct"
])

Calling this also adds a build target named “sdist”.

Environment Variables#

These variables are settable using kwargs to the Environment() constructor.

PACKAGE_METADATA#

Package metadata used by the rest of the wheel building code. This is typically pulled from the “project” section of the pyproject.toml file as shown

metadata = toml.load(open("pyproject.toml"))["project"]

This variable is required.

Most items described in the PyPA Project Metadata Specification are accepted and incorporated into the package’s metadata.

WHEEL_TAG#

This specifies the compatibility tags for the wheel being built, indicating the platform the wheel is compatible with. It is a hyphen-delimited string specifying the the python tag, abi tag, and platform tag.

Common examples are py38-none-none for pure Python 3.8+, or cp38-abi3-manylinux2014_x86_64 for Python 3.8+ with binary compiled libraries for linux x86_64 and conforming to the abi3 binary interface and manylinux2014 platform policy.

This variable is required.

Note

The short-short version is to use the most preferred pure-python tag e.g. py38-none-any for pure-python wheels, or the most preferred architecture dependent tag e.g. the tag returned from enscons.get_binary_tag() for wheels with compiled or architecture dependent components.

Choosing the correct compatibility tags can be tricky. This section will serve as a brief guide on choosing the tags. Useful functions provided by the Packaging library are referenced in this section.

Python Tag

This indicates which Python interpreter your distribution is compatible with. Common Python tags are py3 for any Python 3 interpreter, cp3 for CPython 3, py38 / cp38 for Python / CPython 3.8 and up.

Builds targeting other Python interpreters would specify a different tag here. For example, PyPy uses pp. The current interpreter’s tag is returned by packaging.tags.interpreter_name().

Additional info in PEP 425’s section on Python Tags

ABI tag

If your distribution includes compiled extension modules, this indicates the ABI required.

If your distribution is pure-python, this should be set to none. If your extension module is complied against e.g. CPython 3.8, this should generally be set to cp38.

If your extension modules conform to the Python Limited API then you can use the abi3 tag. This will allow wheels built for a previous version of Python to be compatible with newer versions, despite having compiled C extension modules.

Platform Tag

This tag encodes what your wheel requires of the rest of the computing environment, and is where you declare if it runs on e.g. Mac, Windows, or Linux.

The compatibility tag specification can be found at the PyPA page on Platform Compatibility Tags. Below is a brief synopsis.

any is used for pure Python wheels which don’t have any platform-specific components.

Otherwise, this should encode the platform requirements. Common examples are: linux_x86_64, win_amd64, macosx_10_9_x86_64.

Since Linux platforms vary so wildly, wheels tagged with a generic linux_* are not uploadable to PyPI. Instead, standards for Linux compatibility called manylinux are described in PEP 600, and specific requirements for the latest manylinux2014 policy are found in PEP 599.

It’s recommended to build your wheels on CentOS7 for compatibility with the manylinux2014 policy. This ensures compatilitiy with most modern Linux platforms. PyPA maintains a set of Manylinux Docker Images for the purpose of building Linux wheels.

A full Manylinux platform tag consists of the specific manylinux policy keyword and architecture, e.g. manylinux2014_x86_64 or manylinux_2_17_x86_64.

Note that “manylinux2014” and “manylinux_2_17” are aliases of each other and refer to the same policy. The general tag form is manylinux_GLIBCMAJOR_GLIBCMINOR_ARCH for compatibility with a minimum glibc version and architecture.

The Auditwheel tool can scan built wheels to determine if they use any symbols which do not conform to manylinux.

See which platforms your current interpreter supports with packaging.tags.platform_tags(). The current architecture part of this tag comes from sysconfig.get_platform() with period . and hyphen - replaced with underscores _.

As a convenience, the following helper functions are available:

enscons.get_binary_tag()#

Returns the most specific binary tag for the current platform. Use this if built wheels are only guaranteed to run on the exact platform and interpreter that they were built on.

This should be the tag used for architecture-dependent wheels unless you specifically intend to provide cross-Python compatibility with other ABIs and/or platforms.

enscons.get_universal_tag()#

Returns “py2.py3-none-any” for a pure python distribution compatible with both Python 2 and 3.

You probably don’t want to use this unless you’re still supporting and testing on Python 2. Use py3-none-any for pure Python distributions supporting only Python 3.

enscons.get_abi3_tag()#

Returns the first abi3 tag, or the first binary tag if abi3 is not supported. Use this if compiled extension modules only access Python functions in accordance with the Limited API.

Note: If you’re also targeting a manylinux platform, this function may not return the correct tag.

WHEEL_PATH#

The temporary directory which to build the wheel contents. Files will be copied / generated in this directory, and zipped into a wheel file by the env.WhlFile() function.

This should be set to a directory node (env.Dir()). By default it is set to env.Dir("#build/wheel/").

ROOT_IS_PURELIB#

If the wheel to create is a pure-python library, this should be set to True.

By default, this is set to True if the WHEEL_TAG ends in “none-any”.

This sets the Root-Is-Purelib line in the WHEEL metadata file, and also determines whether “platlib” or “purelib” calls to env.Whl() are copied into the wheel path (as opposed to a data directory)

Command Line Options#

The following command line options are available. Both the Environment variable and the command line option are given for each option. e.g. --wheel_dir=build will be available as env["WHEEL_DIR"].

You will not usually need to set this. They are used by the PEP 517 backend to pass information from the build frontend.

--wheel_dir WHEEL_DIR#

env["WHEEL_DIR"]

Sets the directory which to output generated wheel files.

Default: “dist”.

--dist_dir DIST_DIR#

env["DIST_BASE"]

Sets the directory which to output generated source distributions.

Default: “dist”

--egg_base EGG_INFO_PREFIX#

env["EGG_INFO_PREFIX"]

Sets the directory prefix for the EGG_INFO_PATH variable. If not set, the value is pulled from the package metadata’s src_root value. If neither is set, the EGG_INFO_PATH directory will be created in the current directory.

Generated Environment Variables#

These environment variables are available after the call to env.Whl().

These variables are not settable, and are used internally by enscons. They are provided here as a reference.

WHEEL_DATA_PATH#

This is set to the <package name>-<package version>.data directory within WHEEL_PATH

DIST_INFO_PATH#

This is set to the <package name>-<package version>.dist-info directory within WHEEL_PATH

PACKAGE_NAME#

The name item from the PACKAGE_METADATA dict.

PACKAGE_NAME_SAFE#

The PACKAGE_NAME value normalized for use as a valid filename. This is used as part of the wheel filename, as well as some metadata files within the wheel.

PACKAGE_VERSION#

The version item from the PACKAGE_METADATA

PACKAGE_NAMEVER#

The PACKAGE_NAME_SAFE and PACKAGE_VERSION variables concatenated with a hyphen.

WHEEL_FILE#

The final path to the wheel filename that will be generated. This is set to a file in the directory WHEEL_DIR with a filename generated from the package name, version, and compatibility tags. It can be overridden by setting the target parameter to env.WhlFile()

EGG_INFO_PATH#

Path where source dist metadata is built during the creation of a source distribution. It’s set to a name generated from the package name with “.egg-info” appended to it. The directory is created either in the current directory or directory set by --egg_base, or the value of the package metadata’s src_root key.

Building Your Wheel#

As enscons implements a PEP 517 compatible build backend, it is recommended to use a similarly compatible frontend, such as Build

Install build with

$ pip install build

Then you can build your wheel with

$ python -m build

This will output a wheel file in the dist/ directory by default.

You can also build any defined target using the scons command. e.g.

$ scons bdist_wheel