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:
Aiden Grossman
2023-10-18 03:58:19 -07:00
committed by GitHub
parent 37bafce384
commit 2802013dc6
9 changed files with 154 additions and 0 deletions

View File

@@ -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")

View File

@@ -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}

View File

@@ -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:

View File

@@ -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."""

View File

@@ -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"],

View File

@@ -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])

View File

@@ -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.

View 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")

View File

@@ -60,6 +60,8 @@ class Zlib(MakefilePackage, Package):
provides("zlib-api")
license("Zlib")
@property
def libs(self):
shared = "+shared" in self.spec