From 1122f58546fbbd30fcc4600b9dc63c59648082e5 Mon Sep 17 00:00:00 2001 From: Matthew Woehlke Date: Mon, 8 Jun 2026 10:39:43 -0400 Subject: [PATCH] Resolve attributes as cross-references Rewrite the `attribute` role to be a domain specific cross reference that is resolved against the actual collected attributes set. While our previous practice of creating attribute links using standard references was functional, it made it somewhat awkward to elide the object context for overloaded attributes, and resulted in inconsistency between linked and unlinked attribute references in both markup and visual rendering. Generating the references ourself gives improved consistency of visual presentation, as well as simplified and more standard mechanisms for specifying whether a reference to an overloaded attribute should be qualified. Additionally, resolving references against our internal attribute model (as compared to the previous behavior which effectively used only section targets) means that attributes can be qualified, whether or not they are overloaded, which makes resolution more robust. --- _extensions/cps/__init__.py | 71 +++++++++++++++++++++++++++++++++++-- components.rst | 6 ++-- configurations.rst | 5 +-- recommendations.rst | 6 ++-- schema-supplement.rst | 8 +++-- schema.rst | 46 ++++++++++++------------ searching.rst | 9 ++--- 7 files changed, 112 insertions(+), 39 deletions(-) diff --git a/_extensions/cps/__init__.py b/_extensions/cps/__init__.py index 5b96721..378e23f 100644 --- a/_extensions/cps/__init__.py +++ b/_extensions/cps/__init__.py @@ -12,9 +12,10 @@ import jsb from sphinx import addnodes, domains +from sphinx.roles import XRefRole from sphinx.util import logging from sphinx.util.docutils import SphinxRole -from sphinx.util.nodes import clean_astext +from sphinx.util.nodes import clean_astext, make_refnode logger = logging.getLogger(__name__) @@ -300,6 +301,19 @@ def run(self): node = nodes.reference(self.rawtext, self.text, refuri=uri) return [node], [] +# ============================================================================= +class AttributeRefRole(XRefRole): + refdomain = 'cps' + reftype = 'attribute' + classes = ['code', 'attribute'] + + # ------------------------------------------------------------------------- + def run(self) -> tuple[list[Node], list[system_message]]: + if self.disabled: + return self.create_non_xref_node() + else: + return self.create_xref_node() + # ============================================================================= @dataclass class Attribute: @@ -361,13 +375,13 @@ def __init__(self, *args, **kwargs): # Site-specific roles self.roles['schema'] = SchemaRole() + self.roles['attribute'] = AttributeRefRole() # Additional site-specific roles (these just apply styling) self.add_role('hidden') self.add_role('applies-to') self.add_role('separator') self.add_code_role('object') - self.add_code_role('attribute') self.add_code_role('feature') self.add_code_role('feature.opt', styles=['feature', 'optional']) self.add_code_role('feature.var', styles=['feature', 'var']) @@ -428,6 +442,59 @@ def add_role(self, name, styles=None, parent=roles.generic_custom_role): def add_code_role(self, name, styles=None, parent=roles.code_role): self.add_role(name, styles, parent) + # ------------------------------------------------------------------------- + def resolve_xref(self, env, fromdocname, builder, + typ, target, node, contnode): + if typ != 'attribute': + logger.warning('unknown xref type', + location=node, type='ref', subtype=typ) + return None + + short = False + if target.startswith('~'): + short = True + target = target[1:] + + if '.' in target: + context, _, name = target.partition('.') + else: + name = target + context = None + + attr = self.attributes.get(name) + + if attr is None: + logger.warning('attribute %r not found', target, + location=node, type='ref', subtype=typ) + return None + + if context is None: + if len(attr.instances) > 1: + logger.warning('attribute %r is ambiguous', target, + location=node, type='ref', subtype=typ) + return None + + _, docname, refnode = next(iter(attr.context.values())) + qualified = False + + else: + c = attr.context.get(context) + if c is None: + logger.warning('overloaded attribute %r not found', target, + location=node, type='ref', subtype=typ) + return None + + _, docname, refnode = c + qualified = not short and len(attr.instances) > 1 + + label = refnode['names'][0] + + cont = nodes.literal('', name, classes=['attribute']) + if qualified: + ccont = nodes.inline('', f' ({context})', classes=['applies-to']) + cont = [cont, ccont] + + return make_refnode(builder, fromdocname, docname, label, cont) # ============================================================================= def write_schema(app, exception): diff --git a/components.rst b/components.rst index d79283b..f8424a6 100644 --- a/components.rst +++ b/components.rst @@ -24,9 +24,9 @@ by the at-sign (``@``) and a configuration name. The special case of using the at-sign as a configuration name (e.g. ``foo:foo-core@@``) means that the named configuration is the same as the configuration in which the name appears. -(For example, the component ``foo-ui`` has -non-configuration-specific :attribute:`requires` :string:`":foo-core@@"` -and :attribute:`configurations` :string:`"A"` and :string:`"B"`. +(For example, the component ``foo-ui`` has non-configuration-specific +:attribute:`~component.requires` :string:`":foo-core@@"` +and :attribute:`~component.configurations` :string:`"A"` and :string:`"B"`. The :string:`"A"` configuration of ``foo-ui`` therefore requires ``:foo-core@A``, and similar for other configurations.) diff --git a/configurations.rst b/configurations.rst index 1798477..1e6ab74 100644 --- a/configurations.rst +++ b/configurations.rst @@ -33,7 +33,7 @@ It is recommended that build systems select a configuration as follows: the build system may implement a mechanism to prefer a configuration which "matches" the consuming project's active configuration. -- The package's `configurations (package)`_ shall be searched. +- The package's :attribute:`~package.configurations` shall be searched. The first configuration in this list which matches an available configuration of the component shall be used. @@ -106,7 +106,8 @@ The structure of a configuration-specific CPS is the same as a common CPS, with three exceptions: - The only defined :object:`package` keys are - `name`_, `configuration`_, and `components `_. + :attribute:`name`, :attribute:`configuration`, + and :attribute:`~package.components`. The first two are required. Use of other attributes specified in the schema is ill-formed. diff --git a/recommendations.rst b/recommendations.rst index fdc9cdb..b0c27a8 100644 --- a/recommendations.rst +++ b/recommendations.rst @@ -117,7 +117,7 @@ it can be addressed in one of two manners: - If a "full" dependency merely needs to be linked *after* a link-only dependency, the dependency can simply be listed twice; - once in :attribute:`requires`, + once in :attribute:`~component.requires`, and again in :attribute:`link_requires`. (Tools are encouraged to add link-only dependencies after "full" dependencies.) @@ -134,8 +134,8 @@ Transitive Dependencies ''''''''''''''''''''''' When a package is located, -it is intended that the tool would also -locate any `requires (package)`_ mentioned by the package. +it is intended that the tool would also locate +any :attribute:`~package.requires` mentioned by the package. In some cases, however, a user may want to use only some components of a package, which may have a more limited set of dependencies diff --git a/schema-supplement.rst b/schema-supplement.rst index eb8a4fb..5c3fe82 100644 --- a/schema-supplement.rst +++ b/schema-supplement.rst @@ -25,10 +25,12 @@ By definition, none of the following attributes are required. :type: string :context: package - Specifies the `license`_ that is assumed to apply to a component, + Specifies the :attribute:`license` + that is assumed to apply to a component, if none is otherwise specified. This is convenient for packages - that wish their `license`_ to reflect portions of the package + that wish their :attribute:`license` + to reflect portions of the package that are not reflected by a component (such as data files) when most or all of the compiled artifacts use the same license. @@ -65,7 +67,7 @@ By definition, none of the following attributes are required. If parts of a package use different licenses, this attribute may also be specified on a component if doing so helps to clarifying the licensing. - (See also `default_license`_.) + (See also :attribute:`default_license`.) .. ---------------------------------------------------------------------------- .. cps:attribute:: meta_comment diff --git a/schema.rst b/schema.rst index 9cc879c..2b86303 100644 --- a/schema.rst +++ b/schema.rst @@ -90,13 +90,13 @@ Attribute names are case sensitive. with which this version is compatible. This information is used when a consumer requests a specific version. If the version requested is equal to or newer - than the :attribute:`compat_version`, + than the :attribute:`!compat_version`, the package may be used. If not specified, the package is not compatible with previous versions - (i.e. :attribute:`compat_version` - is implicitly equal to :attribute:`version`). + (i.e. :attribute:`!compat_version` + is implicitly equal to :attribute:`~package.version`). .. ---------------------------------------------------------------------------- .. cps:attribute:: compile_features @@ -120,7 +120,7 @@ Attribute names are case sensitive. A map may be used instead to give different values depending on the language of the consuming source file. - Handling of such shall be the same as for `definitions`_. + Handling of such shall be the same as for :attribute:`definitions`. .. ---------------------------------------------------------------------------- .. cps:attribute:: compile_requires @@ -129,7 +129,7 @@ Attribute names are case sensitive. Specifies additional components required by a component which are needed only at the compile stage. - Unlike `requires (component)`_, + Unlike :attribute:`component.requires`, only the required components' compilation-related attributes should be applied transitively; link requirements of the required component(s) should be ignored. @@ -244,7 +244,8 @@ Attribute names are case sensitive. (which will be known by the tool). See also `Prefix Determination`_ for details. - Exactly **one** of ``cps_path`` or `prefix`_ is required. + Exactly **one** of :attribute:`!cps_path` or :attribute:`prefix` + is required. .. ---------------------------------------------------------------------------- .. cps:attribute:: cps_version @@ -260,7 +261,7 @@ Attribute names are case sensitive. CPS version numbering follows |semver|_. That is, tools that support CPS version ``.`` are expected to be able to read files - with :attribute:`cps_version` ``.``, + with :attribute:`!cps_version` ``.``, even for Z > Y (with the understanding that, in such cases, the tool may miss non-critical information that the CPS provided). @@ -314,7 +315,7 @@ Attribute names are case sensitive. Specifies additional components required by a component which are needed only by the dynamic library loader. - Unlike `requires (component)`_ or `link_requires`_, + Unlike :attribute:`component.requires` or :attribute:`link_requires`, these are not used to resolve symbol references of the consumer, but represent "private" implementation requirements of the component on which this attribute appears. @@ -367,7 +368,7 @@ Attribute names are case sensitive. A map may be used instead to give different values depending on the language of the consuming source file. - Handling of such shall be the same as for `definitions`_. + Handling of such shall be the same as for :attribute:`definitions`. .. ---------------------------------------------------------------------------- .. cps:attribute:: isa @@ -448,7 +449,7 @@ Attribute names are case sensitive. :default: ["c"] Specifies the ABI language or languages of a static library - (`type`_ :string:`"archive"`). + (:attribute:`type` :string:`"archive"`). Officially supported (case-insensitive) values are :string:`"c"` (no special handling required) and :string:`"cpp"` (consuming the static library @@ -462,7 +463,7 @@ Attribute names are case sensitive. Specifies a list of additional libraries (as paths, not components) that must be linked against when linking code that consumes the component. (Note that packages should avoid using this attribute if at all possible. - Use `requires (component)`_ instead whenever possible.) + Use :attribute:`component.requires` instead whenever possible.) .. ---------------------------------------------------------------------------- .. cps:attribute:: link_location @@ -476,7 +477,7 @@ Attribute names are case sensitive. on platforms where the library is separated into multiple file components. For example, on Windows, this attribute shall give the location of the ``.lib``, - while `location`_ shall give the location of the ``.dll``. + while :attribute:`location` shall give the location of the ``.dll``. If the path starts with ``@prefix@``, the package's prefix is substituted @@ -494,7 +495,7 @@ Attribute names are case sensitive. Specifies additional components required by a component which are needed only at the link stage. - Unlike `requires (component)`_, + Unlike :attribute:`component.requires`, only the required components' link dependencies should be applied transitively; additional properties such as compile and include attributes @@ -514,7 +515,7 @@ Attribute names are case sensitive. such as a ``.so`` or ``.jar``. (For Windows DLL components, this should be the location of the ``.dll``. - See also `link_location`_.) + See also :attribute:`link_location`.) If the path starts with ``@prefix@``, the package's prefix is substituted @@ -535,8 +536,8 @@ Attribute names are case sensitive. the name of the CPS file without the ``.cps`` suffix must exactly match (including case) - either :attribute:`name` as-is, - or :attribute:`name` converted to lower case. + either :attribute:`!name` as-is, + or :attribute:`!name` converted to lower case. .. ---------------------------------------------------------------------------- .. cps:attribute:: platform @@ -568,7 +569,8 @@ Attribute names are case sensitive. for non-relocatable package. See also `Prefix Determination`_. - Exactly **one** of `cps_path`_ or ``prefix`` is required. + Exactly **one** of :attribute:`cps_path` or :attribute:`!prefix` + is required. .. ---------------------------------------------------------------------------- .. cps:attribute:: requires @@ -580,12 +582,12 @@ Attribute names are case sensitive. This is used, for example, to indicate transitive dependencies. Relative component names are interpreted relative to the current package. Absolute component names must refer to a package required by this package - (see `requires (package)`_). + (see :attribute:`package.requires`). Compile and link attributes should be applied transitively, as if the consuming component also directly consumed the components required by the component being consumed. - See also `link_requires`_. + See also :attribute:`link_requires`. .. ---------------------------------------------------------------------------- .. cps:attribute:: requires @@ -650,7 +652,7 @@ Attribute names are case sensitive. :overload: Specifies the version of the package. - The format of this string is determined by `version_schema`_. + The format of this string is determined by :attribute:`version_schema`. If not provided, the CPS will not satisfy any request for a specific version of the package. @@ -664,7 +666,7 @@ Attribute names are case sensitive. Specifies the required version of a package. If omitted, any version of the required package is acceptable. Semantics are the same - as for the :attribute:`version` attribute of a |package|. + as for the :attribute:`~package.version` attribute of a |package|. .. ---------------------------------------------------------------------------- .. cps:attribute:: version_schema @@ -692,7 +694,7 @@ Attribute names are case sensitive. It does not imply anything about the compatibility or incompatibility of various versions of a package. - See also `compat_version`_. + See also :attribute:`compat_version`. - :string:`simple` diff --git a/searching.rst b/searching.rst index 16d332e..2718ced 100644 --- a/searching.rst +++ b/searching.rst @@ -101,7 +101,7 @@ or to provide the location of a package which is not installed to any of the standard search paths. When a candidate ``.cps`` file is found, -the tool shall inspect the package's `platform`_. +the tool shall inspect the package's :attribute:`platform`. If the package's platform does not match the target platform, the tool should ignore the ``.cps`` and continue the search. This allows for the installation of packages for different platforms @@ -127,13 +127,14 @@ it is necessary to know the package's prefix as :var:`prefix`, above). This is accomplished in one of two ways: -- If a package specifies `prefix`_, that value is used. +- If a package specifies :attribute:`prefix`, + that value is used. -- If a package specifies `cps_path`_, +- If a package specifies :attribute:`cps_path`, the prefix shall be determined from that value in combination with the absolute location of the ``.cps`` file. -A correctly specified `cps_path`_ will match the location +A correctly specified :attribute:`cps_path` will match the location (that is, the path without the final ``.cps`` file name) of the ``.cps`` file. For example, ``/usr/local/lib/cps/foo/foo.cps``