Generate unique DCP package ids

https://onedigi.atlassian.net/browse/DEL-10035
https://onedigi.atlassian.net/browse/DEL-10081

Signed-off-by: Isaac Hermida <isaac.hermida@digi.com>
This commit is contained in:
Isaac Hermida 2026-04-20 11:24:19 +02:00
parent d743784281
commit 69286f4057
3 changed files with 84 additions and 21 deletions

View File

@ -68,8 +68,11 @@ The script generates exactly one runtime artifact per execution.
Notes:
- `--output-dir` is optional. If omitted, the script writes the DCP to the current directory.
- The output file name is always derived from manifest fields as:
- `<package_id>_artifact_<runtime>_<device_types[0]>.tar.gz`
- The generator appends a unique suffix to the input `package_id` using the
`created_at` timestamp encoded in base36 milliseconds.
- The output file name is always derived from the generated package ID and
manifest fields as:
- `<generated_package_id>_artifact_<runtime>_<device_types[0]>.tar.gz`
- For Podman payloads, the final internal payload name is always `payload/image.tar`
- For LXC payloads, the generator requires a Yocto-style `.tar.gz` bundle, validates its layout,
and stores it inside the DCP using the original file name and format without re-packaging it.
@ -87,9 +90,9 @@ python3 meta-digi-containers/scripts/generate-dcp.py \
--payload /tmp/image.tar
```
This generates:
This generates a bundle named like:
- `./flutter-demo_artifact_podman_ccmp25-dvk.tar.gz`
- `./flutter-demo-<base36_created_at_ms>_artifact_podman_ccmp25-dvk.tar.gz`
## Digi Remote Manager metrics support
@ -97,6 +100,10 @@ This generates:
publish container statistics through the local CCCS Python API.
For generated DCPs, per-container DRM sampling is enabled
through `registration_defaults.stats_publish` in the artifact manifest.
Those manifest defaults establish the initial runtime policy on the target.
After installation, mutable policy such as `autostart`, `monitor`, `restart`, and
`stats_publish` can be inspected or updated through the container manager `config`
`get` and `set` operations without regenerating the DCP.
The image recipe generates the DCP automatically from the following variables:
- `CONTAINER_STATS_PUBLISH_ENABLED`
@ -169,14 +176,16 @@ Outputs are generated in:
Final outputs:
- `${CONTAINER_PACKAGE_ID}_artifact_podman_<device_types[0]>.tar.gz`
- `${CONTAINER_PACKAGE_ID}_artifact_lxc_<device_types[0]>.tar.gz`
- `${CONTAINER_PACKAGE_ID}-<base36_created_at_ms>_artifact_podman_<device_types[0]>.tar.gz`
- `${CONTAINER_PACKAGE_ID}-<base36_created_at_ms>_artifact_lxc_<device_types[0]>.tar.gz`
Notes:
- The generator script makes the DCP filename using `package_id`, `runtime`, and
`device_types[0]` fields from the manifest file.
- In Yocto builds, `CONTAINER_PACKAGE_ID` defaults to `${CONTAINER_NAME}`
- The generator script appends a base36-encoded millisecond timestamp suffix to
the input `package_id`, stores that generated value in the final
`manifest.json`, and uses it in the DCP file name.
- In Yocto builds, `CONTAINER_PACKAGE_ID` defaults to `${CONTAINER_NAME}` before
the generator adds the unique suffix.
Intermediate outputs generated during the build (LXC bundle, Podman archive, OCI/rootfs files)
are removed automatically at the end of artifact creation.

View File

@ -109,14 +109,14 @@ open(sys.argv[1], "w", encoding="utf-8").write(json.dumps(payload, indent=2) + "
changelog_arg="--changelog ${template_dir}/metadata/changelog.txt"
fi
python3 "${generator_script}" \
generated_output="$(python3 "${generator_script}" \
--manifest "${manifest_path}" \
--payload "${src_payload}" \
--output-dir "${DEPLOY_DIR_IMAGE}" \
${readme_arg} \
${changelog_arg}
${changelog_arg})"
generated_output="${DEPLOY_DIR_IMAGE}/${CONTAINER_PACKAGE_ID}_artifact_${runtime}_${primary_device_type}.tar.gz"
generated_output="$(printf '%s' "${generated_output}" | tail -n 1)"
if [ ! -s "${generated_output}" ]; then
bbfatal "Container artifact bundle not generated correctly: ${generated_output}"
fi

View File

@ -15,11 +15,33 @@ import sys
import tarfile
import tempfile
BASE36_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz"
def fail(message: str) -> "NoReturn":
raise SystemExit(f"error: {message}")
def to_base36(value: int) -> str:
if value < 0:
fail("cannot convert negative values to base36")
if value == 0:
return "0"
result: list[str] = []
while value:
value, remainder = divmod(value, 36)
result.append(BASE36_ALPHABET[remainder])
return "".join(reversed(result))
def format_created_at(timestamp: datetime) -> str:
return timestamp.astimezone(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
def build_generated_package_id(base_package_id: str, *, created_at_ms: int) -> str:
return f"{base_package_id}-{to_base36(created_at_ms)}"
def load_manifest(path: Path) -> dict:
try:
data = json.loads(path.read_text(encoding="utf-8"))
@ -167,8 +189,8 @@ def sha256(path: Path) -> str:
return digest.hexdigest()
def build_output_name(manifest: dict) -> str:
return f"{manifest['package_id']}_artifact_{manifest['runtime']}_{manifest['device_types'][0]}.tar.gz"
def build_output_name(*, package_id: str, runtime: str, device_type: str) -> str:
return f"{package_id}_artifact_{runtime}_{device_type}.tar.gz"
def ensure_lxc_layout(payload: Path) -> tuple[str, str]:
@ -188,9 +210,16 @@ def ensure_lxc_layout(payload: Path) -> tuple[str, str]:
return dirs[0].name, dirs[0].name
fail("invalid payload: LXC archive must contain rootfs/ and config")
def write_default_readme(path: Path, manifest: dict, *, created_at: str, payload_name: str) -> None:
def write_default_readme(
path: Path,
manifest: dict,
*,
package_id: str,
created_at: str,
payload_name: str,
) -> None:
content = (
f"Package: {manifest['package_id']}\n"
f"Package: {package_id}\n"
f"Version: {manifest['version']}\n"
f"Runtime: {manifest['runtime']}\n"
f"Platform: {manifest['device_types'][0]}\n"
@ -218,9 +247,17 @@ def write_manifest(path: Path, manifest: dict) -> None:
path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
def build_final_manifest(base: dict, *, artifact_type: str, digest: str, size_bytes: int, created_at: str) -> dict:
def build_final_manifest(
base: dict,
*,
package_id: str,
artifact_type: str,
digest: str,
size_bytes: int,
created_at: str,
) -> dict:
output = {
"package_id": base["package_id"],
"package_id": package_id,
"version": base["version"],
"runtime": base["runtime"],
"artifact_type": artifact_type,
@ -281,9 +318,19 @@ def main() -> int:
except OSError as exc:
fail(f"cannot create output directory {output_dir}: {exc}")
output_name = build_output_name(manifest)
created_at_dt = datetime.now(timezone.utc)
created_at_ms = int(created_at_dt.timestamp() * 1000)
created_at = format_created_at(created_at_dt)
generated_package_id = build_generated_package_id(
manifest["package_id"],
created_at_ms=created_at_ms,
)
output_name = build_output_name(
package_id=generated_package_id,
runtime=manifest["runtime"],
device_type=manifest["device_types"][0],
)
output_path = output_dir / output_name
created_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
with tempfile.TemporaryDirectory(prefix="dcp-bundle-") as tmpdir:
staging_root = Path(tmpdir)
@ -311,6 +358,7 @@ def main() -> int:
final_manifest = build_final_manifest(
manifest,
package_id=generated_package_id,
artifact_type=artifact_type,
digest=digest,
size_bytes=size_bytes,
@ -321,7 +369,13 @@ def main() -> int:
stage_metadata(
readme_path,
metadata_dir / "README.txt",
lambda dst: write_default_readme(dst, manifest, created_at=created_at, payload_name=payload_name),
lambda dst: write_default_readme(
dst,
manifest,
package_id=generated_package_id,
created_at=created_at,
payload_name=payload_name,
),
)
stage_metadata(changelog_path, metadata_dir / "changelog.txt", write_default_changelog)