Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Notes on supporting multiple build-systems using a when decorator #4

Closed
alalazo opened this issue Sep 10, 2021 · 1 comment
Closed

Comments

@alalazo
Copy link
Member

alalazo commented Sep 10, 2021

Below are a few notes for an alternative approach to #3 based on a class decorator. I'm posting it as an issue since there are certain semantics that need to be clarified before we can write a consistent proposal.


Support multiple build-systems using a when decorator

The problem

There are multiple packages that are either changing their build-system during the evolution of the project, or using different build-systems for different platforms. Spack, at the moment, provides no support to model this use case. As a result we wrote some documentation to enumerate various workarounds that people adopted - each with its own drawbacks. What we would like to have in the long term is proper support for packages that can be built using multiple build-systems.

Proposed changes

The proposal made in this SEP is to have classes that can be decorated with a when decorator similarly to what is done for methods. The basic idea is the following:

class Hdf5Base(PackageBase):
	# Common directives, attributes and methods
	depends_on('zlib')


@when('@:X.Z')
class Hdf5(AutotoolsPackage, Hdf5Base):
	depends_on('automake', type='build')

	def install(self, spec, prefix):
		pass
	
	
@when('@X.Y:')
class Hdf5(CMakePackage, Hdf5Base):
	depends_on('cmake@3.16:', type='build')
	
    def install(self, spec, prefix):
		pass

The same name Hdf5 is used multiple times under a decorator. The idea would be to use the class definition that "satisfies" the constraint in the when= decorator once we have a concrete spec associated with the package.

The when decorator at a class level has three different responsibilities:

  1. It needs to constrain every when= parameter in every directive with its argument.
  2. It needs to treat methods as if they have a when decorator on them
  3. It needs to defer inheritance of the base classes to after concretization

Point 1 means that with respect to just metadata, the snippet above should be equivalent to the following:

class Hdf5(Package):
	# From the base class
	depends_on('zlib')
	# From the first decorated class
	depends_on('automake', type='build', when='@:X.Z')
	# From the second decorated class
	depends_on('cmake@3.16:', type='build', when='@X.Y:')

Point 2. means that methods instead should behave like:

class Hdf5(Package):
	@when('@:X.Z')
	def install(self, spec, prefix):
		# From the first decorated class
		pass
	
	@when('@X.Y:')
	def install(self, spec, prefix):
		# From the second decorated class
		pass

Finally point 3. is the more subtle and means that the class is in a sort of "undetermined" state until it is associated with a concrete spec, in which case it behaves like:

# If spec.satisfies('@:X.Z')
class Hdf5(AutotoolsPackage):
	pass

or like:

# If spec.satisfies('@X.Y:')
class Hdf5(CMakePackage):
	pass

Possible issues with the current semantics

Even though the objective here is to deal with multiple build-systems, using a class decorator is a very general mechanism that needs to have clear semantics
under all of its possible applications. Below we'll point out issues with the current idea or cases that need to be defined better.

Structure and behavior of an "undetermined" class

In the simple hdf5 example at the top it is not clear what would be the behavior for operations like:

isinstance(pkg, CMakePackage)

before a concrete spec is associated with the package. There are similar questions that can be asked with respect to the id() of the class or to the presence or absence of class attributes.

Inheritance from a when decorated class

Some packages are fork of other packages and reuse most of their implementation. These packages usually read like:

from spack.pkgkit.builtin import Hdf5

class Hdf5Fork(Hdf5):
	pass

It is not clear what would be the meaning of something like:

from spack.pkgkit.builtin import Hdf5

class Hdf5ForkBase(Hdf5):
	pass

@when('+foo')
class Hdf5Fork(Hdf5ForkBase):
	pass

@when('~foo')
class Hdf5Fork(Hdf5ForkBase):
	pass

if Hdf5 itself uses a when decorator. The problem is that we may end up with a base class that is not yet fully defined, and have on top of that another class definition that is deferred to after concretization. This makes it difficult to deal with MRO and other Python mechanism related to inheritance.

This issue may also have different variations:

from spack.pkgkit.builtin import Hdf5, Hdf5Base

class Hdf5Fork(Hdf5):
	pass

# Does this extend Hdf5 with another conditional?
@when('platform=windows')
class Hdf5Fork(Hdf5Base):
	pass

Would the above be a valid extension for an Hdf5Fork that can be used on Windows?

Multiple constraints are met

With a when decorator applied to a class we may have definitions like:

class Base(PackagesBase):
	pass

@when('+foo')
class Hdf5(Base):
	depends_on('foo')
	
	def install(self, spec, prefix)
		pass

@when('platform=darwin')
class Hdf5(Base):
	depends_on('bar')
	
	def install(self, spec, prefix)
		pass

Given the semantics outlined in the previous paragraph this means that the class above is equivalent to:

class Hdf5(Base)
	depends_on('foo', when='+foo')
	depends_on('bar', when='platform=darwin')
	
	@when('+foo')
	def install(self, spec, prefix)
		pass

	@when('platform=darwin')
	def install(self, spec, prefix)
		pass

This in turn means that if both constraints are met, the directives will be accounted for from both classes, while the install method will be from the first class only.

This behavior can be confusing to users and is complicated further if we add different base classes to the example above. Another case where the semantic is not clear is with class attributes:

class Base(PackagesBase):
	base = 0

@when('+foo')
class Hdf5(Base):
	foo = 1
	bar = 1
	
@when('platform=darwin')
class Hdf5(Base):
	foo = 2
	baz = 2

What should be the behavior of:

getattr(Hdf5, 'foo') == ?
getattr(Hdf5, 'bar') == ?
getattr(Hdf5, 'baz') == ?

in the case the final spec satisfies +foo platform=darwin? If the when decorator behaves like it does for methods, then:

getattr(Hdf5, 'foo') == 1
getattr(Hdf5, 'bar') == 1
getattr(Hdf5, 'baz') is None

and it would become unclear why eventual dependencies stemming from the second class are accounted for in the final spec. If we merge based on order we might have:

getattr(Hdf5, 'foo') == 1
getattr(Hdf5, 'bar') == 1
getattr(Hdf5, 'baz') == 2

in which case we may have an "unexpected" value for Hdf5.foo.

Selection of the build system

Since the approach is based on inheritance without changing other attributes, it's not possible to specify the build system directly. A user can try to influence the selection implicitly by specifying a build-dependency:

hdf5 ^cmake

but the flat representation of specs + clingo do not grant that cmake will be a direct dependency of hdf5 and may result in DAG which are different from what is expected (e.g. hdf5 built with autotools depending on zlib built with cmake).

@alalazo
Copy link
Member Author

alalazo commented Jan 1, 2023

Closing this issue, since we went with the variant based approach (already implemented in v0.19)

@alalazo alalazo closed this as not planned Won't fix, can't repro, duplicate, stale Jan 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant