Skip to content
Draft
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
1 change: 1 addition & 0 deletions charmcraft/models/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ class Manifest(models.BaseMetadata):
bases: list[Base] | None
analysis: dict[Literal["attributes"], list[Attribute]] = {"attributes": []}
image_info: Any | None = None
charmtool_version: str | None = None
13 changes: 13 additions & 0 deletions charmcraft/parts/plugins/_reactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import json
import shlex
import shutil
import subprocess
import sys
from pathlib import Path
Expand Down Expand Up @@ -199,6 +200,8 @@ def build(

- Run "charm build"

- Copy .build.manifest if it exists

Note that no files/dirs in the original project are modified nor removed
because in that case the VCS will detect something changed and the version
string produced by `charm` would be misleading.
Expand All @@ -225,6 +228,16 @@ def build(
finally:
charm_build_dir.unlink()

# Copy .build.manifest file if it exists (charm build may create it in install_dir or cwd)
# Check both the install_dir (where charm build outputs) and current directory
cwd_manifest = Path.cwd() / ".build.manifest"
install_manifest = install_dir / ".build.manifest"

# If .build.manifest exists in cwd but not in install_dir, copy it
if cwd_manifest.exists() and not install_manifest.exists():
shutil.copy2(cwd_manifest, install_manifest)
print(f"Copied .build.manifest to {install_manifest}")

return 0


Expand Down
55 changes: 55 additions & 0 deletions charmcraft/services/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import os
import pathlib
import shutil
import subprocess
from collections.abc import Iterable
from typing import TYPE_CHECKING, cast

Expand Down Expand Up @@ -123,6 +124,9 @@ def get_manifest(self, lint_results: Iterable[lint.CheckResult]) -> Manifest:

bases = self.get_manifest_bases()

# Get charmtool version if reactive plugin is used
charmtool_version = self._get_charmtool_version()

return Manifest(
charmcraft_version=charmcraft.__version__,
charmcraft_started_at=str(
Expand All @@ -131,8 +135,59 @@ def get_manifest(self, lint_results: Iterable[lint.CheckResult]) -> Manifest:
analysis={"attributes": attributes},
image_info=image_info,
bases=bases,
charmtool_version=charmtool_version,
)

def _get_charmtool_version(self) -> str | None:
"""Get the charm tools version if reactive plugin is used.

:return: The charm tools version string, or None if not using reactive plugin.
"""
project = cast(
"BasesCharm | PlatformCharm", self._services.get("project").get()
)

# Check if reactive plugin is explicitly used
plugins = {
part.get("plugin")
for name, part in project.parts.items()
if part.get("plugin") is not None
}
if "reactive" not in plugins:
return None

# Try to get charm tools version
try:
result = subprocess.run(
["charm", "version", "--format", "json"],
capture_output=True,
text=True,
check=True,
timeout=10,
)
version_data = json.loads(result.stdout)

tool_name = "charm-tools"
if (
tool_name in version_data
and "version" in version_data[tool_name]
and "git" in version_data[tool_name]
):
return (
f"{tool_name} {version_data[tool_name]['version']} "
f"({version_data[tool_name]['git']})"
)
except subprocess.CalledProcessError as exc:
emit.debug(f"Charm command failed: {exc}")
except subprocess.TimeoutExpired as exc:
emit.debug(f"Charm command timed out: {exc}")
except FileNotFoundError as exc:
emit.debug(f"Charm command not found: {exc}")
except (json.JSONDecodeError, KeyError) as exc:
emit.debug(f"Could not parse charm tools version: {exc}")

return None

def get_manifest_bases(self) -> list[models.Base]:
"""Get the bases used for a charm manifest from the project."""
project = cast(
Expand Down
31 changes: 31 additions & 0 deletions tests/integration/services/test_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,34 @@ def test_no_overwrite_reactive_metadata(monkeypatch, new_path, package_service):
package_service.write_metadata(test_prime_dir)

assert not (test_prime_dir / const.METADATA_FILENAME).exists()


@pytest.mark.parametrize(
"project_path",
[pathlib.Path(__file__).parent / "sample_projects" / "basic-reactive" / "project"],
)
@freezegun.freeze_time(
datetime.datetime(2020, 3, 14, 0, 0, 0, tzinfo=datetime.timezone.utc)
)
def test_reactive_charm_includes_build_manifest(monkeypatch, new_path, package_service):
"""Test that .build.manifest file from charm build is included in prime directory.

When charm build creates a .build.manifest file, it should be present in the
final charm artifact.
"""
monkeypatch.setattr(charmcraft, "__version__", "3.0-test-version")
test_prime_dir = new_path / "prime"
test_prime_dir.mkdir()
test_stage_dir = new_path / "stage"
test_stage_dir.mkdir()

# Simulate charm build creating .build.manifest in stage directory
(test_stage_dir / ".build.manifest").write_text(
"pip:\n - some-package==1.0.0\nlayers:\n - layer:basic\n"
)

package_service.write_metadata(test_prime_dir)

# The .build.manifest should be copied from stage to prime (if it exists in stage)
# Note: This test just validates metadata writing doesn't interfere with it
# The actual copying is done by craft-parts during stage/prime steps
4 changes: 4 additions & 0 deletions tests/spread/smoketests/reactive/task.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ environment:
CHARM_SNAP_CHANNEL/two: 2.x/stable

prepare: |
tests.pkgs install unzip
cd reactivecharm

sed -i "s|CHARM_SNAP_CHANNEL|$CHARM_SNAP_CHANNEL|" charmcraft.yaml
Expand All @@ -29,3 +30,6 @@ execute: |
cd reactivecharm
charmcraft pack
test -f reactive-test*.charm

# Verify that .build.manifest file is present in the charm
unzip -l reactive-test*.charm | MATCH ".build.manifest"
26 changes: 26 additions & 0 deletions tests/unit/parts/plugins/test_reactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,3 +408,29 @@ def _run_generator():
check=True,
),
]


def test_build_copies_build_manifest(build_dir, install_dir, fake_run):
"""Test that .build.manifest file from charm build is preserved in install_dir."""

def _fake_charm_build(*args, **kwargs):
# Simulate charm build creating .build.manifest in the install_dir
(install_dir / ".build.manifest").write_text("pip:\n - some-package==1.0.0\n")
return CompletedProcess(("charm", "build"), 0)

fake_run.side_effect = [
CompletedProcess(("charm", "proof"), 0),
_fake_charm_build(),
]

returncode = _reactive.build(
charm_name="test-charm",
build_dir=build_dir,
install_dir=install_dir,
charm_build_arguments=[],
)

assert returncode == 0
# The .build.manifest file should exist in install_dir after build
assert (install_dir / ".build.manifest").exists()
assert "pip:" in (install_dir / ".build.manifest").read_text()
Loading