Skip to content
Merged
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ A package for packaging, organizing, and combining artifacts

The current core schemas are:

- `artlink.manifest/v0` for artifact manifests.
- `artlink.manifest/v0` for artifact manifests; its JSON Schema is published at `artlink/schemas/artlink.manifest-v0.json` and `artlink.manifest_json_schema()` returns the same document.
- `artlink.template/v0` for declarative manifest validation templates.
- `artlink.registry/v0` for registry configuration.

Expand Down Expand Up @@ -253,6 +253,8 @@ Each entry point may load a `Manifest`, a manifest file path, a callable returni

By default, a registry rejects multiple manifests with the same name. This is useful for ecosystems such as Python where one installed distribution should own a package name. Hardware package managers often need multiple versions of a reusable block side by side, so they can opt in with `ArtifactRegistry(allow_manifest_versions=True)` or `ArtifactRegistry.from_install_path(prefix, allow_manifest_versions=True)`. Versioned registries resolve `Reference(kind="manifest", target="hdl-filter", version="2.0.0")`; an unversioned reference is rejected if more than one version is available.

With `manifest_version_policy="highest"`, an unversioned reference to a name with several versions resolves to the highest one, where versions are compared by splitting the string into runs of digits and runs of other characters: digit runs compare as integers (so `1.10` is above `1.9`), other runs compare as text, a digit run sorts below a text run at the same position, and a version with more runs sorts above its prefix (so `2.0.0-rc1` is above `2.0.0`). This is a lexical rule, not a release-aware one; artifact ecosystems with pre-release conventions should pin versions in their references.

Registry YAML is a tool configuration file, not an artifact manifest. Its job is to tell a consuming process which install roots, manifest files, template files, and direct local artifacts should be visible in an `ArtifactRegistry`. `load_registry(path)` resolves relative `install_roots`, `manifest_files`, `template_files`, and direct artifact roots relative to the registry config file.

The artlink-owned shape uses a registry schema id:
Expand Down
13 changes: 13 additions & 0 deletions artlink/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,19 @@

__all__ = (
"ARTLINK_MANIFEST_SCHEMA",
"MANIFEST_JSON_SCHEMA_PATH",
"Manifest",
"artifact_path",
"load_manifest",
"manifest_from_mapping",
"manifest_json_schema",
"validate_artifact_files",
)


ARTLINK_MANIFEST_SCHEMA = "artlink.manifest/v0"
# the published JSON Schema for that schema id; a test holds it equal to the model
MANIFEST_JSON_SCHEMA_PATH = Path(__file__).resolve().parent / "schemas" / "artlink.manifest-v0.json"


class Manifest(_ManifestModel):
Expand Down Expand Up @@ -72,6 +76,15 @@ def to_yaml_text(self) -> str:
return yaml.safe_dump(self.model_dump(mode="json"), sort_keys=False)


def manifest_json_schema() -> dict[str, Any]:
"""The JSON Schema of ``artlink.manifest/v0`` as the ``Manifest`` model
defines it, keyed by the yaml field names."""
schema = Manifest.model_json_schema(by_alias=True)
schema["$schema"] = "https://json-schema.org/draft/2020-12/schema"
schema["$id"] = "https://github.com/dau-dev/artlink/blob/main/artlink/schemas/artlink.manifest-v0.json"
return schema


def load_manifest(path: Path, *, validate_paths: bool = False, root: Path | None = None) -> Manifest:
"""Read a manifest file. Every failure, unreadable file, bad YAML or an
invalid document, is a ManifestError."""
Expand Down
244 changes: 244 additions & 0 deletions artlink/schemas/artlink.manifest-v0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
{
"$defs": {
"Artifact": {
"additionalProperties": false,
"description": "A packageable or consumable unit of content.",
"properties": {
"digest": {
"anyOf": [
{
"$ref": "#/$defs/Digest"
},
{
"type": "null"
}
],
"default": null
},
"format": {
"default": "",
"title": "Format",
"type": "string"
},
"id": {
"default": "",
"title": "Id",
"type": "string"
},
"kind": {
"default": "",
"title": "Kind",
"type": "string"
},
"language": {
"default": "",
"title": "Language",
"type": "string"
},
"media_type": {
"default": "",
"title": "Media Type",
"type": "string"
},
"metadata": {
"additionalProperties": true,
"title": "Metadata",
"type": "object"
},
"name": {
"default": "",
"title": "Name",
"type": "string"
},
"path": {
"anyOf": [
{
"format": "path",
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Path"
},
"provides": {
"items": {
"anyOf": [
{
"$ref": "#/$defs/Capability"
},
{
"type": "string"
},
{
"additionalProperties": true,
"type": "object"
}
]
},
"title": "Provides",
"type": "array"
},
"requires": {
"items": {
"anyOf": [
{
"$ref": "#/$defs/Capability"
},
{
"type": "string"
},
{
"additionalProperties": true,
"type": "object"
}
]
},
"title": "Requires",
"type": "array"
},
"role": {
"title": "Role",
"type": "string"
},
"uri": {
"default": "",
"title": "Uri",
"type": "string"
}
},
"required": [
"role"
],
"title": "Artifact",
"type": "object"
},
"Capability": {
"additionalProperties": false,
"description": "A named capability that an artifact provides or requires.",
"properties": {
"kind": {
"default": "",
"title": "Kind",
"type": "string"
},
"metadata": {
"additionalProperties": true,
"title": "Metadata",
"type": "object"
},
"name": {
"title": "Name",
"type": "string"
}
},
"required": [
"name"
],
"title": "Capability",
"type": "object"
},
"Digest": {
"additionalProperties": false,
"properties": {
"algorithm": {
"title": "Algorithm",
"type": "string"
},
"value": {
"title": "Value",
"type": "string"
}
},
"required": [
"algorithm",
"value"
],
"title": "Digest",
"type": "object"
},
"Reference": {
"additionalProperties": false,
"properties": {
"kind": {
"enum": [
"manifest",
"template"
],
"title": "Kind",
"type": "string"
},
"metadata": {
"additionalProperties": true,
"title": "Metadata",
"type": "object"
},
"target": {
"title": "Target",
"type": "string"
},
"version": {
"default": "",
"title": "Version",
"type": "string"
}
},
"required": [
"kind",
"target"
],
"title": "Reference",
"type": "object"
}
},
"$id": "https://github.com/dau-dev/artlink/blob/main/artlink/schemas/artlink.manifest-v0.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"artifacts": {
"items": {
"$ref": "#/$defs/Artifact"
},
"title": "Artifacts",
"type": "array"
},
"intent": {
"default": "",
"title": "Intent",
"type": "string"
},
"metadata": {
"additionalProperties": true,
"title": "Metadata",
"type": "object"
},
"name": {
"title": "Name",
"type": "string"
},
"references": {
"items": {
"$ref": "#/$defs/Reference"
},
"title": "References",
"type": "array"
},
"schema": {
"default": "artlink.manifest/v0",
"title": "Schema",
"type": "string"
},
"version": {
"default": "",
"title": "Version",
"type": "string"
}
},
"required": [
"name"
],
"title": "Manifest",
"type": "object"
}
61 changes: 61 additions & 0 deletions artlink/tests/test_manifest_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""``artlink.manifest/v0`` has a published JSON Schema, generated from the
``Manifest`` model and held equal to it, and the manifests this package
writes and documents validate against it."""

import json
import re
from pathlib import Path

import jsonschema
import yaml

from artlink import Artifact, Capability, Digest, Manifest, manifest_json_schema
from artlink.manifest import MANIFEST_JSON_SCHEMA_PATH

README = Path(__file__).resolve().parents[2] / "README.md"


def test_the_published_schema_is_the_model():
published = json.loads(MANIFEST_JSON_SCHEMA_PATH.read_text(encoding="utf-8"))
assert published == manifest_json_schema(), "regenerate artlink/schemas/artlink.manifest-v0.json from manifest_json_schema()"
assert published["$schema"] == "https://json-schema.org/draft/2020-12/schema"
jsonschema.Draft202012Validator.check_schema(published)


def test_a_written_manifest_validates_against_the_schema():
manifest = Manifest(
name="golden",
version="1.2.3",
intent="input",
artifacts=(
Artifact(
id="rtl",
path=Path("rtl/filter.sv"),
kind="source",
role="hdl-source",
provides=(Capability(kind="hdl-module", name="filter"), "filter"),
digest=Digest(algorithm="sha256", value="a" * 64),
),
Artifact(uri="https://example.invalid/doc.pdf", role="docs"),
),
)
document = yaml.safe_load(manifest.to_yaml_text())
jsonschema.validate(document, manifest_json_schema())
assert Manifest(**document) == manifest


def test_the_readme_manifest_examples_validate_against_the_schema():
text = README.read_text(encoding="utf-8")
documents = [yaml.safe_load(block) for block in re.findall(r"```yaml\n(.*?)```", text, re.DOTALL)]
manifests = [doc for doc in documents if isinstance(doc, dict) and doc.get("schema") == "artlink.manifest/v0"]
assert manifests
for document in manifests:
jsonschema.validate(document, manifest_json_schema())


def test_the_schema_refuses_what_the_model_refuses():
schema = manifest_json_schema()
validator = jsonschema.Draft202012Validator(schema)
assert list(validator.iter_errors({"schema": "artlink.manifest/v0", "name": "m", "unknown": 1})) # additionalProperties
assert list(validator.iter_errors({"schema": "artlink.manifest/v0"})) # name required
assert list(validator.iter_errors({"schema": "artlink.manifest/v0", "name": "m", "references": [{"kind": "package", "target": "x"}]}))
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ develop = [
"check-dist",
"codespell",
"hatchling",
"jsonschema",
"mdformat",
"mdformat-tables>=1",
"pytest",
Expand Down