Skip to content

Latest commit

 

History

History
328 lines (280 loc) · 15.8 KB

File metadata and controls

328 lines (280 loc) · 15.8 KB

Plugin workspace dependency contract

ost plugin test --workspace discovers bundle manifests in immediate child directories and plugins/*, plus plain-library descriptors in immediate child directories and libs/*. It sorts them by path, validates their dependency graph, and only then resolves a runtime or runs per-bundle verification. The validated graph supplies each bundle's transitive runtime/test closure and deterministic bundle and library build order.

The graph check is separately askable:

ost plugin test --workspace --graph-only     # graph only; exits on its result

--graph-only runs the dependency-graph checks and stops. It needs no build, no resolved runtime, and no packaged artifact, so it belongs at the front of a PR lane rather than behind everything the per-bundle pyramid requires. Without it the two were welded together: on a fresh checkout the verb validated the graph, reported it valid, and then failed because nothing had been built yet, leaving a repository to either build every bundle or parse the graph out of --json.

Versioned manifest extension

Legacy manifests without composition fields remain valid. A manifest that declares requires.bundles, requires.libraries, or provides schema.contract must opt into the extension explicitly:

manifest:
  schema: openstrata.plugin/v1alpha1
plugin:
  name: vrmFormat
  version: 0.2.0
  kind: usd-fileformat
runtime:
  openusd: ">=25.05,<27.0"
requires:
  capabilities: [usd-stage-read]
  bundles:
    - id: vrmSchema
      version: ">=0.2,<0.3"
      contract: 1
usd:
  plug_info: plugin/resources/vrmFormat/plugInfo.json

Each dependency requires a portable bundle id and a numeric dotted-version range. contract is allowed only when the provider is a usd-schema bundle. Dependency entries reject unknown keys.

A schema provider declares its authored-data contract separately from its semantic implementation version:

manifest:
  schema: openstrata.plugin/v1alpha1
plugin:
  name: vrmSchema
  version: 0.2.4
  kind: usd-schema
schema:
  codeless: true
  contract: 1

Compatible implementation releases keep contract unchanged. A breaking type, property, or token surface increments it and requires authored-data migration notes. Consumers of a versioned schema contract must select it explicitly.

Versioned manifests recursively reject unknown keys below requires:. Plain libraries use a separate producer descriptor while the plugin consumer names only identity and compatible version:

# plugins/usdVrm/openstrata.plugin.yaml
requires:
  libraries:
    - id: vrmContainer
      version: ">=0.1,<0.2"
# libs/vrmContainer/openstrata.library.yaml
schema: openstrata.library/v1alpha1
library:
  id: vrmContainer
  version: 0.1.0
cmake:
  package: vrmContainer
  target: vrmContainer::vrmContainer
runtime:
  directories: [bin, lib]

The library may itself declare requires.libraries for a transitive closure. OST validates missing, duplicate, incompatible, malformed, and cyclic library edges. It never infers identity from CMake target names; consumers continue to use find_package(vrmContainer CONFIG REQUIRED). A library descriptor carries no plugin kind, plugInfo.json, registration, or OpenUSD dependency. Legacy plugin manifests retain their previous permissive parsing for compatibility, but using either composition field requires the versioned plugin header.

Dependency directions

  • A public schema bundle has no dependency on a file format, resolver, or other plugin implementation.
  • An asset resolver cannot depend on a file-format bundle.
  • A file-format bundle may consume schema and resolver bundles.
  • Every cycle, including a self-cycle, is invalid.

These checks preserve standalone bundle ownership. Composition does not synthesize add_subdirectory links or link targets.

Source-workspace composition

After graph validation succeeds, source-workspace commands consume the same graph rather than asking each caller to restate it:

  • plugin test --workspace composes each primary bundle with its transitive dependency closure before running L2 and above;
  • a selected plugin doctor|test|run <bundle> resolves the same closure when its containing workspace is unambiguous;
  • plugin build <bundle> builds source dependencies in deterministic topological order, installs them to an OST-owned target-specific prefix, and passes that prefix through normal CMake package discovery;
  • plain-library runtime directories materialized below that prefix are added to the loader environment for selected test/run/view sessions;
  • plugin inspect --json and test report dependencies.json expose selected library identity, version, descriptor, CMake package/target, prefix, runtime paths, and source-workspace provenance;
  • generated source-CI cells use the manifest closure selected by bundle: and do not gain a second, manually maintained with: list;
  • explicit --with remains additive for external or ad-hoc bundles and keeps its existing caller-defined ordering.

A selected primary bundle that declares neither requires.bundles nor requires.libraries has an empty closure and skips workspace discovery entirely: unrelated sibling bundles (a broken manifest, a stale copy) cannot fail its commands. Once a bundle declares dependencies, an unloadable or invalid workspace graph fails closed.

Dependency builds install, deepest dependency first, into .strata/targets/<target-id>/workspace-prefix. OpenStrata prepends that private prefix to CMAKE_PREFIX_PATH, so consumers use normal installed CMake package discovery. The prefix is target-specific and rebuilt for a composed build; it is not part of a bundle's installed interface.

The primary bundle keeps priority in the plugin and loader search paths; resolved dependencies follow in a stable order, then the runtime. Duplicate bundle identities are rejected or deduplicated only after identity/version/ contract agreement—path order must not silently pick a provider.

A plugin package materializes its selected plain-library runtime under runtime/libraries/, adds those directories to the packaged manifest's loader paths, and records the library closure in dependencies.json and the artifact manifest.

Every package also carries openstrata.activation.json, activate.ps1, activate.sh, and openstrata_activate.py. The JSON document is the portable consumer contract: it names the package-relative USD plugin, dynamic-library, and Python roots plus the target OS loader variable. Dot-source activate.ps1 or source activate.sh to prepend the existing roots without requiring ost. On Windows with Python 3.8 or newer, import openstrata_activate before pxr; the module calls os.add_dll_directory() for every packaged library root and retains the handles for the life of the process. This is the supported bridge from requires.runtime_libs to non-ost consumers; parsing the plugin YAML and guessing loader behavior is not.

For example, after the OpenUSD host itself is active:

# OpenUSD command-line host
. .\activate.ps1
usdcat tests/fixtures/minimal.vrm

# Python 3.8+ on Windows: retain package DLL-directory handles before pxr loads.
python -c "import openstrata_activate; from pxr import Usd; assert Usd.Stage.Open('tests/fixtures/minimal.vrm')"

The OpenUSD installation remains responsible for activating its own bin/lib/Python directories; the package entrypoint adds and retains the package's staged dependency directories. A vendor/runtime Python launcher normally provides the first half. When embedding OpenUSD into a stock Windows Python process, register the host's DLL directories with os.add_dll_directory() before importing openstrata_activate.

Package-origin verification carries its oracle too. For every declared tests.roundtrip fixture that has an adjacent <fixture>.golden.usda, ost plugin package stages both files and emits openstrata.verification.json. That versioned contract records the fixture/oracle pair and both SHA-256 digests; the artifact manifest.json points to it and includes both files in its hashed files[] inventory. ost plugin test --from-package --up-to 5 verifies the contract before flattening. An oracle absent from source remains an optional L5 SKIP, but an oracle declared by the packaged contract that is missing or has changed is a validation failure.

Managed plugin builds also bind packages to the bytes they produced. After a successful ost plugin build, the target's .ost-build-complete.json records the target/runtime/compiler/generator build fingerprint and the path, size, and SHA-256 of the primary bundle's package-relevant registration, library, and Python outputs. ost plugin package recomputes that set and reports one of:

Status Meaning
matched Every current package-relevant output matches the last managed build.
untracked No output-bearing managed completion exists; the bytes may come from plain CMake or another external producer.
mismatched A managed output is missing, changed, or newly present relative to the completion.

mismatched fails packaging by default with the changed path, expected and observed digests, and last build fingerprint. If an external/plain-CMake output is intentional, --allow-unmanaged-output permits packaging while recording the origin as external-or-unmanaged-override; it does not rewrite the status to matched. The same object is emitted in human/JSON package output and under provenance.build_outputs in the artifact manifest. An artifact with no managed completion remains packageable as untracked, preserving plain CMake as a supported producer without presenting it as an ost build.

ost plugin package --workspace --product additionally emits one aggregate openstrata.plugin-product artifact. Its archive has this fixed layout:

openstrata.product.json
members/<bundle-id>/<bundle archive>.tar.zst
members/<bundle-id>/manifest.json
members/<bundle-id>/SHA256SUMS
members/<bundle-id>/sbom.spdx.json
members/<bundle-id>/provenance.intoto.jsonl  # when the member has provenance

openstrata.product.json records the validated dependency order and each member's archive digest, manifest, checksums, evidence, optional debug archive, and dependency closure. The product is built from the exact per-bundle package outputs—not from sibling source paths—so every member remains independently verifiable after a single product download. Product identity is the enclosing project's project.name, effective project version, and target; its archive is named <name>-<version>-<target>-plugin-product.tar.zst. Member bundle names and versions stay independent and are pinned by the product contract.

Use the product commands instead of unpacking members manually:

# A producer dist directory or its manifest.json carries the expected digest.
ost plugin product verify dist/products/<name>/<version>/<target>
ost plugin product install dist/products/<name>/<version>/<target> \
  --prefix ./installed-product

# A standalone downloaded archive can be pinned explicitly.
ost plugin product verify product.tar.zst --expect-digest sha256:<64-hex>
ost plugin product install product.tar.zst --expect-digest sha256:<64-hex> \
  --prefix ./installed-product

Verification covers the product digest, strict contract and order, member archive digests/sizes, each member SHA256SUMS, evidence presence, extracted file inventory, and bundle validity. Installation refuses to replace an existing prefix, expands members under bundles/<id>/ in dependency order, and emits aggregate activate.ps1, activate.sh, openstrata.activation.json, and openstrata_activate.py entrypoints. The aggregate itself also has a producer manifest, SBOM, digest, and registry kind product, so ost artifact import / verify / transport remain available for digest-addressed registry workflows.

A requires.bundles provider travels as both halves. Its link half is staged under runtime/bundles/<id>/lib, beside the provider-relative path its plugInfo.json already names; ordinary requires.libraries runtime files stay under runtime/libraries/. Its USD registration half is staged under runtime/bundles/<id>/<provider plugInfo root> and declared in the packaged manifest's requires.runtime_plugin_paths, which the session adds to PXR_PLUGINPATH_NAME behind the bundle's own root. Both are required for the package to be independently installable: the link half satisfies the loader, and only the registration half lets USD find a kind: usd-schema provider's plugInfo.json and generatedSchema.usda and apply its schemas. Staging one without the other produces an artifact that records a resolved closure, resolves its own file format, and then fails at Usd.Stage.Open().

requires.runtime_plugin_paths is written by ost plugin package from the resolved workspace graph; authored bundles do not normally set it. The L0 bundle.runtime_plugin_paths diagnostic fails when a declared path is missing, is not a directory, or contains no plugInfo.json — a staged tree that registers nothing is indistinguishable at discovery time from one that was never staged.

Graph result

With --json, the normal workspace result includes data.graph:

{
  "passed": true,
  "nodes": [{"id":"vrmFormat","version":"0.2.0","kind":"usd-fileformat"}],
  "edges": [{"from":"vrmFormat","to":"vrmSchema","version":">=0.2,<0.3","contract":1}],
  "libraries": [{"id":"vrmContainer","version":"0.1.0","package":"vrmContainer","target":"vrmContainer::vrmContainer"}],
  "library_edges": [{"from":"vrmFormat","from_kind":"bundle","to":"vrmContainer","version":">=0.1,<0.2"}],
  "issues": []
}

An invalid graph exits with validation status 5 before bundle reports are written. Issues use stable codes:

Code Meaning
WORKSPACE_BUNDLE_ID_INVALID A discovered plugin identity is not portable.
WORKSPACE_DUPLICATE_BUNDLE_ID More than one discovered bundle has the same identity.
WORKSPACE_DEPENDENCY_ID_INVALID A dependency id is not portable.
WORKSPACE_DUPLICATE_DEPENDENCY A consumer repeats the same dependency id.
WORKSPACE_DEPENDENCY_MISSING No unique discovered bundle provides the required id.
WORKSPACE_DEPENDENCY_VERSION_INVALID A dependency version range cannot be parsed.
WORKSPACE_DEPENDENCY_VERSION_MISMATCH The provider version does not satisfy the range.
WORKSPACE_SCHEMA_CONTRACT_INVALID A schema provider or consumer declares contract 0.
WORKSPACE_SCHEMA_CONTRACT_REQUIRED A consumer did not select the provider's contract.
WORKSPACE_SCHEMA_CONTRACT_MISSING A consumer selects a contract that the schema does not provide.
WORKSPACE_SCHEMA_CONTRACT_MISMATCH Required and provided schema contracts differ.
WORKSPACE_SCHEMA_CONTRACT_NOT_APPLICABLE A contract is attached to a non-schema dependency or bundle.
WORKSPACE_DEPENDENCY_DIRECTION_FORBIDDEN The dependency violates the bundle ownership direction.
WORKSPACE_DEPENDENCY_CYCLE The directed bundle graph contains a cycle.
WORKSPACE_DUPLICATE_LIBRARY_ID More than one descriptor provides the same library id.
WORKSPACE_DUPLICATE_LIBRARY_DEPENDENCY A bundle or library repeats one library edge.
WORKSPACE_LIBRARY_DEPENDENCY_ID_INVALID A library dependency id is not portable.
WORKSPACE_LIBRARY_DEPENDENCY_MISSING No unique descriptor provides the required library.
WORKSPACE_LIBRARY_DEPENDENCY_VERSION_INVALID A library version range cannot be parsed.
WORKSPACE_LIBRARY_DEPENDENCY_VERSION_MISMATCH The provider version does not satisfy the range.
WORKSPACE_LIBRARY_DEPENDENCY_CYCLE The directed plain-library graph contains a cycle.
WORKSPACE_LIBRARY_RUNTIME_MISSING Build/package/test/run needs an installed library runtime directory which is absent.