Migrate your 12-factor charm to use the uv plugin

Migrate your 12-factor charm to use the uv plugin

In the upcoming V2 release of the paas-charm library and charms using the 26.04 base, the uv plugin will be the default. V1 charms use the charm plugin by default, and converting your V1 charm is considered a breaking change. This guide walks you through manually converting a V1 12-factor charm that uses the legacy charm plugin to the modern uv plugin backed by uv.

Why migrate?

The uv plugin provides fast, reproducible Python dependency resolution via a lock file (uv.lock), replaces the loose requirements.txt approach with a proper pyproject.toml, and removes the need for the charm-strict-dependencies workaround.

Prerequisites

  • uv installed locally.
  • charmcraft installed.
  • The astral-uv snap available for the build environment.

Replace requirements.txt with pyproject.toml

Delete requirements.txt from your charm directory and create a pyproject.toml in its place, declaring your charm’s Python dependencies with pinned versions.

For example, if your requirements.txt looks like:

cosl==1.9.1
dpcharmlibs-interfaces==1.0.2
jsonschema >=4.19,<4.20
ops >= 3.8.0
pydantic==2.13.3
paas-charm==1.11.3

Then replace the file with its equivalent pyproject.toml:

[project]
name = "my-charm-k8s"          # use your charm's name
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
    "cosl==1.9.1",
    "dpcharmlibs-interfaces==1.0.2",
    "jsonschema==4.19.2",
    "ops==3.8.0",
    "pydantic==2.13.3",
    "paas-charm==1.11.3",
]

Tip: Pin every dependency to an exact version so that the generated uv.lock is fully reproducible across machines and CI environments. The versions above are illustrative — keep the ones your charm already depends on.

Generate uv.lock

With pyproject.toml in place, run uv sync inside the charm directory to resolve dependencies and generate the lock file:

cd path/to/your/charm
uv sync

This creates (or refreshes) uv.lock. The lock file must be present before you run charmcraft pack. Commit uv.lock to your repository.

Update charmcraft.yaml

Change the plugin key

Replace the plugin: charm block with plugin: uv.

The key differences are:

  • Add source: .
  • Change plugin: charmplugin: uv
  • Remove charm-strict-dependencies: false
  • Add astral-uv to build-snaps

For example, if your charmcraft.yaml contains the following snippet:

parts:
  charm:
    charm-strict-dependencies: false
    plugin: charm
    build-snaps:
      - rustup
    override-build: |-
      rustup default stable
      craftctl default

Then update your file to use:

parts:
  charm:
    source: .
    plugin: uv
    build-snaps:
      - astral-uv
      - rustup
    override-build: |-
      rustup default stable
      craftctl default

(If applicable) Add a config part for paas-config.yaml

If your charm ships a paas-config.yaml file, you need a dedicated dump part to stage it into the packed charm. Without this part, the file will not be included in your charm because the uv plugin only stages Python artifacts.

parts:
  charm:
    source: .
    plugin: uv
    build-snaps:
      - astral-uv
      - rustup
    override-build: |-
      rustup default stable
      craftctl default
  config:
    plugin: dump
    source: .
    stage:
      - paas-config.yaml

Pack the charm

Always run uv sync before packing to ensure the lock file is up-to-date, then pack as usual:

cd path/to/your/charm
uv sync
charmcraft pack

Summary of file changes

File Action
requirements.txt Delete
pyproject.toml Create – declare pinned dependencies
uv.lock Generate via uv sync and commit
charmcraft.yaml Update parts.charm

Troubleshooting

  • charmcraft pack fails because uv.lock is missing: run uv sync in the charm directory before packing, and make sure uv.lock is committed.
  • paas-config.yaml is missing from the packed charm: confirm you added the config dump part shown above. The uv plugin only stages Python artifacts, so non-Python files need their own part.
2 Likes

Also, if you have an icon, it won’t get picked up automatically any more so you need to include it in the dump plugin section as well.

1 Like

When we migrated our charm metadata (not a 12-factor one) to use PEP 621 [project] in pyproject.toml, we found that some tools got confused by its presence. That table signals that a Python source tree is meant to be packaged. Charms are not, because they are, in essence, applications (not libraries).

As such, folks might want to use PEP 735 dependency groups instead:

# pyproject.toml
[dependency-groups]
main = [
    "cosl==1.9.1",
    "dpcharmlibs-interfaces==1.0.2",
    "jsonschema==4.19.2",
    "ops==3.8.0",
    "pydantic==2.13.3",
    "paas-charm==1.11.3",
]

# There's no standard way of specifying
# a minimum Python version outside of [project],
# so a tool-specific mechanism may be used
[tool.uv.dependency-groups]
main = {requires-python = ">=3.10"}
1 Like