From d15dfa02459679b1615d582b5f718a0be53e13e5 Mon Sep 17 00:00:00 2001 From: Peter Sabaini Date: Tue, 28 Apr 2026 12:34:49 +0200 Subject: [PATCH 1/6] feat: reactive on 26.04 Signed-off-by: Peter Sabaini --- .github/workflows/tests.yml | 60 ++++++++++++++++++++++++++++++------- charmtools/build/builder.py | 18 +++++++++-- docs/changelog.rst | 3 ++ setup.py | 2 +- tests/test_build.py | 15 ++++++++++ tests/test_utils.py | 25 +++++++++++----- 6 files changed, 101 insertions(+), 22 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 819e162..5679dc3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,11 +14,12 @@ jobs: - '3.8' # focal - '3.10' # jammy - '3.12' # noble + - '3.14' # resolute steps: - name: Check out code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Tox @@ -31,7 +32,7 @@ jobs: needs: unit runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Add fake tag for git describe run: git tag v0.0.0 - uses: snapcore/action-build@v1 @@ -44,9 +45,13 @@ jobs: integration: strategy: matrix: - charmcraft_channel: - - "2.x/stable" - - "3.x/beta" + include: + - charmcraft_channel: "2.x/stable" + charmcraft_label: "2.x-stable" + - charmcraft_channel: "3.x/stable" + charmcraft_label: "3.x-stable" + - charmcraft_channel: "latest/candidate" + charmcraft_label: "latest-candidate" name: Integration test needs: build runs-on: ubuntu-latest @@ -62,7 +67,7 @@ jobs: sudo iptables -F FORWARD sudo iptables -P FORWARD ACCEPT - name: Checkout layer-basic - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: repository: juju-solutions/layer-basic @@ -109,8 +114,8 @@ jobs: architectures: [amd64] EOF charmcraft pack -p tests/charm-minimal -v - - name: Build reactive charm with charmcraft-3.x - if: ${{ matrix.charmcraft_channel == '3.x/beta' }} + - name: Build reactive charm with charmcraft-3.x on ubuntu@24.04 + if: ${{ matrix.charmcraft_channel == '3.x/stable' }} run: | set -euxo pipefail sudo snap install --classic --channel ${{ matrix.charmcraft_channel }} charmcraft @@ -141,6 +146,38 @@ jobs: EOF charmcraft pack -p tests/charm-minimal -v mv minimal_amd64.charm minimal_ubuntu-24.04-amd64.charm + - name: Build reactive charm with charmcraft latest/candidate on ubuntu@26.04 + if: ${{ matrix.charmcraft_channel == 'latest/candidate' }} + run: | + set -euxo pipefail + sudo snap install --classic --channel ${{ matrix.charmcraft_channel }} charmcraft + cat << EOF | tee tests/charm-minimal/charmcraft.yaml + type: charm + parts: + charm-tools: + plugin: nil + override-build: | + ls -lR \$CRAFT_PROJECT_DIR/ + snap install --dangerous --classic /root/project/charm-snap/charm_0.0.0_amd64.snap + rm -rf \$CRAFT_PROJECT_DIR/parts/charm/src/charm-snap + charm: + after: [charm-tools] + source: . + plugin: reactive + reactive-charm-build-arguments: + - -v + - --binary-wheels-from-source + - --upgrade-buildvenv-core-deps + - --ignore-requires-python + build-packages: + - python3-dev + - libpq-dev + base: ubuntu@26.04 + platforms: + amd64: + EOF + charmcraft pack -p tests/charm-minimal -v + mv minimal_amd64.charm minimal_ubuntu-26.04-amd64.charm ## action to interactively debug CI failures. # - name: Setup upterm session # if: failure() @@ -149,15 +186,16 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: charmcraft execution logs ${{ matrix.runs-on }} + name: charmcraft execution logs ${{ matrix.charmcraft_label }} path: ~/snap/charmcraft/common/cache/charmcraft/log/*.log - name: Upload built charms uses: actions/upload-artifact@v4 with: - name: Built charms + name: Built charms ${{ matrix.charmcraft_label }} overwrite: true path: | minimal_ubuntu-18.04-amd64.charm minimal_ubuntu-20.04-amd64.charm minimal_ubuntu-22.04-amd64.charm minimal_ubuntu-24.04-amd64.charm + minimal_ubuntu-26.04-amd64.charm diff --git a/charmtools/build/builder.py b/charmtools/build/builder.py index fb04c84..e69341b 100755 --- a/charmtools/build/builder.py +++ b/charmtools/build/builder.py @@ -163,6 +163,14 @@ class Builder(object): DEFAULT_SERIES = 'trusty' METRICS_URL = 'https://www.google-analytics.com/collect' METRICS_ID = 'UA-96529618-2' + CHARMCRAFT_BUILD_PACKAGES = ( + 'git', + 'virtualenv', + 'python3-venv', + 'python3-pip', + 'python3-setuptools', + 'python3-wheel', + ) def __init__(self): self.config = BuildConfig() @@ -778,13 +786,19 @@ def workaround_charmcraft_maybe_ensure_build_packages(self): The charmcraft reactive plugin ought to provide the bare minimum of build package dependencies, however until it does let's help here under the right circumstances. + + Keep this list conservative and limited to generic tooling needed by + ``charm build`` itself. Charm-specific native dependencies still belong + in the charm's ``build-packages``. """ if (os.geteuid() == 0 and (os.environ.get('CRAFT_PART_NAME', None) or os.environ.get('CHARMCRAFT_PART_NAME', None))): + packages = self.CHARMCRAFT_BUILD_PACKAGES log.warning('Probably running as root in charmcraft, proactively ' - 'installing the `git` and `virtualenv` packages.') - subprocess.run(('apt', '-y', 'install', 'git', 'virtualenv'), + 'installing the minimum charm build packages: %s.', + ', '.join(packages)) + subprocess.run(('apt', '-y', 'install') + packages, check=True, env=utils.host_env()) def generate(self): diff --git a/docs/changelog.rst b/docs/changelog.rst index e3b9a4d..70b6e50 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,9 @@ Changelog Current release ^^^^^^^^^^^^ +* Add Python 3.14 / Ubuntu 26.04 CI coverage for reactive charm builds +* Relax the `virtualenv` dependency for newer Python compatibility +* Install additional generic build tooling in the charmcraft workaround * Add ability to specify constraints for `WheelhouseTactic` (#693) * Fix `charm-tools` version (#692) * Pin `setuptools` < 82 and drop archived `vergit` dependency (#689) diff --git a/setup.py b/setup.py index d1e2ad6..8c2ff50 100755 --- a/setup.py +++ b/setup.py @@ -72,7 +72,7 @@ 'path<17', 'pip>=1.5.4', 'jujubundlelib<0.6', - 'virtualenv>=1.11.4,<21', + 'virtualenv>=20.26', 'colander<1.9', 'jsonschema<4.18.0', 'keyring<24', diff --git a/tests/test_build.py b/tests/test_build.py index f7f0a0f..9dc97ca 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -57,6 +57,21 @@ def test_environment_hide_metrics(self): builder = build.Builder() self.assertTrue(builder.hide_metrics) + @mock.patch('charmtools.build.builder.subprocess.run') + @mock.patch('charmtools.build.builder.os.geteuid', return_value=0) + def test_workaround_charmcraft_installs_minimum_build_packages( + self, _geteuid, run): + builder = build.Builder() + with mock.patch.dict(os.environ, {'CRAFT_PART_NAME': 'charm'}): + builder.workaround_charmcraft_maybe_ensure_build_packages() + + args, kwargs = run.call_args + self.assertEqual( + args[0], + ('apt', '-y', 'install') + builder.CHARMCRAFT_BUILD_PACKAGES) + self.assertTrue(kwargs['check']) + self.assertIn('env', kwargs) + def test_invalid_layer(self): # Test that invalid metadata.yaml files get a BuildError exception. builder = build.Builder() diff --git a/tests/test_utils.py b/tests/test_utils.py index 7fb29a3..2b894b2 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,9 +1,9 @@ from __future__ import print_function -import unittest from unittest import TestCase from charmtools import utils from six import StringIO +import mock class TestUtils(TestCase): @@ -45,7 +45,7 @@ def react(db): self.assertIn("@when('db.ready'", output) self.assertIn("bar", output) - @unittest.mock.patch("os.environ") + @mock.patch("os.environ") def test_host_env(self, mock_environ): mock_environ.copy.return_value = { 'PREFIX': 'fake-prefix', @@ -61,14 +61,14 @@ def test_host_env(self, mock_environ): {'SOME_OTHER_KEY': 'fake-some-other-key', 'PATH': '/usr/bin:/bin'}, utils.host_env()) - @unittest.mock.patch.object(utils, "Process") + @mock.patch.object(utils, "Process") def test_upgrade_venv_core_packages(self, mock_Process): utils.upgrade_venv_core_packages('/some/dir', env={'some': 'envvar'}) mock_Process.assert_called_once_with( ('/some/dir/bin/pip', 'install', '-U', 'pip', 'setuptools'), env={'some': 'envvar'}) - @unittest.mock.patch.object(utils, "Process") + @mock.patch.object(utils, "Process") def test_pin_setuptools_for_pep440(self, mock_Process): utils.pin_setuptools_for_pep440('/some/dir', env={'some': 'envvar'}) mock_Process.assert_called_once_with( @@ -76,8 +76,8 @@ def test_pin_setuptools_for_pep440(self, mock_Process): 'setuptools<67'), env={'some': 'envvar'}) - @unittest.mock.patch("sys.exit") - @unittest.mock.patch.object(utils, "Process") + @mock.patch("sys.exit") + @mock.patch.object(utils, "Process") def test_get_venv_package_list(self, mock_Process, mock_sys_exit): mock_Process().return_value = utils.ProcessResult('fakecmd', 0, '', '') utils.get_venv_package_list('/some/dir', env={'some': 'envvar'}) @@ -89,11 +89,20 @@ def test_get_venv_package_list(self, mock_Process, mock_sys_exit): utils.get_venv_package_list('/some/dir', env={'some': 'envvar'}) mock_sys_exit.assert_called_once_with(1) - @unittest.mock.patch.object(utils, "Process") - def test_get_oython_version(self, process_klass): + @mock.patch.object(utils, "Process") + def test_get_python_version(self, process_klass): process_klass().return_value = utils.ProcessResult( ['python3', '--version'], 0, b'Python 3.12.4', b'') self.assertEqual( utils.get_python_version('/some/dir', env={'some': 'envvar'}), (3, 12, 4) ) + + @mock.patch.object(utils, "Process") + def test_get_python_version_314(self, process_klass): + process_klass().return_value = utils.ProcessResult( + ['python3', '--version'], 0, b'Python 3.14.0', b'') + self.assertEqual( + utils.get_python_version('/some/dir', env={'some': 'envvar'}), + (3, 14, 0) + ) From 29635849ebf1a73651371ca4c727735cc9ca4e3e Mon Sep 17 00:00:00 2001 From: Peter Sabaini Date: Tue, 28 Apr 2026 12:49:00 +0200 Subject: [PATCH 2/6] test: add workflow dispatch Signed-off-by: Peter Sabaini --- .github/workflows/tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5679dc3..4d222a2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,7 +3,8 @@ name: Test Suite on: pull_request: branches: [ master ] - + workflow_dispatch: + jobs: unit: name: Unit tests From ff514096337a4cb99fc66b150937001c0f93cb25 Mon Sep 17 00:00:00 2001 From: Peter Sabaini Date: Tue, 28 Apr 2026 12:59:47 +0200 Subject: [PATCH 3/6] test: install legacy cgi for cheetah Signed-off-by: Peter Sabaini --- docs/changelog.rst | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 70b6e50..7092943 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Current release ^^^^^^^^^^^^ * Add Python 3.14 / Ubuntu 26.04 CI coverage for reactive charm builds +* Add `legacy-cgi` dependency for Python 3.13+ template support * Relax the `virtualenv` dependency for newer Python compatibility * Install additional generic build tooling in the charmcraft workaround * Add ability to specify constraints for `WheelhouseTactic` (#693) diff --git a/setup.py b/setup.py index 8c2ff50..500f4ca 100755 --- a/setup.py +++ b/setup.py @@ -61,6 +61,7 @@ exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), install_requires=[ 'cheetah3>=3.0.0,<4.0', + 'legacy-cgi;python_version >= "3.13"', 'pyyaml>=5.0,!=5.4.0,!=5.4.1,!=6.0,<7.0', 'requests>=2.0.0,<3.0.0', 'blessings<2.0', From 929ee953c5a52f90febac5642137b8e7dd9de0fa Mon Sep 17 00:00:00 2001 From: Peter Sabaini Date: Tue, 28 Apr 2026 15:05:33 +0200 Subject: [PATCH 4/6] fix: modernize virtualenv Signed-off-by: Peter Sabaini --- docs/changelog.rst | 1 + snap/snapcraft.yaml | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7092943..e68de7b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,7 @@ Current release * Add Python 3.14 / Ubuntu 26.04 CI coverage for reactive charm builds * Add `legacy-cgi` dependency for Python 3.13+ template support +* Fix snap build with modern `virtualenv` by allowing its pure-Python wheel * Relax the `virtualenv` dependency for newer Python compatibility * Install additional generic build tooling in the charmcraft workaround * Add ability to specify constraints for `WheelhouseTactic` (#693) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 235f64e..809f51a 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -78,8 +78,14 @@ parts: # Your build configuration is incomplete and previously worked by accident! # setuptools_scm requires setuptools>=61 pip install --upgrade 'setuptools<82' + # Most dependencies are built from source for the snap, but + # virtualenv 20.26+ uses hatchling. Building that sdist inside the + # staged core22 environment can import the staged python3-pathspec, + # which is too old for hatchling. virtualenv is pure Python, so use its + # wheel while keeping source builds for the rest of the dependency set. pip install \ --no-binary :all: \ + --only-binary virtualenv \ --prefix $CRAFT_PART_INSTALL/usr \ . From 9610519f73fc0b043d90a5c2087c110c889f90cf Mon Sep 17 00:00:00 2001 From: Peter Sabaini Date: Tue, 28 Apr 2026 16:14:14 +0200 Subject: [PATCH 5/6] fix: more virtualenv Signed-off-by: Peter Sabaini --- snap/snapcraft.yaml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 809f51a..d7b3553 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -78,14 +78,15 @@ parts: # Your build configuration is incomplete and previously worked by accident! # setuptools_scm requires setuptools>=61 pip install --upgrade 'setuptools<82' - # Most dependencies are built from source for the snap, but - # virtualenv 20.26+ uses hatchling. Building that sdist inside the - # staged core22 environment can import the staged python3-pathspec, - # which is too old for hatchling. virtualenv is pure Python, so use its - # wheel while keeping source builds for the rest of the dependency set. + # Most dependencies are built from source for the snap, but modern + # virtualenv and its pure-Python dependencies use hatchling. Building + # those sdists inside the staged core22 environment can import the + # staged python3-pathspec, which is too old for hatchling. Use wheels for + # that pure-Python dependency set while keeping source builds for the + # rest of the dependency set. pip install \ --no-binary :all: \ - --only-binary virtualenv \ + --only-binary virtualenv,distlib,filelock,platformdirs,python-discovery \ --prefix $CRAFT_PART_INSTALL/usr \ . From 11a38e6ad797acd21c674e829703e6cf82be8053 Mon Sep 17 00:00:00 2001 From: Peter Sabaini Date: Thu, 30 Apr 2026 16:24:16 +0200 Subject: [PATCH 6/6] feat: use core24 Signed-off-by: Peter Sabaini --- charmtools/build/tactics.py | 68 ++++++++++++++++++++++++++++++---- charmtools/diff_match_patch.py | 2 +- docs/changelog.rst | 3 +- setup.py | 6 ++- snap/snapcraft.yaml | 53 +++++++++++++------------- tests/test_build.py | 40 ++++++++++++++++++++ 6 files changed, 133 insertions(+), 39 deletions(-) diff --git a/charmtools/build/tactics.py b/charmtools/build/tactics.py index 114b47b..dfca669 100644 --- a/charmtools/build/tactics.py +++ b/charmtools/build/tactics.py @@ -4,6 +4,7 @@ import logging import os import re +import sys import tarfile import tempfile import zipfile @@ -1064,6 +1065,13 @@ class WheelhouseTactic(ExactMatch, Tactic): _default_cons = [ "setuptools<82", ] + _python314_requirement_overrides = [ + ( + 'Cython', + "Cython>=3.1,<4;python_version >= '3.14'", + 'Cython 0.29.x does not build on Python 3.14', + ), + ] def __init__(self, *args, **kwargs): super(WheelhouseTactic, self).__init__(*args, **kwargs) @@ -1217,15 +1225,17 @@ def _add(self, wheelhouse, *reqs, constraints=None): _ignore_requires_python = ('--ignore-requires-python', ) env = self._get_env() try: - if self.binary_build_from_source or self.binary_build: - # Handle constraints - if constraints: - env['PIP_CONSTRAINT'] = constraints - env['PIP_BUILD_CONSTRAINT'] = constraints - else: - env.pop('PIP_CONSTRAINT', None) - env.pop('PIP_BUILD_CONSTRAINT', None) + # Apply constraints to both dependency resolution and PEP 517 + # build environments. This is needed for source downloads too, + # because pip may need to build sdists to inspect metadata. + if constraints: + env['PIP_CONSTRAINT'] = str(constraints) + env['PIP_BUILD_CONSTRAINT'] = str(constraints) + else: + env.pop('PIP_CONSTRAINT', None) + env.pop('PIP_BUILD_CONSTRAINT', None) + if self.binary_build_from_source or self.binary_build: self._pip('wheel', *_no_binary_opts if self.binary_build_from_source else tuple(), @@ -1389,8 +1399,50 @@ def _process_per_layer(self, wheelhouse): log.debug('Per-layer wheelhouse is not compatible with constraints') self._add(wheelhouse, '-r', self.entity) + def _apply_python_requirement_overrides(self, python_version=None): + """Apply requirement overrides needed by the target build Python version.""" + python_version = python_version or sys.version_info + if python_version < (3, 14): + return + + override_names = { + safe_name(name) + for name, _line, _reason in self._python314_requirement_overrides + } + updated_lines = [] + overridden = set() + for line in self.lines or []: + try: + req = next(requirements.parse(line)) + req_name = safe_name(req.name) + except (StopIteration, ValueError): + updated_lines.append(line) + continue + + if req_name in override_names: + updated_lines.append( + f'# {line} # overridden by charm-tools for Python 3.14' + ) + overridden.add(req_name) + else: + updated_lines.append(line) + + if overridden: + updated_lines.append('# Default Python 3.14 wheelhouse overrides') + for name, line, reason in self._python314_requirement_overrides: + if safe_name(name) in overridden: + updated_lines.append(f'# {reason}') + updated_lines.append(line) + updated_lines.append('') + self.lines = updated_lines + def _process_combined(self, wheelhouse): self.read() + if self._venv: + python_version = utils.get_python_version(self._venv, env=self._get_env()) + else: + python_version = sys.version_info + self._apply_python_requirement_overrides(python_version) log.debug('Processing wheelhouse:') for line in self.lines or []: log.debug(' %s', line.strip()) diff --git a/charmtools/diff_match_patch.py b/charmtools/diff_match_patch.py index 020408a..19b2f17 100644 --- a/charmtools/diff_match_patch.py +++ b/charmtools/diff_match_patch.py @@ -1815,7 +1815,7 @@ def patch_fromText(self, textline): return patches text = textline.split('\n') while len(text) != 0: - m = re.match("^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$", text[0]) + m = re.match(r"^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$", text[0]) if not m: raise ValueError("Invalid patch string: " + text[0]) patch = patch_obj() diff --git a/docs/changelog.rst b/docs/changelog.rst index e68de7b..ec689c7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,7 +6,8 @@ Current release * Add Python 3.14 / Ubuntu 26.04 CI coverage for reactive charm builds * Add `legacy-cgi` dependency for Python 3.13+ template support -* Fix snap build with modern `virtualenv` by allowing its pure-Python wheel +* Move the snap build to `core24` so modern source builds can use the packaged `pathspec` +* Fix snap build with modern `virtualenv` while keeping source builds on all architectures * Relax the `virtualenv` dependency for newer Python compatibility * Install additional generic build tooling in the charmcraft workaround * Add ability to specify constraints for `WheelhouseTactic` (#693) diff --git a/setup.py b/setup.py index 500f4ca..9eb3b92 100755 --- a/setup.py +++ b/setup.py @@ -64,16 +64,18 @@ 'legacy-cgi;python_version >= "3.13"', 'pyyaml>=5.0,!=5.4.0,!=5.4.1,!=6.0,<7.0', 'requests>=2.0.0,<3.0.0', + 'six>=1.10', 'blessings<2.0', 'ruamel.yaml<0.16.0;python_version < "3.7"', 'pathspec<=0.3.4;python_version < "3.7"', 'ruamel.yaml<0.18;python_version >= "3.7"', - 'pathspec<0.11;python_version >= "3.7"', + 'pathspec>=0.10.1,<0.13;python_version >= "3.7"', 'otherstuf<=1.1.0', 'path<17', 'pip>=1.5.4', 'jujubundlelib<0.6', - 'virtualenv>=20.26', + 'virtualenv>=20.25;python_version < "3.14"', + 'virtualenv>=20.26;python_version >= "3.14"', 'colander<1.9', 'jsonschema<4.18.0', 'keyring<24', diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index d7b3553..19d0d3a 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -6,7 +6,7 @@ description: | license: GPL-3.0 confinement: classic grade: stable -base: core22 +base: core24 apps: charm: command: bin/wrappers/charm @@ -23,35 +23,35 @@ environment: LD_LIBRARY_PATH: "" PATH: $SNAP/usr/local/bin:$PATH PYTHONHOME: $SNAP - PYTHONPATH: $SNAP/usr/lib/python3/dist-packages:$SNAP/usr/lib/python3.10/site-packages:$SNAP/usr/lib/python3.10:$SNAP/usr/lib/python3.10/lib-dynload:$SNAP/usr/local/lib/python3.10/dist-packages + PYTHONPATH: $SNAP/usr/lib/python3/dist-packages:$SNAP/usr/lib/python3.12/site-packages:$SNAP/usr/lib/python3.12:$SNAP/usr/lib/python3.12/lib-dynload:$SNAP/usr/local/lib/python3.12/dist-packages parts: charm-tools: source: . - # the core22 python plugin does not work for classic snaps. + # the core24 python plugin does not work for classic snaps. # a side effect of this is that we do patchelf manually as a separate part. plugin: nil build-packages: - libffi-dev - - libpython3.10-dev + - libpython3.12-dev - python3-pip - rustc - cargo - pkg-config # Note that we cannot use any -dev type packages here, as that would pull # in the libc6 package. The libc6 package contains absolute symlinks - # pointing at /, which is not allowed in a core22 classic snap. + # pointing at /, which is not allowed in a core24 classic snap. stage-packages: - libbrotli1 - git-core - - libpython3.10 - - libpython3.10-minimal - - libpython3.10-stdlib + - libpython3.12t64 + - libpython3.12-minimal + - libpython3.12-stdlib - python3-minimal - - python3.10-minimal + - python3.12-minimal - python3-cryptography - python3-pip - python3-setuptools - - python3-distutils + - python3-six - python3-importlib-resources - python3-pkg-resources - python3-virtualenv @@ -63,30 +63,28 @@ parts: - python3-pathspec - python3-path - python3-jsonschema - - python3-keyring - python3-secretstorage - python3-translationstring - python3-iso8601 - python3-parse build-environment: - MAKEFLAGS: -j$(nproc) - - CFLAGS: -I/usr/include/python3.10 + - CFLAGS: -I/usr/include/python3.12 override-build: | # Upgrading setuptools helps to fix: # # ERROR: setuptools==59.6.0 is used in combination with setuptools_scm>=8.x # Your build configuration is incomplete and previously worked by accident! # setuptools_scm requires setuptools>=61 - pip install --upgrade 'setuptools<82' - # Most dependencies are built from source for the snap, but modern - # virtualenv and its pure-Python dependencies use hatchling. Building - # those sdists inside the staged core22 environment can import the - # staged python3-pathspec, which is too old for hatchling. Use wheels for - # that pure-Python dependency set while keeping source builds for the - # rest of the dependency set. + pip install --break-system-packages --upgrade --no-binary :all: 'setuptools<82' + + # Keep source builds for all dependencies so the snap can be built on + # every supported architecture, including architectures that may not have + # wheels. core24 provides a new enough python3-pathspec for modern build + # backends such as hatchling, so no binary-only workaround is needed. pip install \ + --break-system-packages \ --no-binary :all: \ - --only-binary virtualenv,distlib,filelock,platformdirs,python-discovery \ --prefix $CRAFT_PART_INSTALL/usr \ . @@ -94,10 +92,11 @@ parts: # pass `--no-deps`, otherwise it will overwrite the charm-tools we just # installed above! pip install \ + --break-system-packages \ --no-deps \ --prefix $CRAFT_PART_INSTALL/usr \ https://github.com/openstack-charmers/charm-templates-openstack/archive/master.zip#egg=charm_templates_openstack - sed -i "$CRAFT_PART_INSTALL/usr/lib/python3.10/site.py" -e 's/^ENABLE_USER_SITE = None$/ENABLE_USER_SITE = False/' + sed -i "$CRAFT_PART_INSTALL/usr/lib/python3.12/site.py" -e 's/^ENABLE_USER_SITE = None$/ENABLE_USER_SITE = False/' mkdir -p $CRAFT_PART_INSTALL/bin/wrappers cp helpers/snap-wrappers/charm $CRAFT_PART_INSTALL/bin/wrappers/ @@ -146,11 +145,11 @@ parts: # libraries from the core or shipped snap. We accomplish this by # patching RPATH or interpreter into dynamically linked binaries. # - # /snap/core22/current/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 - # /snap/core22/current/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1 - # /snap/core22/current/lib/powerpc64le-linux-gnu/ld64.so.2 - # /snap/core22/current/lib/s390x-linux-gnu/ld64.so.1 - interp_prefix=/snap/core22/current/lib/$CRAFT_ARCH_TRIPLET + # /snap/core24/current/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 + # /snap/core24/current/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1 + # /snap/core24/current/lib/powerpc64le-linux-gnu/ld64.so.2 + # /snap/core24/current/lib/s390x-linux-gnu/ld64.so.1 + interp_prefix=/snap/core24/current/lib/$CRAFT_ARCH_TRIPLET ld_arch=$(echo $CRAFT_TARGET_ARCH | sed -e 's,amd64,x86-64,' -e 's,arm64,aarch64,') for interp in "ld-linux-${ld_arch}.so.?" "ld64.so.?" "/dev/null"; do if [ -e ${interp_prefix}/${interp} ]; then @@ -166,7 +165,7 @@ parts: $(find . -exec file {} \; | awk -F\: '/ELF.*dynamic/{print$1}'); do patchelf \ --force-rpath \ - --set-rpath /snap/core22/current/usr/lib/$CRAFT_ARCH_TRIPLET:/snap/$CRAFT_PROJECT_NAME/current/lib:/snap/$CRAFT_PROJECT_NAME/current/lib/$SNAPCRAFT_ARCH_TRIPLET:/snap/$CRAFT_PROJECT_NAME/current/usr/lib:/snap/$CRAFT_PROJECT_NAME/current/usr/lib/$SNAPCRAFT_ARCH_TRIPLET \ + --set-rpath /snap/core24/current/usr/lib/$CRAFT_ARCH_TRIPLET:/snap/$CRAFT_PROJECT_NAME/current/lib:/snap/$CRAFT_PROJECT_NAME/current/lib/$CRAFT_ARCH_TRIPLET:/snap/$CRAFT_PROJECT_NAME/current/usr/lib:/snap/$CRAFT_PROJECT_NAME/current/usr/lib/$CRAFT_ARCH_TRIPLET \ $binary patchelf --set-interpreter $interp_prefix/$interp $binary || true diff --git a/tests/test_build.py b/tests/test_build.py index 9dc97ca..9601042 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -636,6 +636,46 @@ def _side(*a, **kw): self.assertEqual(captured['env']['PIP_BUILD_CONSTRAINT'], str(self.build_dir / wh.CONS_FILENAME)) + def test_wheelhouse_python314_cython_override(self): + wh = build.tactics.WheelhouseTactic( + path('wheelhouse.txt'), + mock.Mock(directory=self.build_dir), + mock.Mock(url='charm'), + mock.Mock()) + wh.lines = [ + '# layer:basic', + 'Cython<3.0.0', + 'PyYAML<7.0.0;python_version >= "3.7"', + '', + ] + wh.cons_lines = [] + td = path(tempfile.mkdtemp()) + + @contextmanager + def fake_tempdir(chdir=False): + try: + yield td + finally: + pass + + with mock.patch.object(build.tactics.sys, 'version_info', (3, 10, 0)): + with mock.patch.object(build.tactics.utils, 'get_python_version', return_value=(3, 14, 0)): + with mock.patch.object(build.tactics.utils, 'tempdir', fake_tempdir): + with mock.patch.object(build.tactics.WheelhouseTactic, '_add'): + wh._venv = path('/tmp/fake-venv') + wh._process_combined(self.build_dir) + + wheelhouse_txt = (self.build_dir / 'wheelhouse.txt').text() + self.assertIn( + '# Cython<3.0.0 # overridden by charm-tools for Python 3.14', + wheelhouse_txt) + self.assertIn( + "Cython>=3.1,<4;python_version >= '3.14'", + wheelhouse_txt) + self.assertIn( + 'PyYAML<7.0.0;python_version >= "3.7"', + wheelhouse_txt) + @mock.patch.object(build.tactics, 'log') @mock.patch.object(build.tactics.YAMLTactic, 'read', lambda s: setattr(s, '_read', True))