Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
63 changes: 51 additions & 12 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ name: Test Suite
on:
pull_request:
branches: [ master ]

workflow_dispatch:

jobs:
unit:
name: Unit tests
Expand All @@ -14,11 +15,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
Expand All @@ -31,7 +33,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
Expand All @@ -44,9 +46,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
Expand All @@ -62,7 +68,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

Expand Down Expand Up @@ -109,8 +115,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
Expand Down Expand Up @@ -141,6 +147,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()
Expand All @@ -149,15 +187,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
18 changes: 16 additions & 2 deletions charmtools/build/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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):
Expand Down
5 changes: 5 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ Changelog
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)
* Fix `charm-tools` version (#692)
* Pin `setuptools` < 82 and drop archived `vergit` dependency (#689)
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -72,7 +73,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',
Expand Down
7 changes: 7 additions & 0 deletions snap/snapcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +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 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,distlib,filelock,platformdirs,python-discovery \
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will break (most likely) some of the not so common architectures we support, e.g. ppc64el and s390x

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I've now switched to core24 so we get a newer python3-pathspec, this otherwise breaks hatchling

Also had to introduce Cython min. version as this would otherwise break

--prefix $CRAFT_PART_INSTALL/usr \
.

Expand Down
15 changes: 15 additions & 0 deletions tests/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
25 changes: 17 additions & 8 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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',
Expand All @@ -61,23 +61,23 @@ 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(
('/some/dir/bin/pip', 'install', '-U', 'pip<23.1',
'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'})
Expand All @@ -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)
)
Loading