Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
3 changes: 2 additions & 1 deletion .github/workflows/cache_comparison.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 10
- name: Set up Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, 3.14
- name: Set up Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, 3.14, 3.14t
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: |-
Expand All @@ -23,6 +23,7 @@ jobs:
3.12
3.13
3.14
3.14t
- env:
BASE_REF: ${{ github.event.inputs.base_ref }}
BUILD_COMMIT: ${{ github.event.inputs.build_commit }}
Expand Down
528 changes: 505 additions & 23 deletions .github/workflows/release.yaml

Large diffs are not rendered by default.

393 changes: 370 additions & 23 deletions .github/workflows/test.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build-support/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ function determine_python() {
which "${PY}" && return 0
fi

version='3.14'
version='3.14t'
interpreter_path="$(command -v "python${version}")"
if [[ -z "${interpreter_path}" ]]; then
echo "pants: Failed to find a Python ${version} interpreter" 1>&2 && return 1
Expand Down
5 changes: 5 additions & 0 deletions docs/notes/2.34.x.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ Thank you to [Klaviyo](https://www.klaviyo.com/) for their Platinum tier support

### Highlights

- Pants now runs on free-threaded CPython 3.14t by default. The Pants engine and Pants' own Python code are thread-safe without the GIL.
Each release also ships a regular (GIL) CPython 3.14 distribution as an escape hatch: set `[GLOBAL].pants_free_threaded = false` in `pants.toml` (or the `PANTS_FREE_THREADED` env var) to select it.
Release artifacts are named with the full ABI tag (e.g. `cp314t`). Launching via `scie-pants` requires a launcher release with free-threaded interpreter support; older launchers will fail to locate the release asset and must be updated.


### Deprecations

### General
Expand Down
2 changes: 1 addition & 1 deletion pants
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

# This bootstrap script runs pants from the live sources in this repo.
#
# The script defaults to running with Python 3.11. To use another Python version,
# The script defaults to running with Python 3.14t. To use another Python version,
# prefix the script with `PY=python3.14`.

set -eo pipefail
Expand Down
12 changes: 12 additions & 0 deletions src/python/pants/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,18 @@ pex_binary(
scie_bind_resource_path=["PANTS_NATIVE_CLIENT=pants/bin/native_client"],
scie_exe="{scie.env.PANTS_NATIVE_CLIENT}",
scie_env=["NO_SCIE_WARNING=true", "_PANTS_SERVER_EXE={scie.env.VIRTUAL_ENV}/bin/pants"],
# The scie interpreter must match the wheels: each release lane builds one
# parametrization with the corresponding CPython (PY=python3.14t or python3.14).
**parametrize(
"cp314t",
scie_pbs_free_threaded=True,
output_path="${spec_path_normalized}/pants-pex-cp314t${file_suffix}",
),
**parametrize(
"cp314",
scie_pbs_free_threaded=False,
output_path="${spec_path_normalized}/pants-pex-cp314${file_suffix}",
),
)

# NB: we use `dummy.c` to avoid clang/gcc complaining `error: no input files` when building
Expand Down
19 changes: 12 additions & 7 deletions src/python/pants/base/exception_sink_integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,18 @@ def test_dumps_traceback_on_sigabrt(self):

# Check that the logs show an abort signal and the beginning of a traceback.
pid_specific_log_file, shared_log_file = get_log_file_paths(ctx.workdir, pid)
regex_str = """\
Fatal Python error: Aborted

Thread [^\n]+ \\(most recent call first\\):
"""

assert re.search(regex_str, read_file(pid_specific_log_file))
# On free-threaded CPython, faulthandler can't enumerate all threads, so it emits a
# different header than the GIL-enabled per-thread traceback format.
regex_str = (
"Fatal Python error: Aborted\n\n"
"(?:Thread [^\n]+ \\(most recent call first\\):"
"|<Cannot show all threads while the GIL is disabled>\n"
"Stack \\(most recent call first\\):)\n"
)

pid_specific_log_contents = read_file(pid_specific_log_file)
assert re.search(regex_str, pid_specific_log_contents)
assert ' File "' in pid_specific_log_contents

# faulthandler.enable() only allows use of a single logging file at once for fatal tracebacks.
assert "" == read_file(shared_log_file)
18 changes: 9 additions & 9 deletions src/python/pants/core/subsystems/python_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

logger = logging.getLogger(__name__)

_PBS_URL_TEMPLATE = "https://github.com/astral-sh/python-build-standalone/releases/download/20251014/cpython-3.14.0+20251014-{}-install_only_stripped.tar.gz"
_PBS_URL_TEMPLATE = "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.14.6+20260610-{}-freethreaded-install_only_stripped.tar.gz"


class PythonBootstrapSubsystem(Subsystem):
Expand All @@ -49,23 +49,23 @@ class PythonBootstrapSubsystem(Subsystem):
default={
"linux_arm64": (
_PBS_URL_TEMPLATE.format("aarch64-unknown-linux-gnu"),
"7dbb43b742c040835a277318355fb359b41e509dbf4fbb614da38005a9290e16",
29297561,
"3d82c130c647988c0cb83fbf6b69637024dfa184da0b4d25c1498a3e9655b2f1",
30304472,
),
"linux_x86_64": (
_PBS_URL_TEMPLATE.format("x86_64-unknown-linux-gnu"),
"493c477b4a88bb1ea2f6c6f57fa0e88ffbe55d9e7b1405c4699f2d41c04eb154",
34580370,
"d4872b127c641aadb0846a0a4f2c3f6377393a34e89de7668274ec2434d50c00",
36178201,
),
"macos_arm64": (
_PBS_URL_TEMPLATE.format("aarch64-apple-darwin"),
"057476264b07222a2baeff68a733647f91a9d61c94f79beba46a44eb42101749",
16923076,
"f47f0d5b02fc634385bdc630dd293678cfbdbc009de2b04403ce04c92473b483",
26071097,
),
"macos_x86_64": (
_PBS_URL_TEMPLATE.format("x86_64-apple-darwin"),
"56dcb0cdafabac9d6d976690fb05d9ee92d20ce798c3aabe9049259ebe7d3e0d",
16905036,
"f8daa286f7315afc2fc1d51e3f6169ef133bd030f344d182f5f24849439aa4be",
26138236,
),
},
help=softwrap(
Expand Down
61 changes: 61 additions & 0 deletions src/python/pants/option/bootstrap_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
)
from pants.option.option_value_container import OptionValueContainer
from pants.option.options import Options
from pants.option.ranked_value import Rank
from pants.util.docutil import bin_name, doc_url
from pants.util.logging import LogLevel
from pants.util.osutil import CPU_COUNT
Expand Down Expand Up @@ -903,6 +904,17 @@ class BootstrapOptions:
"""
),
)
pants_free_threaded = BoolOption(
advanced=True,
default=True,
help=softwrap(
f"""
Run Pants on a free-threaded CPython interpreter. Like `pants_version`, this is
consumed by the `{bin_name()}` launcher to select which Pants distribution to
install and has no effect once Pants is running.
"""
),
)
pants_bin_name = StrOption(
advanced=True,
default="pants",
Expand Down Expand Up @@ -1924,6 +1936,55 @@ def validate_remote_headers(opt_name: str) -> None:
)
)

advisory = cls._free_threaded_advisory(
requested=opts.pants_free_threaded,
explicitly_set=opts.get_rank("pants_free_threaded") > Rank.HARDCODED,
abiflags=sys.abiflags,
scie_pants_version=os.environ.get("SCIE_PANTS_VERSION"),
)
if advisory:
logger.warning(advisory)

@staticmethod
def _free_threaded_advisory(
*,
requested: bool,
explicitly_set: bool,
abiflags: str,
scie_pants_version: str | None,
) -> str | None:
"""Warn when `pants_free_threaded` was set but the launcher did not honor it.

The option is consumed by the `scie-pants` launcher, so a launcher that predates it (or a
standalone scie) silently ignores it. We detect that mismatch from the ABI Pants is
actually running on.
"""
if not explicitly_set:
return None
running_free_threaded = "t" in abiflags
if requested == running_free_threaded:
return None
want = "free-threaded" if requested else "GIL-enabled"
got = "free-threaded" if running_free_threaded else "GIL-enabled"
if scie_pants_version is None:
return softwrap(
f"""
`[GLOBAL] pants_free_threaded` is set to {str(requested).lower()}, but Pants is
running on a {got} CPython and the setting was not applied. This option is
consumed by the `scie-pants` launcher and has no effect on a standalone scie or a
`PANTS_SOURCE` run; download the {want} distribution instead.
"""
)
return softwrap(
f"""
`[GLOBAL] pants_free_threaded` is set to {str(requested).lower()}, but Pants is
running on a {got} CPython. Your `scie-pants` launcher ({scie_pants_version}) did not
apply the setting: it either predates `pants_free_threaded` or found no {want}
distribution for Pants {VERSION}. Update `scie-pants` to select the {want}
distribution.
"""
)

@staticmethod
def maybe_enable_stack_trampoline(opts: OptionValueContainer) -> None:
if sys.platform == "linux" and opts.enable_stack_trampoline is not None:
Expand Down
40 changes: 39 additions & 1 deletion src/python/pants/option/global_options_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@

from pants.base.build_environment import get_buildroot
from pants.engine.internals.native_engine import PyRemotingOptions
from pants.option.bootstrap_options import DynamicRemoteOptions, ExecutionOptions, RemoteProvider
from pants.option.bootstrap_options import (
BootstrapOptions,
DynamicRemoteOptions,
ExecutionOptions,
RemoteProvider,
)
from pants.option.errors import OptionsError
from pants.option.global_options import GlobalOptions
from pants.option.options_bootstrapper import OptionsBootstrapper
Expand Down Expand Up @@ -271,3 +276,36 @@ def test_remote_provider_matches_rust_enum(
client_key_path=None,
append_only_caches_base_path=None,
)


@pytest.mark.parametrize(
"requested, explicitly_set, abiflags, scie_pants_version, expected",
[
(True, False, "", "0.14.0", None),
(True, False, "t", None, None),
(True, True, "t", "0.14.0", None),
(False, True, "", "0.14.0", None),
(True, True, "", "0.14.0", "did not"),
(False, True, "t", "0.14.0", "did not"),
(True, True, "", None, "standalone scie"),
(False, True, "t", None, "standalone scie"),
],
)
def test_free_threaded_advisory(
requested: bool,
explicitly_set: bool,
abiflags: str,
scie_pants_version: str | None,
expected: str | None,
) -> None:
advisory = BootstrapOptions._free_threaded_advisory(
requested=requested,
explicitly_set=explicitly_set,
abiflags=abiflags,
scie_pants_version=scie_pants_version,
)
if expected is None:
assert advisory is None
else:
assert advisory is not None
assert expected in advisory
12 changes: 11 additions & 1 deletion src/python/pants_release/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,14 @@

python_sources()

python_tests(name="test")
python_tests(
name="test",
overrides={
"generate_github_workflows_test.py": {
"dependencies": [
"//pants.toml:files",
"src/python/pants/backend/python/util_rules/pex_cli.py",
],
},
},
)
Loading
Loading