Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# _cmac_build.py is a build-time helper referenced by
# [tool.setuptools.cmdclass]; it is not part of any package, so it has to be
# added to the sdist explicitly or building from the sdist fails.
include _cmac_build.py
129 changes: 129 additions & 0 deletions _cmac_build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"""Build-time customization for the ``cmac.calc_kdp_ray_fir`` Cython extension.

Three parts of the extension build cannot be expressed statically in
``pyproject.toml``: NumPy's C header directory is only discoverable once
NumPy is importable, ``[tool.setuptools] ext-modules`` cannot express
``define-macros`` two-tuples, and the interpreter's own ``CFLAGS`` may name
options the available compiler does not accept. All three are applied here
and wired up through ``[tool.setuptools.cmdclass]``, so no ``setup.py`` is
needed.
"""

import os
import re
import subprocess
import tempfile

from setuptools.command.build_ext import build_ext as _build_ext

# Compile against the stable NumPy C API rather than the deprecated one.
NUMPY_API_MACRO = ("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")

# gcc: "unrecognized command-line option '-partition=none'"
# clang: "unknown argument: '-partition=none'"
# GCC quotes with U+2018/U+2019 under a UTF-8 locale, so the quoting around
# the flag is matched loosely rather than assumed to be ASCII.
QUOTES = "\"'`\u2018\u2019\u201c\u201d"
UNSUPPORTED_FLAG = re.compile(
r"(?:unrecognized\s+(?:command[-\s]line\s+)?option"
r"|unknown\s+argument:?)"
r"\s*[" + QUOTES + r"]*\s*"
r"(-[^\s,;()" + QUOTES + r"]+)",
re.IGNORECASE,
)

# Ask the compiler for ASCII diagnostics so the pattern above has the easiest
# possible job; the loose quoting stays as a backstop if this is ignored.
C_LOCALE_ENV = dict(os.environ, LC_ALL="C", LANG="C")

# Every compiler/linker command line distutils may hand us. Which of these
# exist varies with the setuptools version, so they are probed by name.
FLAG_ATTRS = (
"compiler",
"compiler_so",
"compiler_so_cxx",
"compiler_cxx",
"linker_so",
"linker_so_cxx",
"linker_exe",
)


def _rejected_flags(command, source):
"""Return the flags in ``command`` that the compiler refuses outright.

Returns an empty set when the trial compile succeeds, and also when it
fails for any other reason -- this is a best-effort cleanup, so a real
build error is left for the real build to report.
"""
probe = subprocess.run(
list(command) + ["-c", source, "-o", os.devnull],
capture_output=True,
text=True,
env=C_LOCALE_ENV,
)
if probe.returncode == 0:
return set()
return set(UNSUPPORTED_FLAG.findall(probe.stderr))


def drop_unsupported_flags(compiler):
"""Strip options the compiler rejects from ``compiler``'s command lines.

A conda-forge interpreter records the flags of whichever GCC built it in
``sysconfig``'s ``CFLAGS``, and ``customize_compiler`` copies those into
every command line verbatim. When the compiler doing the building is not
that same GCC -- an older system gcc, or a conda toolchain pinned a
release behind -- the build dies on an option it has never heard of, e.g.
``-partition=none`` from GCC 16. Dropping those flags costs nothing: they
are optimization and LTO tuning, not semantics.
"""
if not getattr(compiler, "compiler_so", None):
return set()

dropped = set()
with tempfile.TemporaryDirectory() as tmpdir:
source = os.path.join(tmpdir, "flag_probe.c")
with open(source, "w") as handle:
handle.write("int main(void) { return 0; }\n")

# Each pass can only surface the flags the compiler reaches before
# giving up, so re-probe until it stops complaining. The bound just
# guarantees termination if a flag somehow survives removal.
for _ in range(8):
rejected = _rejected_flags(compiler.compiler_so, source) - dropped
if not rejected:
break
dropped |= rejected
for attr in FLAG_ATTRS:
command = getattr(compiler, attr, None)
if command:
setattr(
compiler,
attr,
[arg for arg in command if arg not in rejected],
)
return dropped


class build_ext(_build_ext):
"""``build_ext`` that resolves NumPy's headers and sanitizes CFLAGS."""

def finalize_options(self):
super().finalize_options()
import numpy

self.include_dirs.append(numpy.get_include())
for ext in self.distribution.ext_modules or []:
ext.define_macros.append(NUMPY_API_MACRO)

def build_extensions(self):
# Runs after distutils has built self.compiler from sysconfig.
dropped = drop_unsupported_flags(self.compiler)
if dropped:
self.announce(
"dropping compiler flags not supported by this compiler: "
+ " ".join(sorted(dropped)),
level=2,
)
super().build_extensions()
57 changes: 49 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,16 +1,47 @@
[build-system]
requires = [
"setuptools>=45",
# >=77 for SPDX `license` / `license-files` support.
"setuptools>=77",
"wheel",
"cython",
"numpy>=2.0; python_version>='3.9'",
# Building against NumPy 2 yields an extension that also runs on NumPy 1.
"numpy>=2.0",
]
build-backend = "setuptools.build_meta"

[project]
name = "cmac"
version = "0.2.0"
description = "Corrected Moments in Antenna Coordinates"
dynamic = ["readme", "version"]
readme = "README.rst"
requires-python = ">=3.10"
license = "BSD-3-Clause"
license-files = ["LICENSE", "LICENSE_GPL.txt"]
authors = [
{ name = "Scott Collis" },
{ name = "Zachary Sherman" },
{ name = "Robert Jackson" },
]
maintainers = [
{ name = "Data Informatics and Geophysical Retrievals (DIGR)" },
]
classifiers = [
"Development Status :: 2 - Pre-Alpha",
"Intended Audience :: Science/Research",
"Intended Audience :: Developers",
"Programming Language :: Python",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Scientific/Engineering",
"Topic :: Scientific/Engineering :: Atmospheric Science",
]

[project.urls]
Homepage = "https://github.com/ARM-Development/cmac"
Source = "https://github.com/ARM-Development/cmac"
Documentation = "https://www.arm.gov/data/data-sources/cmac-69"

[project.optional-dependencies]
# Install with `pip install -e .[test]` to get the testing extras.
Expand All @@ -25,12 +56,22 @@ test = [
"act-atmos"]

[tool.setuptools]
# Explicitly register the extension modules to be compiled
script-files = [
"scripts/cmac",
"scripts/cmac_animation",
"scripts/cmac_dask",
]
# Explicitly register the extension modules to be compiled. The NumPy include
# directory and the NumPy API macro are added by _cmac_build.build_ext below.
ext-modules = [
{ name = "cmac.calc_kdp_ray_fir", sources = ["cmac/calc_kdp_ray_fir.pyx"] }
]

[tool.cibuildwheel]
build = "cp310-* cp311-* cp312-* cp313-*"
skip = "*-musllinux_i686 *-manylinux_i686 pp*"
build-verbosity = 1
[tool.setuptools.packages.find]
# namespaces = false matches the old find_packages() behaviour: cmac/tests has
# no __init__.py and is deliberately left out of the distribution.
include = ["cmac*"]
namespaces = false

[tool.setuptools.cmdclass]
build_ext = "_cmac_build.build_ext"
138 changes: 0 additions & 138 deletions setup.py

This file was deleted.

Loading