Document how to handle changing build systems (#25174)
This commit is contained in:
		| @@ -63,6 +63,7 @@ on these ideas for each distinct build system that Spack supports: | |||||||
|    build_systems/intelpackage |    build_systems/intelpackage | ||||||
|    build_systems/rocmpackage |    build_systems/rocmpackage | ||||||
|    build_systems/custompackage |    build_systems/custompackage | ||||||
|  |    build_systems/multiplepackage | ||||||
|  |  | ||||||
| For reference, the :py:mod:`Build System API docs <spack.build_systems>` | For reference, the :py:mod:`Build System API docs <spack.build_systems>` | ||||||
| provide a list of build systems and methods/attributes that can be | provide a list of build systems and methods/attributes that can be | ||||||
|   | |||||||
							
								
								
									
										350
									
								
								lib/spack/docs/build_systems/multiplepackage.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										350
									
								
								lib/spack/docs/build_systems/multiplepackage.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,350 @@ | |||||||
|  | .. Copyright 2013-2021 Lawrence Livermore National Security, LLC and other | ||||||
|  |    Spack Project Developers. See the top-level COPYRIGHT file for details. | ||||||
|  |  | ||||||
|  |    SPDX-License-Identifier: (Apache-2.0 OR MIT) | ||||||
|  |  | ||||||
|  | .. _multiplepackage: | ||||||
|  |  | ||||||
|  | ---------------------- | ||||||
|  | Multiple Build Systems | ||||||
|  | ---------------------- | ||||||
|  |  | ||||||
|  | Quite frequently, a package will change build systems from one version to the | ||||||
|  | next. For example, a small project that once used a single Makefile to build | ||||||
|  | may now require Autotools to handle the increased number of files that need to | ||||||
|  | be compiled. Or, a package that once used Autotools may switch to CMake for | ||||||
|  | Windows support. In this case, it becomes a bit more challenging to write a | ||||||
|  | single build recipe for this package in Spack. | ||||||
|  |  | ||||||
|  | There are several ways that this can be handled in Spack: | ||||||
|  |  | ||||||
|  | #. Subclass the new build system, and override phases as needed (preferred) | ||||||
|  | #. Subclass ``Package`` and implement ``install`` as needed | ||||||
|  | #. Create separate ``*-cmake``, ``*-autotools``, etc. packages for each build system | ||||||
|  | #. Rename the old package to ``*-legacy`` and create a new package | ||||||
|  | #. Move the old package to a ``legacy`` repository and create a new package | ||||||
|  | #. Drop older versions that only support the older build system | ||||||
|  |  | ||||||
|  | Of these options, 1 is preferred, and will be demonstrated in this | ||||||
|  | documentation. Options 3-5 have issues with concretization, so shouldn't be | ||||||
|  | used. Options 4-5 also don't support more than two build systems. Option 6 only | ||||||
|  | works if the old versions are no longer needed. Option 1 is preferred over 2 | ||||||
|  | because it makes it easier to drop the old build system entirely. | ||||||
|  |  | ||||||
|  | The exact syntax of the package depends on which build systems you need to | ||||||
|  | support. Below are a couple of common examples. | ||||||
|  |  | ||||||
|  | ^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  | Makefile -> Autotools | ||||||
|  | ^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  |  | ||||||
|  | Let's say we have the following package: | ||||||
|  |  | ||||||
|  | .. code-block:: python | ||||||
|  |  | ||||||
|  |    class Foo(MakefilePackage): | ||||||
|  |        version("1.2.0", sha256="...") | ||||||
|  |  | ||||||
|  |        def edit(self, spec, prefix): | ||||||
|  |            filter_file("CC=", "CC=" + spack_cc, "Makefile") | ||||||
|  |  | ||||||
|  |        def install(self, spec, prefix): | ||||||
|  |            install_tree(".", prefix) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | The package subclasses from :ref:`makefilepackage`, which has three phases: | ||||||
|  |  | ||||||
|  | #. ``edit`` (does nothing by default) | ||||||
|  | #. ``build`` (runs ``make`` by default) | ||||||
|  | #. ``install`` (runs ``make install`` by default) | ||||||
|  |  | ||||||
|  | In this case, the ``install`` phase needed to be overridden because the | ||||||
|  | Makefile did not have an install target. We also modify the Makefile to use | ||||||
|  | Spack's compiler wrappers. The default ``build`` phase is not changed. | ||||||
|  |  | ||||||
|  | Starting with version 1.3.0, we want to use Autotools to build instead. | ||||||
|  | :ref:`autotoolspackage` has four phases: | ||||||
|  |  | ||||||
|  | #. ``autoreconf`` (does not if a configure script already exists) | ||||||
|  | #. ``configure`` (runs ``./configure --prefix=...`` by default) | ||||||
|  | #. ``build`` (runs ``make`` by default) | ||||||
|  | #. ``install`` (runs ``make install`` by default) | ||||||
|  |  | ||||||
|  | If the only version we need to support is 1.3.0, the package would look as | ||||||
|  | simple as: | ||||||
|  |  | ||||||
|  | .. code-block:: python | ||||||
|  |  | ||||||
|  |    class Foo(AutotoolsPackage): | ||||||
|  |        version("1.3.0", sha256="...") | ||||||
|  |  | ||||||
|  |        def configure_args(self): | ||||||
|  |            return ["--enable-shared"] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | In this case, we use the default methods for each phase and only override | ||||||
|  | ``configure_args`` to specify additional flags to pass to ``./configure``. | ||||||
|  |  | ||||||
|  | If we wanted to write a single package that supports both versions 1.2.0 and | ||||||
|  | 1.3.0, it would look something like: | ||||||
|  |  | ||||||
|  | .. code-block:: python | ||||||
|  |  | ||||||
|  |    class Foo(AutotoolsPackage): | ||||||
|  |        version("1.3.0", sha256="...") | ||||||
|  |        version("1.2.0", sha256="...", deprecated=True) | ||||||
|  |  | ||||||
|  |        def configure_args(self): | ||||||
|  |            return ["--enable-shared"] | ||||||
|  |  | ||||||
|  |        # Remove the following once version 1.2.0 is dropped | ||||||
|  |        @when("@:1.2") | ||||||
|  |        def patch(self): | ||||||
|  |            filter_file("CC=", "CC=" + spack_cc, "Makefile") | ||||||
|  |  | ||||||
|  |        @when("@:1.2") | ||||||
|  |        def autoreconf(self, spec, prefix): | ||||||
|  |            pass | ||||||
|  |  | ||||||
|  |        @when("@:1.2") | ||||||
|  |        def configure(self, spec, prefix): | ||||||
|  |            pass | ||||||
|  |  | ||||||
|  |        @when("@:1.2") | ||||||
|  |        def install(self, spec, prefix): | ||||||
|  |            install_tree(".", prefix) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | There are a few interesting things to note here: | ||||||
|  |  | ||||||
|  | * We added ``deprecated=True`` to version 1.2.0. This signifies that version | ||||||
|  |   1.2.0 is deprecated and shouldn't be used. However, if a user still relies | ||||||
|  |   on version 1.2.0, it's still there and builds just fine. | ||||||
|  | * We moved the contents of the ``edit`` phase to the ``patch`` function. Since | ||||||
|  |   ``AutotoolsPackage`` doesn't have an ``edit`` phase, the only way for this | ||||||
|  |   step to be executed is to move it to the ``patch`` function, which always | ||||||
|  |   gets run. | ||||||
|  | * The ``autoreconf`` and ``configure`` phases become no-ops. Since the old | ||||||
|  |   Makefile-based build system doesn't use these, we ignore these phases when | ||||||
|  |   building ``foo@1.2.0``. | ||||||
|  | * The ``@when`` decorator is used to override these phases only for older | ||||||
|  |   versions. The default methods are used for ``foo@1.3:``. | ||||||
|  |  | ||||||
|  | Once a new Spack release comes out, version 1.2.0 and everything below the | ||||||
|  | comment can be safely deleted. The result is the same as if we had written a | ||||||
|  | package for version 1.3.0 from scratch. | ||||||
|  |  | ||||||
|  | ^^^^^^^^^^^^^^^^^^ | ||||||
|  | Autotools -> CMake | ||||||
|  | ^^^^^^^^^^^^^^^^^^ | ||||||
|  |  | ||||||
|  | Let's say we have the following package: | ||||||
|  |  | ||||||
|  | .. code-block:: python | ||||||
|  |  | ||||||
|  |    class Bar(AutotoolsPackage): | ||||||
|  |        version("1.2.0", sha256="...") | ||||||
|  |  | ||||||
|  |        def configure_args(self): | ||||||
|  |            return ["--enable-shared"] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | The package subclasses from :ref:`autotoolspackage`, which has four phases: | ||||||
|  |  | ||||||
|  | #. ``autoreconf`` (does not if a configure script already exists) | ||||||
|  | #. ``configure`` (runs ``./configure --prefix=...`` by default) | ||||||
|  | #. ``build`` (runs ``make`` by default) | ||||||
|  | #. ``install`` (runs ``make install`` by default) | ||||||
|  |  | ||||||
|  | In this case, we use the default methods for each phase and only override | ||||||
|  | ``configure_args`` to specify additional flags to pass to ``./configure``. | ||||||
|  |  | ||||||
|  | Starting with version 1.3.0, we want to use CMake to build instead. | ||||||
|  | :ref:`cmakepackage` has three phases: | ||||||
|  |  | ||||||
|  | #. ``cmake`` (runs ``cmake ...`` by default) | ||||||
|  | #. ``build`` (runs ``make`` by default) | ||||||
|  | #. ``install`` (runs ``make install`` by default) | ||||||
|  |  | ||||||
|  | If the only version we need to support is 1.3.0, the package would look as | ||||||
|  | simple as: | ||||||
|  |  | ||||||
|  | .. code-block:: python | ||||||
|  |  | ||||||
|  |    class Bar(CMakePackage): | ||||||
|  |        version("1.3.0", sha256="...") | ||||||
|  |  | ||||||
|  |        def cmake_args(self): | ||||||
|  |            return [self.define("BUILD_SHARED_LIBS", True)] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | In this case, we use the default methods for each phase and only override | ||||||
|  | ``cmake_args`` to specify additional flags to pass to ``cmake``. | ||||||
|  |  | ||||||
|  | If we wanted to write a single package that supports both versions 1.2.0 and | ||||||
|  | 1.3.0, it would look something like: | ||||||
|  |  | ||||||
|  | .. code-block:: python | ||||||
|  |  | ||||||
|  |    class Bar(CMakePackage): | ||||||
|  |        version("1.3.0", sha256="...") | ||||||
|  |        version("1.2.0", sha256="...", deprecated=True) | ||||||
|  |  | ||||||
|  |        def cmake_args(self): | ||||||
|  |            return [self.define("BUILD_SHARED_LIBS", True)] | ||||||
|  |  | ||||||
|  |        # Remove the following once version 1.2.0 is dropped | ||||||
|  |        def configure_args(self): | ||||||
|  |            return ["--enable-shared"] | ||||||
|  |  | ||||||
|  |        @when("@:1.2") | ||||||
|  |        def cmake(self, spec, prefix): | ||||||
|  |            configure("--prefix=" + prefix, *self.configure_args()) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | There are a few interesting things to note here: | ||||||
|  |  | ||||||
|  | * We added ``deprecated=True`` to version 1.2.0. This signifies that version | ||||||
|  |   1.2.0 is deprecated and shouldn't be used. However, if a user still relies | ||||||
|  |   on version 1.2.0, it's still there and builds just fine. | ||||||
|  | * Since CMake and Autotools are so similar, we only need to override the | ||||||
|  |   ``cmake`` phase, we can use the default ``build`` and ``install`` phases. | ||||||
|  | * We override ``cmake`` to run ``./configure`` for older versions. | ||||||
|  |   ``configure_args`` remains the same. | ||||||
|  | * The ``@when`` decorator is used to override these phases only for older | ||||||
|  |   versions. The default methods are used for ``bar@1.3:``. | ||||||
|  |  | ||||||
|  | Once a new Spack release comes out, version 1.2.0 and everything below the | ||||||
|  | comment can be safely deleted. The result is the same as if we had written a | ||||||
|  | package for version 1.3.0 from scratch. | ||||||
|  |  | ||||||
|  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  | Multiple build systems for the same version | ||||||
|  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  |  | ||||||
|  | During the transition from one build system to another, developers often | ||||||
|  | support multiple build systems at the same time. Spack can only use a single | ||||||
|  | build system for a single version. To decide which build system to use for a | ||||||
|  | particular version, take the following things into account: | ||||||
|  |  | ||||||
|  | 1. If the developers explicitly state that one build system is preferred over | ||||||
|  |    another, use that one. | ||||||
|  | 2. If one build system is considered "experimental" while another is considered | ||||||
|  |    "stable", use the stable build system. | ||||||
|  | 3. Otherwise, use the newer build system. | ||||||
|  |  | ||||||
|  | The developer preference for which build system to use can change over time as | ||||||
|  | a newer build system becomes stable/recommended. | ||||||
|  |  | ||||||
|  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  | Dropping support for old build systems | ||||||
|  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  |  | ||||||
|  | When older versions of a package don't support a newer build system, it can be | ||||||
|  | tempting to simply delete them from a package. This significantly reduces | ||||||
|  | package complexity and makes the build recipe much easier to maintain. However, | ||||||
|  | other packages or Spack users may rely on these older versions. The recommended | ||||||
|  | approach is to first support both build systems (as demonstrated above), | ||||||
|  | :ref:`deprecate <deprecate>` versions that rely on the old build system, and | ||||||
|  | remove those versions and any phases that needed to be overridden in the next | ||||||
|  | Spack release. | ||||||
|  |  | ||||||
|  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  | Three or more build systems | ||||||
|  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  |  | ||||||
|  | In rare cases, a package may change build systems multiple times. For example, | ||||||
|  | a package may start with Makefiles, then switch to Autotools, then switch to | ||||||
|  | CMake. The same logic used above can be extended to any number of build systems. | ||||||
|  | For example: | ||||||
|  |  | ||||||
|  | .. code-block:: python | ||||||
|  |  | ||||||
|  |    class Baz(CMakePackage): | ||||||
|  |        version("1.4.0", sha256="...")  # CMake | ||||||
|  |        version("1.3.0", sha256="...")  # Autotools | ||||||
|  |        version("1.2.0", sha256="...")  # Makefile | ||||||
|  |  | ||||||
|  |        def cmake_args(self): | ||||||
|  |            return [self.define("BUILD_SHARED_LIBS", True)] | ||||||
|  |  | ||||||
|  |        # Remove the following once version 1.3.0 is dropped | ||||||
|  |        def configure_args(self): | ||||||
|  |            return ["--enable-shared"] | ||||||
|  |  | ||||||
|  |        @when("@1.3") | ||||||
|  |        def cmake(self, spec, prefix): | ||||||
|  |            configure("--prefix=" + prefix, *self.configure_args()) | ||||||
|  |  | ||||||
|  |        # Remove the following once version 1.2.0 is dropped | ||||||
|  |        @when("@:1.2") | ||||||
|  |        def patch(self): | ||||||
|  |            filter_file("CC=", "CC=" + spack_cc, "Makefile") | ||||||
|  |  | ||||||
|  |        @when("@:1.2") | ||||||
|  |        def cmake(self, spec, prefix): | ||||||
|  |            pass | ||||||
|  |  | ||||||
|  |        @when("@:1.2") | ||||||
|  |        def install(self, spec, prefix): | ||||||
|  |            install_tree(".", prefix) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ^^^^^^^^^^^^^^^^^^^ | ||||||
|  | Additional examples | ||||||
|  | ^^^^^^^^^^^^^^^^^^^ | ||||||
|  |  | ||||||
|  | When writing new packages, it often helps to see examples of existing packages. | ||||||
|  | Here is an incomplete list of existing Spack packages that have changed build | ||||||
|  | systems before: | ||||||
|  |  | ||||||
|  | ================  =====================  ================ | ||||||
|  | Package           Previous Build System  New Build System | ||||||
|  | ================  =====================  ================ | ||||||
|  | amber             custom                 CMake | ||||||
|  | arpack-ng         Autotools              CMake | ||||||
|  | atk               Autotools              Meson | ||||||
|  | blast             None                   Autotools | ||||||
|  | dyninst           Autotools              CMake | ||||||
|  | evtgen            Autotools              CMake | ||||||
|  | fish              Autotools              CMake | ||||||
|  | gdk-pixbuf        Autotools              Meson | ||||||
|  | glib              Autotools              Meson | ||||||
|  | glog              Autotools              CMake | ||||||
|  | gmt               Autotools              CMake | ||||||
|  | gtkplus           Autotools              Meson | ||||||
|  | hpl               Makefile               Autotools | ||||||
|  | interproscan      Perl                   Maven | ||||||
|  | jasper            Autotools              CMake | ||||||
|  | kahip             SCons                  CMake | ||||||
|  | kokkos            Makefile               CMake | ||||||
|  | kokkos-kernels    Makefile               CMake | ||||||
|  | leveldb           Makefile               CMake | ||||||
|  | libdrm            Autotools              Meson | ||||||
|  | libjpeg-turbo     Autotools              CMake | ||||||
|  | mesa              Autotools              Meson | ||||||
|  | metis             None                   CMake | ||||||
|  | mpifileutils      Autotools              CMake | ||||||
|  | muparser          Autotools              CMake | ||||||
|  | mxnet             Makefile               CMake | ||||||
|  | nest              Autotools              CMake | ||||||
|  | neuron            Autotools              CMake | ||||||
|  | nsimd             CMake                  nsconfig | ||||||
|  | opennurbs         Makefile               CMake | ||||||
|  | optional-lite     None                   CMake | ||||||
|  | plasma            Makefile               CMake | ||||||
|  | preseq            Makefile               Autotools | ||||||
|  | protobuf          Autotools              CMake | ||||||
|  | py-pygobject      Autotools              Python | ||||||
|  | singularity       Autotools              Makefile | ||||||
|  | span-lite         None                   CMake | ||||||
|  | ssht              Makefile               CMake | ||||||
|  | string-view-lite  None                   CMake | ||||||
|  | superlu           Makefile               CMake | ||||||
|  | superlu-dist      Makefile               CMake | ||||||
|  | uncrustify        Autotools              CMake | ||||||
|  | ================  =====================  ================ | ||||||
|  |  | ||||||
|  | Packages that support multiple build systems can be a bit confusing to write. | ||||||
|  | Don't hesitate to open an issue or draft pull request and ask for advice from | ||||||
|  | other Spack developers! | ||||||
| @@ -612,6 +612,7 @@ it executable, then runs it with some arguments. | |||||||
|        installer = Executable(self.stage.archive_file) |        installer = Executable(self.stage.archive_file) | ||||||
|        installer('--prefix=%s' % prefix, 'arg1', 'arg2', 'etc.') |        installer('--prefix=%s' % prefix, 'arg1', 'arg2', 'etc.') | ||||||
|  |  | ||||||
|  | .. _deprecate: | ||||||
|  |  | ||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
| Deprecating old versions | Deprecating old versions | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Adam J. Stewart
					Adam J. Stewart