diff options
-rw-r--r-- | lib/spack/docs/packaging_guide.rst | 27 | ||||
-rw-r--r-- | lib/spack/spack/cmd/create.py | 3 | ||||
-rw-r--r-- | lib/spack/spack/cmd/info.py | 21 | ||||
-rw-r--r-- | lib/spack/spack/directives.py | 43 | ||||
-rw-r--r-- | lib/spack/spack/test/cmd/create.py | 1 | ||||
-rw-r--r-- | lib/spack/spack/test/cmd/info.py | 1 | ||||
-rw-r--r-- | lib/spack/spack/test/directives.py | 38 | ||||
-rw-r--r-- | var/spack/repos/builtin.mock/packages/licenses-1/package.py | 18 | ||||
-rw-r--r-- | var/spack/repos/builtin/packages/zlib/package.py | 2 |
9 files changed, 154 insertions, 0 deletions
diff --git a/lib/spack/docs/packaging_guide.rst b/lib/spack/docs/packaging_guide.rst index acc79ea342..ae6be5b4a6 100644 --- a/lib/spack/docs/packaging_guide.rst +++ b/lib/spack/docs/packaging_guide.rst @@ -6799,3 +6799,30 @@ To achieve backward compatibility with the single-class format Spack creates in Overall the role of the adapter is to route access to attributes of methods first through the ``*Package`` hierarchy, and then back to the base class builder. This is schematically shown in the diagram above, where the adapter role is to "emulate" a method resolution order like the one represented by the red arrows. + +------------------------------ +Specifying License Information +------------------------------ + +A significant portion of software that Spack packages is open source. Most open +source software is released under one or more common open source licenses. +Specifying the specific license that a package is released under in a project's +`package.py` is good practice. To specify a license, find the SPDX identifier for +a project and then add it using the license directive: + +.. code-block:: python + + license("<SPDX Identifier HERE>") + +Note that specifying a license without a when clause makes it apply to all +versions and variants of the package, which might not actually be the case. +For example, a project might have switched licenses at some point or have +certain build configurations that include files that are licensed differently. +To account for this, you can specify when licenses should be applied. For +example, to specify that a specific license identifier should only apply +to versionup to and including 1.5, you could write the following directive: + +.. code-block:: python + + license("...", when="@:1.5") + diff --git a/lib/spack/spack/cmd/create.py b/lib/spack/spack/cmd/create.py index 474e271d17..32c6ed13e1 100644 --- a/lib/spack/spack/cmd/create.py +++ b/lib/spack/spack/cmd/create.py @@ -63,6 +63,9 @@ class {class_name}({base_class_name}): # notify when the package is updated. # maintainers("github_user1", "github_user2") + # FIXME: Add the SPDX identifier of the project's license below. + license("UNKNOWN") + {versions} {dependencies} diff --git a/lib/spack/spack/cmd/info.py b/lib/spack/spack/cmd/info.py index f0850d5dcf..5e667f4876 100644 --- a/lib/spack/spack/cmd/info.py +++ b/lib/spack/spack/cmd/info.py @@ -72,6 +72,10 @@ def variant(s): return spack.spec.ENABLED_VARIANT_COLOR + s + plain_format +def license(s): + return spack.spec.VERSION_COLOR + s + plain_format + + class VariantFormatter: def __init__(self, variants): self.variants = variants @@ -348,6 +352,22 @@ def print_virtuals(pkg): color.cprint(" None") +def print_licenses(pkg): + """Output the licenses of the project.""" + + color.cprint("") + color.cprint(section_title("Licenses: ")) + + if len(pkg.licenses) == 0: + color.cprint(" None") + else: + pad = padder(pkg.licenses, 4) + for when_spec in pkg.licenses: + license_identifier = pkg.licenses[when_spec] + line = license(" {0}".format(pad(license_identifier))) + color.cescape(when_spec) + color.cprint(line) + + def info(parser, args): spec = spack.spec.Spec(args.package) pkg_cls = spack.repo.PATH.get_pkg_class(spec.name) @@ -377,6 +397,7 @@ def info(parser, args): (args.all or not args.no_dependencies, print_dependencies), (args.all or args.virtuals, print_virtuals), (args.all or args.tests, print_tests), + (args.all or True, print_licenses), ] for print_it, func in sections: if print_it: diff --git a/lib/spack/spack/directives.py b/lib/spack/spack/directives.py index 9ac992b209..7ebf68e548 100644 --- a/lib/spack/spack/directives.py +++ b/lib/spack/spack/directives.py @@ -64,6 +64,7 @@ __all__ = [ "depends_on", "extends", "maintainers", + "license", "provides", "patch", "variant", @@ -862,6 +863,44 @@ def maintainers(*names: str): return _execute_maintainer +def _execute_license(pkg, license_identifier: str, when): + # If when is not specified the license always holds + when_spec = make_when_spec(when) + if not when_spec: + return + + for other_when_spec in pkg.licenses: + if when_spec.intersects(other_when_spec): + when_message = "" + if when_spec != make_when_spec(None): + when_message = f"when {when_spec}" + other_when_message = "" + if other_when_spec != make_when_spec(None): + other_when_message = f"when {other_when_spec}" + err_msg = ( + f"{pkg.name} is specified as being licensed as {license_identifier} " + f"{when_message}, but it is also specified as being licensed under " + f"{pkg.licenses[other_when_spec]} {other_when_message}, which conflict." + ) + raise OverlappingLicenseError(err_msg) + + pkg.licenses[when_spec] = license_identifier + + +@directive("licenses") +def license(license_identifier: str, when=None): + """Add a new license directive, to specify the SPDX identifier the software is + distributed under. + + Args: + license_identifiers: A list of SPDX identifiers specifying the licenses + the software is distributed under. + when: A spec specifying when the license applies. + """ + + return lambda pkg: _execute_license(pkg, license_identifier, when) + + @directive("requirements") def requires(*requirement_specs, policy="one_of", when=None, msg=None): """Allows a package to request a configuration to be present in all valid solutions. @@ -920,3 +959,7 @@ class DependencyPatchError(DirectiveError): class UnsupportedPackageDirective(DirectiveError): """Raised when an invalid or unsupported package directive is specified.""" + + +class OverlappingLicenseError(DirectiveError): + """Raised when two licenses are declared that apply on overlapping specs.""" diff --git a/lib/spack/spack/test/cmd/create.py b/lib/spack/spack/test/cmd/create.py index b99d221d02..089dc8b0c5 100644 --- a/lib/spack/spack/test/cmd/create.py +++ b/lib/spack/spack/test/cmd/create.py @@ -27,6 +27,7 @@ create = SpackCommand("create") [r"TestNamedPackage(Package)", r"def install(self"], ), (["file://example.tar.gz"], "example", [r"Example(Package)", r"def install(self"]), + (["-n", "test-license"], "test-license", [r'license("UNKNOWN")']), # Template-specific cases ( ["-t", "autoreconf", "/test-autoreconf"], diff --git a/lib/spack/spack/test/cmd/info.py b/lib/spack/spack/test/cmd/info.py index 4b2f5d2b39..c4528f9852 100644 --- a/lib/spack/spack/test/cmd/info.py +++ b/lib/spack/spack/test/cmd/info.py @@ -88,6 +88,7 @@ def test_info_fields(pkg_query, parser, print_buffer): "Installation Phases:", "Virtual Packages:", "Tags:", + "Licenses:", ) args = parser.parse_args(["--all", pkg_query]) diff --git a/lib/spack/spack/test/directives.py b/lib/spack/spack/test/directives.py index e32ec6ac08..677eb043a9 100644 --- a/lib/spack/spack/test/directives.py +++ b/lib/spack/spack/test/directives.py @@ -89,6 +89,44 @@ def test_maintainer_directive(config, mock_packages, package_name, expected_main assert pkg_cls.maintainers == expected_maintainers +@pytest.mark.parametrize( + "package_name,expected_licenses", [("licenses-1", [("MIT", "+foo"), ("Apache-2.0", "~foo")])] +) +def test_license_directive(config, mock_packages, package_name, expected_licenses): + pkg_cls = spack.repo.PATH.get_pkg_class(package_name) + for license in expected_licenses: + assert spack.spec.Spec(license[1]) in pkg_cls.licenses + assert license[0] == pkg_cls.licenses[spack.spec.Spec(license[1])] + + +def test_duplicate_exact_range_license(): + package = namedtuple("package", ["licenses", "name"]) + package.licenses = {spack.directives.make_when_spec("+foo"): "Apache-2.0"} + package.name = "test_package" + + msg = ( + r"test_package is specified as being licensed as MIT when \+foo, but it is also " + r"specified as being licensed under Apache-2.0 when \+foo, which conflict." + ) + + with pytest.raises(spack.directives.OverlappingLicenseError, match=msg): + spack.directives._execute_license(package, "MIT", "+foo") + + +def test_overlapping_duplicate_licenses(): + package = namedtuple("package", ["licenses", "name"]) + package.licenses = {spack.directives.make_when_spec("+foo"): "Apache-2.0"} + package.name = "test_package" + + msg = ( + r"test_package is specified as being licensed as MIT when \+bar, but it is also " + r"specified as being licensed under Apache-2.0 when \+foo, which conflict." + ) + + with pytest.raises(spack.directives.OverlappingLicenseError, match=msg): + spack.directives._execute_license(package, "MIT", "+bar") + + def test_version_type_validation(): # A version should be a string or an int, not a float, because it leads to subtle issues # such as 3.10 being interpreted as 3.1. diff --git a/var/spack/repos/builtin.mock/packages/licenses-1/package.py b/var/spack/repos/builtin.mock/packages/licenses-1/package.py new file mode 100644 index 0000000000..d5c67830c9 --- /dev/null +++ b/var/spack/repos/builtin.mock/packages/licenses-1/package.py @@ -0,0 +1,18 @@ +# Copyright 2013-2023 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) + +from spack.package import * + + +class Licenses1(Package): + """Package with a licenses field.""" + + homepage = "https://www.example.com" + url = "https://www.example.com/license" + + license("MIT", when="+foo") + license("Apache-2.0", when="~foo") + + version("1.0", md5="0123456789abcdef0123456789abcdef") diff --git a/var/spack/repos/builtin/packages/zlib/package.py b/var/spack/repos/builtin/packages/zlib/package.py index a4edbea4a0..144e3b0ec6 100644 --- a/var/spack/repos/builtin/packages/zlib/package.py +++ b/var/spack/repos/builtin/packages/zlib/package.py @@ -60,6 +60,8 @@ class Zlib(MakefilePackage, Package): provides("zlib-api") + license("Zlib") + @property def libs(self): shared = "+shared" in self.spec |