Design proposal — consumer flow: multi-registry resolve + install ergonomics¶
Status: proposed / under discussion. Decisions in §5 are firm; a lot is still open (§6). Target: TBD — Part A (multi-registry) is a strong candidate to slot ahead of the planned 0.15.0 IP-XACT ports work (0.15.0-ipxact-ports-businterfaces.md); sequencing is itself an open question (§8).
Captures a design conversation driven by a real adopter evaluating the consumer side.
1. Motivation (from a real adopter)¶
Two questions from a customer integrating IP at the SoC level:
- Q1 — multiple registries. An SoC declares ~4 dependencies. They are easy to identify by VLNV, but what if they live in different registries — one in JFrog Artifactory (OCI), another in a Git repo? The customer suggested baking the registry into the dependency path (docker-compose / image-ref style); we prefer registries supplied on the command, not per-VLNV.
- Q2 — one command. Why several commands to consume an IP (resolve → install → pull),
versus
pip install <name>? And — once source isn't necessarily "pulled" — how does a Makefile-based consumer get the source on disk if the cache only holds the.ipkg?
2. Current behavior (grounding — what ships today in 0.14.0)¶
--registryis single-valued per command (not repeatable).installis already a one-shot:hdlpkg install <manifest> --registry …does **resolve- write
ip.lock+ fetch the.ipkginto the content-addressed cache in one command (_cmd_install).resolve= update the lock without fetching;pull <vlnv> --output DIR= extract one** core's source tree to disk;gen <target>= emit ready-to-run tool inputs (extracting deps into<cache>/src/<digest>/). - The lockfile already records a per-package
source(a free-form string), set at resolve fromregistry.source_for(vlnv):path:…,registry:http://…,oci://…, orgit+<url>@<sha>(lockfile.py,_build_lock). So the lock is already heterogeneous-registry capable — only the single--registryon the CLI limits it. No lock-format change is needed for multi-registry.
3. Part A — multi-registry resolve¶
3a. Rejected: registry baked into the VLNV (docker-compose style)¶
A VLNV is the core's identity; it must mean the same thing wherever the core is hosted.
Putting the registry in the dependency path couples identity to location and breaks down:
re-hosting or mirroring an IP would force edits to every consumer's ip.toml; a diamond
dependency on the same core reached via two registries becomes two "different" packages; and
per-dependency infra/credentials leak into the manifest. Cargo, npm, and pip all keep the
package name pure and treat registries/indexes as configuration. Firm rejection.
3b. Proposed: an ordered search path via repeatable --registry + a CompositeRegistry¶
- Make
--registryrepeatable; CLI order = precedence. - A small
CompositeRegistry(implements the existingRegistryinterface) aggregates the backends:versions(ref)returns the union across all registries (the resolver sees every available version);manifest/artifact_bytes/source_for(vlnv)delegate to the first registry in order that has that exact VLNV. source_fortherefore returns the actual registry used, recorded in the lock — so a resolve spanning JFrog + Git + a local path pins each core to its true origin, no format change. Checksums catch content divergence: if the same exact VLNV exists in two registries with different bytes, first-wins pins one digest and any later fetch of different bytes failslock.verify. Add a warning when a VLNV is shadowed (found in >1 registry with differing digests) so it is visible.
3c. Bonus: lock-driven --locked fetch¶
Because the lock stores each package's source, install --locked / gen --locked could
dispatch each package through registry_from_location(pkg.source) and fetch it from its own
recorded registry — multi-registry "just works" from the lock with zero --registry
flags, and stays reproducible. (Today --locked re-uses the single --registry passed.)
4. Part B — consumer flow ergonomics¶
4a. install is already the single consumer command¶
The perceived "many commands" is largely a docs/mental-model gap: the normal consume flow is
install (resolve + lock + fetch) then gen (build). resolve (lock-only) and pull
(extract one core) are specialized. Keeping resolve/--locked split is deliberate — it is what
makes builds reproducible (the opposite of pip conflating everything).
4b. A pip-like declare-and-install one-shot¶
Today declaring + installing a new dep is hdlpkg add <vlnv> then hdlpkg install. Propose a
one-shot hdlpkg install <vlnv> [<vlnv>…] that adds the dep(s) to ip.toml, resolves,
locks, and caches — the real pip install <name> experience. Build (gen) stays separate.
4c. The Makefile reuse gap — vendor source to a predictable tree¶
The content-addressed cache holds .ipkg blobs, not loose source, so a Makefile cannot read
it directly. To feed a Makefile the source must be extracted to disk. Today that is
pull <vlnv> --output DIR per dependency (the "replace the submodule" flow), or gen for the
built-in toolflows (which extracts into <cache>/src/<digest>/, an internal path). install
alone is not enough for a Makefile consumer. Propose an install-and-vendor capability — a
hdlpkg vendor/sync command, or install --vendor DIR — that extracts all locked deps
into a predictable layout (e.g. deps/<vendor>/<name>/), the node_modules of HDL, so existing
Makefiles can include them.
4d. Encrypted IP is forward-compatible¶
Extraction still lays files down even for encrypted IP — they are just encrypted envelopes
(IEEE 1735 pragma protect); the source is physically present for the tool to include and the
EDA tool decrypts at compile time. The cache/lock pin the ciphertext digest. So "vendor to
a directory → your Makefile builds" holds; the bytes are simply opaque to humans. ("Not
readable" ≠ "not on disk.")
5. Decisions so far (firm)¶
- VLNV stays location-independent — reject registry-in-the-VLNV.
- Multiple registries are a CLI/config search path, ordered, not per-dependency.
- No lock-format change for multi-registry — the per-package
sourcealready exists. gen(build) stays a separate, deliberate step frominstall(fetch).
6. Open questions / still negotiable¶
- Duplicate-VLNV precedence. First-in-order wins + warning (proposed) — or hard-error when the same exact VLNV appears in >1 registry, forcing the user to disambiguate? Error-mode is safer but noisier.
- Version union vs registry pinning. Should
versions(ref)union across all registries (proposed, maximizes availability) — or, once a package is first seen in registry N, should all its versions come from N (avoids mixing sibling versions across registries)? - Where registries are declared. CLI-only (
--registry …), or also a project/workspace config (e.g. a[registries]list or a separate, possibly gitignored config file)? A per-project list is convenient but re-introduces some config; it must not become per-dep. --lockedsource precedence. Should--lockedfetch from the lock's recordedsource(ignoring--registry), from--registry, or let--registryoverride? Changing the current default (re-use--registry) has compatibility implications.- Unreachable registry during resolve. Fall through to the next registry silently, warn, or fail? Affects the reproducibility / offline guarantees.
- One-shot command surface. Overload
install <vlnv>vs a new flag vsadd --install. Ifinstallaccepts both a manifest path and VLNVs, resolve the ambiguity. - Vendoring shape. New
vendor/synccommand vsinstall --vendor DIR? Layout (deps/<vendor>/<name>/vs include the version/library)? Opt-in vs default? How it coexists withgen's own<cache>/src/<digest>/extraction (avoid two source copies). - Default consumer posture. node_modules-style (vendor by default) vs Go-modules-style (build from cache, vendor on request)? This shapes how strong the "one command" story is.
- Credentials across many registries. Already per-host via the credential store; confirm no need for per-registry inline tokens.
- Resolver semantics unchanged. Confirm multi-registry does not alter unification/conflict policy — it only widens the candidate set.
7. Phasing (tentative)¶
- [done —
feature/multi-registry]CompositeRegistry+ repeatable--registryacross resolve/install/gen/tree/pull. The CLI order is the precedence;versionsunions across all reachable backends andmanifest/artifact_bytes/source_for(and the inheritedfetch) delegate to the first backend that has the exact VLNV (so the lock pins each core's true origin — no lock-format change). A shadowed VLNV warns and takes the first; an unreachable backend is skipped with a warning. Decisions taken with the maintainer: first-wins + warn on duplicate VLNV, union versions across registries, warn + continue on an unreachable registry. Implemented inregistry.py(CompositeRegistry,composite_registry_from_locations,_registry_label) andcli.py(repeatable--registry,_selected_registries,_print_registry_warnings); covered bytests/unit/test_composite_registry.pyandtests/integration/test_multi_registry_cli.py, documented in the user guide. Caveat: a backend whoseversions()swallows transport errors and returns[](e.g.HttpRegistry) cannot be distinguished from "unknown package", so the unreachable-warning fires only when a backend actually raises — acceptable for now, revisit if it bites. - [done —
feature/multi-registry]--lockedfetch from the lock'ssource(offline + multi-registry straight from the lock). Wheninstall --locked/gen --lockedis run with no--registry/--search, each package is fetched from the exactsourceits lock entry recorded, via a newLockSourceRegistrythat dispatches each VLNV throughregistry_from_lock_source(which maps the recordedpath:/registry:/oci:/git+…@shaforms back to a backend). An explicit--registry/--searchstill overrides (prior behavior). Decision taken for the §6 open question:--lockeddefaults to the lock's recorded source, and--registryoverrides it — backward compatible (explicit flags behave as before) while making the no-flag case "install from the lock". Caveat: the recorded OCIsourcedoes not encode the transport scheme, so it is rebuilt as HTTPS; a plaintextoci+http://registry must be reached with an explicit--registry. Implemented inregistry.py(registry_from_lock_source,LockSourceRegistry) andcli.py(_locked_registry); covered bytests/unit/test_lock_source_registry.pyandtests/integration/test_locked_from_lock_source_cli.py; documented in the user guide. - [done —
feature/multi-registry]hdlpkg install <vlnv>one-shot (declare + resolve + lock + cache).install's positional becamenargs="*": avendor:library:name[@constraint]token is added to the manifest's[dependencies](reusingediting.add_dependency), then the normal resolve/lock/cache runs; a non-spec token is the manifest path (defaultip.toml, at most one), and several specs may be added at once. Decision for the §6 "command surface" open question: overloadinstall(not a new flag /add --install), with spec-vs-path disambiguation by whether the token parses as aPackageRef. Adding deps re-resolves, so it is refused with--locked; a self-dependency and two manifest paths are refused too. Implemented incli.py(_install_targets,_add_dependencies,_parse_dependency_spec/_looks_like_dependency_specfactored out of_cmd_add); covered bytests/integration/test_install_oneshot_cli.py; man page regenerated; documented in the user guide. Build (gen) stays a separate, deliberate step (firm decision §5.4). - [done —
feature/multi-registry]vendorsource materialization to a predictable tree (the Makefile case). A newhdlpkg vendor [path]command reads the lock, fetches each core (reusing_locked_registry, so it works from the recorded source / cache, offline), verifies the digests against the lock (fail closed), and extracts each into<DIR>/<vendor>/<library>/<name>/(default./deps), clearing any stale tree first so the vendored copy matches the lock. Decisions for the §6 "vendoring shape" open question: a newvendorcommand (notinstall --vendor), layout<vendor>/<library>/<name>/(include library — VLNV identity is vendor:library:name — but not the version, so a Makefile include path is stable across version bumps), opt-in (run it explicitly; the default posture stays Go-modules-style "build from cache, vendor on request", not node_modules-style vendor-by-default). It coexists withgen's own<cache>/src/<digest>/extraction rather than replacing it —vendoris for consumers driving their own Makefile,genfor the built-in toolflows. Implemented incli.py(_cmd_vendor); covered bytests/integration/test_vendor_cli.py; man page regenerated; documented in the user guide. - [done —
feature/multi-registry] Docs + ahdlpkg-livetestscenario exercising heterogeneous registries. The user guide gained sections for the multi-registry search path, installing straight from the lock, theinstall <vlnv>one-shot, andhdlpkg vendor;vendoris listed in the README andarchitecture.mdcommand tables.hdlpkg-livetestgained a--multi-registrymode (run_livetest.py:test_multi_registry) that drives all four phases end to end through the real CLI against two local-directory registries — union resolve, first-wins shadow + warning,--lockedfetch from the recorded source,vendor, andinstall <vlnv>— needing no server, Docker, or network (passes locally).
Parts A (1–2) and B (3–4) are independent; A can ship first. All five phases are now
implemented on feature/multi-registry.
8. Sequencing¶
Resolved in implementation: all five phases landed together on feature/multi-registry
(no ip.toml/ip.lock format change; the CLI surface grew the repeatable --registry, the
install <vlnv> overload, and the new vendor command). What remains is a maintainer call on
which 0.x release carries it (a candidate to slot as 0.15.0 ahead of, or alongside, the
IP-XACT ports work) and merging feature/multi-registry -> develop.
The original open question follows.
Open. This work is customer-driven and touches the core consume flow every adopter uses. Candidate: slot Part A (multi-registry) as the next release ahead of, or alongside, the planned 0.15.0 IP-XACT ports work — to be decided with the maintainer. No version is committed in this document yet.