summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/spack/docs/packaging_guide.rst27
-rw-r--r--lib/spack/spack/cmd/create.py3
-rw-r--r--lib/spack/spack/cmd/info.py21
-rw-r--r--lib/spack/spack/directives.py43
-rw-r--r--lib/spack/spack/test/cmd/create.py1
-rw-r--r--lib/spack/spack/test/cmd/info.py1
-rw-r--r--lib/spack/spack/test/directives.py38
-rw-r--r--var/spack/repos/builtin.mock/packages/licenses-1/package.py18
-rw-r--r--var/spack/repos/builtin/packages/zlib/package.py2
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