Skip to content

Add a new drop directive for extending packages#35045

Closed
tgamblin wants to merge 1 commit intodevelopfrom
disinherit-directive
Closed

Add a new drop directive for extending packages#35045
tgamblin wants to merge 1 commit intodevelopfrom
disinherit-directive

Conversation

@tgamblin
Copy link
Copy Markdown
Member

@tgamblin tgamblin commented Jan 19, 2023

Closes #33506; related to #33057.

When you inherit from a package in Spack, you inherit all the metadata from its directives, including version, provides, depends_on, conflicts, etc.

In some cases, you may not want this metadata. This PR adds a new drop directive that allows you to throw out things inherited from the base class. For example:

from spack.pkg.builtin.mpich import Mpich

class MyMpich(Mpich):
    drop("versions")    # don't inherit any versions from builtin Mpich
    version("5.0", "08721a102fefcea2ae4add8c9cc548df77e9224f5385ad0872a9150fdd26a415")
    version("6.0", "9cc39dd33dd4227bb82301d285437588d705290846d22ab6b8791c7e631ce385")

Without the drop("versions") directive, this package would have versions 5.0, 6.0, and anything inherited from Mpich. With it, this package has only versions 5.0 and 6.0.

You can drop any of: conflicts, dependencies, extendees, patches, provided, resources, variants, or versions.

  • add new drop directive
  • Two packages were modifying their versions dictionary in their constructors to achieve this, but this causes spack url stats to fail with a concurrent modification exception as it iterates over all packages. Fixed these to use drop instead.
  • Update documentation
  • Add tests

When you inherit from a package in Spack, you inherit all the metadata from its
directives, including `version`, `provides`, `depends_on`, `conflicts`, etc.

In some cases, you may not want this metadata. This PR adds a new `disinherit` directive
that allows you to throw out things inherited from the base class.  For example:

```python
from spack.pkg.builtin.mpich import Mpich

class MyMpich(Mpich):
    disinherit("versions")    # don't inherit any versions from builtin Mpich
    version("5.0", "08721a102fefcea2ae4add8c9cc548df77e9224f5385ad0872a9150fdd26a415")
    version("6.0", "9cc39dd33dd4227bb82301d285437588d705290846d22ab6b8791c7e631ce385")
```

Without the `disinherit("versions")` directive, this package would have versions `5.0`,
`6.0`, *and* anything inherited from `Mpich`. With it, this package has only versions
`5.0` and `6.0`.

You can `disinherit` any of: `conflicts`, `dependencies`, `extendees`, `patches`,
`provided`, `resources`, `variants`, or `versions`.

- [x] add new `disinherit directive`
- [x] Two packages were modifying their `versions` dictionary in their constructors to
      achieve this, but this causes `spack url stats` to fail with a concurrent
      modification exception as it iterates over all packages. Fixed these to use
      `disinherit` instead.
- [x] Update documentation
- [x] Add tests
@spackbot-app spackbot-app bot added core PR affects Spack core functionality directives documentation Improvements or additions to documentation new-version tests General test capability(ies) update-package labels Jan 19, 2023
@spackbot-app spackbot-app bot requested a review from w8jcik January 19, 2023 23:07
@tgamblin
Copy link
Copy Markdown
Member Author

Would like to get some thoughts on this before it gets merged, so don't just merge it after one approval :)

@tgamblin tgamblin marked this pull request as draft January 19, 2023 23:15
@tgamblin tgamblin marked this pull request as ready for review January 20, 2023 04:22
@tgamblin tgamblin assigned tldahlgren and unassigned tldahlgren Jan 20, 2023
@tgamblin tgamblin requested a review from tldahlgren January 20, 2023 04:50
@w8jcik
Copy link
Copy Markdown
Contributor

w8jcik commented Jan 20, 2023

The change looks great. Thank you for the effort that you put into adding it.

Coming to implementation, disinherit could accept predicate what to remove, but it would be also unnecessary complication. Current solution covers my use case and looks simple.

So again, it looks good to me :)

Copy link
Copy Markdown
Member

@alalazo alalazo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this works very well for simple packages.

The current implementation though require users to duplicate code, if they want to get rid of e.g. a single variant. This is fine for small packages, less so if we want to remove a single variant, or conflict, from a package like trilinos.

Another consideration is that methods inherited from base classes might refer to entities that got pruned, and produce some inconsistent behavior if people don't carefully check the recipe.

@tgamblin
Copy link
Copy Markdown
Member Author

tgamblin commented Jan 20, 2023

The current implementation though require users to duplicate code, if they want to get rid of e.g. a single variant.

I was thinking of adding:

disinherit("versions", except=["1.0", "2.0", ...])

and, e.g.:

# keeps all provides("mpi...") (by matching the spec)
disinherit("provided", except="mpi")

and:

# get rid of any conflicts with hdf5 > 10
disinherit("conflicts", except="hdf5@:10")

Of the things you could disinherit:

  • Versions have obvious identifiers (the version) and also have satisfies(), e.g. disinherit("versions", except="10:")
  • Variants and resources have names
  • Conflicts, dependencies, extendees, and provided can be matched with spec semantics
  • Patches I don't really know what to do with -- you could specify the sha256, the URL, or the path but they all seem cumbersome. I would lean towards just disallowing except= for patches unless we really need this.

If that sounds good it wouldn't be hard to throw something together.

@becker33
Copy link
Copy Markdown
Member

This looks substantially like a duplicate of #33506

Patches I don't really know what to do with -- you could specify the sha256, the URL, or the path but they all seem cumbersome. I would lean towards just disallowing except= for patches unless we really need this.

We definitely need to be able to disinherit patches, but I don't off the top of my head have a use-case for except

@tgamblin
Copy link
Copy Markdown
Member Author

tgamblin commented Jan 20, 2023

@becker33: Thanks -- I guess I missed #33506. Sorry @iarspider -- I didn't mean to step on your toes.

Personally I like this better b/c it's a single directive, not a bunch of them, and I think we need to be able to drop all of something and also drop specific things with except=. I kind of like drop() from #33506 instead of disinherit() -- would that alleviate your concern @alalazo?

I would be curious what @iarspider thinks of this -- with the changes mentioned in #35045 (comment) would this work for you?

I think I can bring over the filename/url naming for patches from #33506 and adapt @iarspider's test too.

@iarspider
Copy link
Copy Markdown
Contributor

@tgamblin I would prefer the opposite syntax - disinherit(..., only=...), since I rarely need to drop more than one of something. Or maybe both, while obv not allowing things like disinherit(..., only=..., except=...) :)

@tgamblin
Copy link
Copy Markdown
Member Author

@iarspider: We were thinking we'd go with your drop() over disinherit -- is that ok?

I don't mind doing both drop(..., only="") and drop(..., except="")... I suspect someone will ask for both eventually.

@iarspider
Copy link
Copy Markdown
Contributor

iarspider commented Jan 20, 2023 via email

@tgamblin tgamblin changed the title Add a new disinherit directive for extending packages Add a new drop directive for extending packages Jan 20, 2023
@becker33
Copy link
Copy Markdown
Member

@tgamblin here is the use case for this that we discussed on our call earlier:

On RHEL systems, RHEL provides special [email protected] versions that include substantial work to make them binary compatible with the system gcc.

We want to be able to install e.g. [email protected] from a tarball of binaries to take advantage of this work. This requires being able to drop all dependencies (because they are irrelevant) and patches (they won't apply) for that one particular version (eventually probably a list of versions, to include [email protected] as well).

The ideal interface for this use case would probably be something like:

with when("@10.3.1"):
    drop("patches")
    drop("dependencies")

@scheibelp
Copy link
Copy Markdown
Member

scheibelp commented Mar 15, 2023

The use case that originally drew my interest to this PR does not require when: if there is a package in the builtin repository like:

class Foo(Package):
    depends_on(cmake@:upper.version)
    ...
    # a bunch of useful logic
  • I want to inherit Foo, but my DAG (i.e. some higher dependent) needs a CMake version after upper.version
  • I experiment personally with installing Foo using that later CMake and find that it works

I can submit a PR to loosen the constraints in Foo but:

  • To use that I pull the latest state of builtin (along with possibly other incompatible changes to other packages)
  • While waiting I need to maintain this change as a patch to the Foo package (e.g. a .patch file, or a separate branch of Spack with those changes applied)

So it would be nice to inherit Foo but discard the constraints that aren't necessary:

class Foo(builtin.Foo):
    drop("dependencies")

ideally, we could drop only specific dependencies and keep a subset of them, but for this use case it shouldn't be a major issue to rewrite the depends_on constraints that I want to keep.

@alalazo
Copy link
Copy Markdown
Member

alalazo commented Mar 17, 2023

I have a couple of use cases where drop would be useful. One is build system specific:

In that case I am using conflicts to hide some weird implementation details when we have to drop one of the two generators. A better implementation would be using drop to drop the generator variant, and any metadata dependent on the presence of the value that isn't used.

The other one is a from a user (can't find the PR right now). That user wanted to override for a package the list of supported cuda_arch, but needs to get rid to all the metadata that reference them (which is inherited from the base class).

@danielsjensen1
Copy link
Copy Markdown
Contributor

I think this works very well for simple packages.

The current implementation though require users to duplicate code, if they want to get rid of e.g. a single variant. This is fine for small packages, less so if we want to remove a single variant, or conflict, from a package like trilinos.

Another consideration is that methods inherited from base classes might refer to entities that got pruned, and produce some inconsistent behavior if people don't carefully check the recipe.

I put in an implementation that allows for the removal of individual directives in addition to entire categories at #48947 as that has primarily been my use case. I often need to remove just one conflict when I'm inheriting a package. I didn't see this issue earlier so I was dumb and wrote a completely separate implementation.

@github-actions
Copy link
Copy Markdown
Contributor

This pull request has been automatically marked as stale because it has not had
any activity in the last 6 months. It will be closed if there is no further activity.
Thank you for your contributions!

@github-actions github-actions bot added the stale label Aug 10, 2025
@github-actions
Copy link
Copy Markdown
Contributor

This pull request was closed because it had no activity for 30 days after being marked stale. If you feel this is in error, please feel free to reopen this pull request.

@github-actions github-actions bot closed this Sep 10, 2025
@becker33 becker33 reopened this Sep 17, 2025
Copy link
Copy Markdown
Member

@becker33 becker33 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two levels of additions to this that I think are necessary, but I don't know that they need to be part of this PR.

  1. The use case to tighten a when constraint:

If we have a builtin package foo with some dependencies

class Foo(Package):
    depends_on("bar", when="+extra_capabilities")
    ... # 5000 lines of additional dependencies

and a user discovers that bar is not actually necessary for +extra_capabilities after some version cut-off X.Y.Z. Then the user will want to drop the specific dependency on "bar" without having to recreate the rest of the dependencies, so that they can recreate the constrained bar dependencies.

from spack_repo.builtin.packages.foo.package import Foo as BuiltinFoo
class Foo(BuiltinFoo):
    drop("dependencies", "bar")
    depends_on("bar", when="@:X.Y +extra_capabilities")
  1. The further case that we've discussed more, regarding when clauses on the drop directive

I'm fully convinced that (2) should wait for a follow-on PR (if we determine it's necessary. But (1) is much more essential to the use case in my mind. It seems we would want to be able to drop:

  1. A particular version by its version string
  2. A conflict by its first parameter (arbitrarily, this one is less clear how it maps to user intuition than the others
  3. A dependency by either its name or by a matching spec (not sure which is better)
  4. An extension by the same as a dependency
  5. A provider by the same as a dependency (more likely name in this case)
  6. A patch by its name/url
  7. A variant by its name
  8. A resource by its name/url
  9. (no need for dropping a specific build_system, since it's implemented as a variant)

All of these seem reasonably easy to match on if we can settle on a clear interface and documentation of what the additional argument does in terms of constraining the drop.

The obvious next step of conditions/exceptions to the constraint is clearly a future work issue.


.. code-block:: python

from spack.pkg.builtin.mpich import Mpich
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
from spack.pkg.builtin.mpich import Mpich
from spack_repo.builtin.packages.mpich.package import Mpich


.. code-block:: python

from spack.pkg.myrepo.my_package import MyPackage
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
from spack.pkg.myrepo.my_package import MyPackage
from spack_repo.myrepo.packages.my_package.package import MyPackage

version("1.0", "0209444070d9c8af9b62c94095a217e3bc6843692d1e3fdc1ff5371e03aac47c")
version("2.0", "5dda192154047d6296ba14a4ab2d869c6926fd7f44dce8ce94f63aae2e359c5b")

Every repository registered with Spack ends up in a submodule of ``spack.pkg`` with a
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Every repository registered with Spack ends up in a submodule of ``spack.pkg`` with a
Every repository registered with Spack ends up in a submodule of ``spack_repo`` with a

Comment on lines +3637 to +3669
^^^^^^^^^^^^^^^^^^^^^^^^
``disinherit``
^^^^^^^^^^^^^^^^^^^^^^^^

When you inherit from a package in Spack, you inherit all the metadata from its
directives, including ``version``, ``provides``, ``depends_on``, ``conflicts``, etc. For
example, ``NewPackage`` above will have four versions: ``1.0`` and ``2.0`` inherited
from ``MyPackage``, as well as, ``3.0``, and ``4.0`` defined in ``NewPackage``.

If you do not want your package to define all the same things as its base class, you can
use the ``disinherit`` directive to start fresh in your subclass:

.. code-block:: python

from spack.pkg.myrepo.my_package import MyPackage

class NewerPackage(MyPackage):
disinherit("versions") # don't inherit any versions from MyPackage
version("5.0", "08721a102fefcea2ae4add8c9cc548df77e9224f5385ad0872a9150fdd26a415")
version("6.0", "9cc39dd33dd4227bb82301d285437588d705290846d22ab6b8791c7e631ce385")

Now, ``NewerPackage`` will have **only** versions ``5.0`` and ``6.0``, and will not
inherit ``1.0`` or ``2.0`` from ``MyPackage``. You can ``disinherit`` many different
properties from base packages. The full list of options is:

* ``conflicts``
* ``dependencies``
* ``extendees``
* ``patches``
* ``provided``
* ``resources``
* ``variants``
* ``versions``
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. This is out of date in the directive name.
  2. This talks about the composition of the versions as if no versions come from mpich.
  3. We should discuss an example where you only want to remove one version -- in that case, you have to remove them all and reimplement the ones you want to remove



@directive()
def disinherit(dict_name: str):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We agreed to change this to drop

assert "4.0" not in pkg.versions


def test_disinherit(config, mock_packages):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def test_disinherit(config, mock_packages):
def test_disinherit_invalid(config, mock_packages):

# SPDX-License-Identifier: (Apache-2.0 OR MIT)

from spack.package import *
from spack.pkg.builtin.mock.a import A
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
from spack.pkg.builtin.mock.a import A
from spack_repo.builtin_mock.packages.a.package import A

@github-actions
Copy link
Copy Markdown
Contributor

This pull request has been automatically marked as stale because it has not had any activity in the last 6 months. It will be closed in 30 days if there is no further activity.

If the pull request is waiting for a reply from reviewers, feel free to ping them as a reminder. If it is waiting and has no assigned reviewer, feel free to ping @spack/spack-releasers or simply leave a comment saying this should not be marked stale. This will reset the pull request's stale state.

To get more eyes on your pull request, you can post a link in the #pull-requests channel of the Spack Slack.
Thank you for your contributions!

@github-actions github-actions bot added the stale label Mar 17, 2026
@tgamblin
Copy link
Copy Markdown
Member Author

This is likely to be superseded by #48947 for v1.2. Closing in anticipation of that; we can reopen if it becomes interesting again.

@tgamblin tgamblin closed this Mar 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core PR affects Spack core functionality directives documentation Improvements or additions to documentation new-version stale tests General test capability(ies) update-package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants