Add license directive (#39346)
This patch adds in a license directive to get the ball rolling on adding in license information about packages to spack. I'm primarily interested in just adding license into spack, but this would also help with other efforts that people are interested in such as adding license information to the ASP solve for concretization to make sure licenses are compatible. Usage: 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: ```python license("<SPDX Identifier HERE>") ``` For example, for Apache 2.0, you might write: ```python license("Apache-2.0") ``` 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: ```python license("MIT", when="@:1.5") ```
This commit is contained in:
@@ -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")
|
||||
|
||||
|
@@ -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}
|
||||
|
@@ -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:
|
||||
|
@@ -64,6 +64,7 @@ class OpenMpi(Package):
|
||||
"depends_on",
|
||||
"extends",
|
||||
"maintainers",
|
||||
"license",
|
||||
"provides",
|
||||
"patch",
|
||||
"variant",
|
||||
@@ -862,6 +863,44 @@ def _execute_maintainer(pkg):
|
||||
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."""
|
||||
|
@@ -27,6 +27,7 @@
|
||||
[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"],
|
||||
|
@@ -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])
|
||||
|
@@ -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.
|
||||
|
18
var/spack/repos/builtin.mock/packages/licenses-1/package.py
Normal file
18
var/spack/repos/builtin.mock/packages/licenses-1/package.py
Normal file
@@ -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")
|
@@ -60,6 +60,8 @@ class Zlib(MakefilePackage, Package):
|
||||
|
||||
provides("zlib-api")
|
||||
|
||||
license("Zlib")
|
||||
|
||||
@property
|
||||
def libs(self):
|
||||
shared = "+shared" in self.spec
|
||||
|
Reference in New Issue
Block a user