distribution: gate producers by declared artifacts
This commit is contained in:
parent
482ad388d8
commit
a3ff22531f
3 changed files with 60 additions and 4 deletions
|
|
@ -19,8 +19,10 @@ its release lands. Those decisions live here, once.
|
||||||
- The signed `edge` archive is one multi-producer archive. Its implementation
|
- The signed `edge` archive is one multi-producer archive. Its implementation
|
||||||
currently lives in `souveraine/packaging/arch/publish-edge.sh`; this manifest
|
currently lives in `souveraine/packaging/arch/publish-edge.sh`; this manifest
|
||||||
names it so consumers do not grow their own publisher.
|
names it so consumers do not grow their own publisher.
|
||||||
- `../tools/validate-distribution.py` checks graph integrity without requiring
|
- `../tools/validate-distribution.py` checks graph integrity and can verify a
|
||||||
a runner, a device, or a secret.
|
producer's complete emitted package/architecture set without requiring a
|
||||||
|
device or a signing secret. A producer must fail rather than publish a
|
||||||
|
package absent from this graph.
|
||||||
- `.gitea/workflows/distribution.yml` makes that check a gate in the OS repo.
|
- `.gitea/workflows/distribution.yml` makes that check a gate in the OS repo.
|
||||||
|
|
||||||
The manifest is deliberately honest about maturity. A profile marked `blocked`
|
The manifest is deliberately honest about maturity. A profile marked `blocked`
|
||||||
|
|
|
||||||
|
|
@ -46,11 +46,12 @@ souveraine-q6voiced = { producer = "pixel3arch", architectures = ["aarch64"], st
|
||||||
hexagonrpc-blueline = { producer = "pixel3arch", architectures = ["aarch64"], state = "managed" }
|
hexagonrpc-blueline = { producer = "pixel3arch", architectures = ["aarch64"], state = "managed" }
|
||||||
tqftpserv-blueline = { producer = "pixel3arch", architectures = ["aarch64"], state = "managed" }
|
tqftpserv-blueline = { producer = "pixel3arch", architectures = ["aarch64"], state = "managed" }
|
||||||
blueline-edge-sense = { producer = "pixel3arch", architectures = ["aarch64"], state = "managed" }
|
blueline-edge-sense = { producer = "pixel3arch", architectures = ["aarch64"], state = "managed" }
|
||||||
souveraine-stevia = { producer = "pixel3arch", architectures = ["aarch64"], state = "managed" }
|
|
||||||
souveraine-squeekboard = { producer = "pixel3arch", architectures = ["aarch64"], state = "managed" }
|
souveraine-squeekboard = { producer = "pixel3arch", architectures = ["aarch64"], state = "managed" }
|
||||||
souveraine-stt = { producer = "pixel3arch", architectures = ["aarch64"], state = "managed" }
|
souveraine-stt = { producer = "pixel3arch", architectures = ["aarch64"], state = "managed" }
|
||||||
blueline-camera = { producer = "pixel3arch", architectures = ["aarch64"], state = "managed" }
|
blueline-camera = { producer = "pixel3arch", architectures = ["aarch64"], state = "managed" }
|
||||||
blueline-usb-gadget = { producer = "pixel3arch", architectures = ["aarch64"], state = "managed" }
|
blueline-usb-gadget = { producer = "pixel3arch", architectures = ["aarch64"], state = "managed" }
|
||||||
|
blueline-fingerprintd = { producer = "pixel3arch", architectures = ["aarch64"], state = "managed" }
|
||||||
|
blueline-boot-promote = { producer = "pixel3arch", architectures = ["aarch64"], state = "managed" }
|
||||||
hyprgrass = { producer = "pixel3arch", architectures = ["aarch64"], state = "managed" }
|
hyprgrass = { producer = "pixel3arch", architectures = ["aarch64"], state = "managed" }
|
||||||
smoo = { producer = "pixel3arch", architectures = ["aarch64"], state = "managed" }
|
smoo = { producer = "pixel3arch", architectures = ["aarch64"], state = "managed" }
|
||||||
usb-signaller = { producer = "pixel3arch", architectures = ["aarch64"], state = "managed" }
|
usb-signaller = { producer = "pixel3arch", architectures = ["aarch64"], state = "managed" }
|
||||||
|
|
@ -67,7 +68,7 @@ blockers = ["generic ARM provisioning has no completed installer"]
|
||||||
[profiles.blueline]
|
[profiles.blueline]
|
||||||
status = "blocked"
|
status = "blocked"
|
||||||
architectures = ["aarch64"]
|
architectures = ["aarch64"]
|
||||||
packages = ["souveraine", "upower-souveraine", "souveraine-updater", "souveraine-viewtop", "linux-blueline", "souveraine-callaudio", "souveraine-callaudiod", "souveraine-ucm-blueline", "souveraine-q6voiced", "hexagonrpc-blueline", "tqftpserv-blueline", "blueline-edge-sense", "souveraine-stevia", "souveraine-squeekboard", "souveraine-stt", "blueline-camera", "blueline-usb-gadget", "hyprgrass", "smoo", "usb-signaller"]
|
packages = ["souveraine", "upower-souveraine", "souveraine-updater", "souveraine-viewtop", "linux-blueline", "souveraine-callaudio", "souveraine-callaudiod", "souveraine-ucm-blueline", "souveraine-q6voiced", "hexagonrpc-blueline", "tqftpserv-blueline", "blueline-edge-sense", "souveraine-squeekboard", "souveraine-stt", "blueline-camera", "blueline-usb-gadget", "blueline-fingerprintd", "blueline-boot-promote", "hyprgrass", "smoo", "usb-signaller"]
|
||||||
blockers = ["linux-blueline is not published to the shared archive", "rootfs overlay content still needs package ownership"]
|
blockers = ["linux-blueline is not published to the shared archive", "rootfs overlay content still needs package ownership"]
|
||||||
|
|
||||||
[profiles.x86-laptop]
|
[profiles.x86-laptop]
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,14 @@ def main() -> int:
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument("manifest", type=Path)
|
parser.add_argument("manifest", type=Path)
|
||||||
parser.add_argument("--ready", metavar="PROFILE", help="require one profile to be installer-ready")
|
parser.add_argument("--ready", metavar="PROFILE", help="require one profile to be installer-ready")
|
||||||
|
parser.add_argument("--producer", metavar="NAME", help="validate artifacts emitted by one producer")
|
||||||
|
parser.add_argument(
|
||||||
|
"--artifact",
|
||||||
|
metavar="PACKAGE:ARCHITECTURE",
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
help="one package artifact emitted by --producer; repeat for every artifact",
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
try:
|
try:
|
||||||
with args.manifest.open("rb") as source:
|
with args.manifest.open("rb") as source:
|
||||||
|
|
@ -89,6 +97,49 @@ def main() -> int:
|
||||||
if target.get("state") == "supported" and profile.get("status") == "planned":
|
if target.get("state") == "supported" and profile.get("status") == "planned":
|
||||||
error(f"target {target_name}: supported target cannot use a planned profile")
|
error(f"target {target_name}: supported target cannot use a planned profile")
|
||||||
|
|
||||||
|
if args.producer:
|
||||||
|
if args.producer not in producers:
|
||||||
|
error(f"unknown producer {args.producer!r}")
|
||||||
|
if not args.artifact:
|
||||||
|
error(f"producer {args.producer}: no artifacts declared")
|
||||||
|
|
||||||
|
emitted: set[tuple[str, str]] = set()
|
||||||
|
for artifact in args.artifact:
|
||||||
|
try:
|
||||||
|
package_name, architecture = artifact.rsplit(":", 1)
|
||||||
|
except ValueError:
|
||||||
|
error(f"invalid artifact {artifact!r}; expected PACKAGE:ARCHITECTURE")
|
||||||
|
continue
|
||||||
|
package = packages.get(package_name)
|
||||||
|
if package is None:
|
||||||
|
error(f"producer {args.producer}: undeclared artifact {artifact}")
|
||||||
|
continue
|
||||||
|
declared_producers = package.get("producers", [package.get("producer")])
|
||||||
|
if declared_producers != [args.producer]:
|
||||||
|
error(f"producer {args.producer}: does not own {package_name}")
|
||||||
|
if package.get("state") != "managed":
|
||||||
|
error(f"producer {args.producer}: {package_name} is {package.get('state')}, not managed")
|
||||||
|
if architecture not in package.get("architectures", []):
|
||||||
|
error(f"producer {args.producer}: {package_name} does not declare {architecture}")
|
||||||
|
if architecture not in producers.get(args.producer, {}).get("architectures", []):
|
||||||
|
error(f"producer {args.producer}: does not declare {architecture}")
|
||||||
|
emitted.add((package_name, architecture))
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
(name, architecture)
|
||||||
|
for name, package in packages.items()
|
||||||
|
if package.get("state") == "managed"
|
||||||
|
and package.get("producer") == args.producer
|
||||||
|
for architecture in package.get("architectures", [])
|
||||||
|
}
|
||||||
|
if emitted != expected:
|
||||||
|
missing = sorted(expected - emitted)
|
||||||
|
unexpected = sorted(emitted - expected)
|
||||||
|
if missing:
|
||||||
|
error(f"producer {args.producer}: missing artifacts {missing}")
|
||||||
|
if unexpected:
|
||||||
|
error(f"producer {args.producer}: unexpected artifacts {unexpected}")
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
print("distribution manifest is invalid:", file=sys.stderr)
|
print("distribution manifest is invalid:", file=sys.stderr)
|
||||||
print("\n".join(f" - {item}" for item in errors), file=sys.stderr)
|
print("\n".join(f" - {item}" for item in errors), file=sys.stderr)
|
||||||
|
|
@ -106,6 +157,8 @@ def main() -> int:
|
||||||
if profile.get("status") != "ready":
|
if profile.get("status") != "ready":
|
||||||
print(f"profile {args.ready} is not ready for a public installer", file=sys.stderr)
|
print(f"profile {args.ready} is not ready for a public installer", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
if args.producer:
|
||||||
|
print(f"producer contract valid: {args.producer}, {len(args.artifact)} artifacts")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue