Skip to content

Commit 07df700

Browse files
toonnhamishmack
andauthored
Spdx license expressions (#859)
* Add parser combinators to parse SPDX license expressions The most important parser is `compoundExpression` it parses any SPDX license expression into a list of SPDX simple-expressions. The rationale is Nixpkgs' license metadata isn't capable of distinguishing between the AND and OR relationships. License exceptions aren't currently taken into account. * Add tests for the SPDX parser combinators I simply added a file with expressions that are expected to fail to parse or parse successfully. * Add the SPDX license list The SPDX license list as attrsets following the nixpkgs lib.licenses convention. This uses fetchurl which is not ideal because it's not available during pure evaluation. It does not yet include the SPDX exceptions list. * Refactor cabal-licenses.nix to use spdx/licenses.nix The handling of the generic licenses is undecided still. Some have been removed because they have better official identifiers. * Refactor license mapping in builders The common code in the comp- and setup-builders has been extracted and refactored to use the SPDX expression parser. * Use spdx-license-list-data from nixpkgs This conveniently solves the impurity problem with using fetchurl : ) I'm not sure threading `pkgs` through everything to get access to the spdx license list package is the right way to go about this. * hscolour to "LGPL-2.1-only" and remove "LGPL" * Use evalPackages for spdx and move shim to overlay * Better fix for LGPL packages. Co-authored-by: Hamish Mackenzie <[email protected]>
1 parent d70a242 commit 07df700

File tree

9 files changed

+217
-33
lines changed

9 files changed

+217
-33
lines changed

builder/comp-builder.nix

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{ stdenv, buildPackages, ghc, lib, gobject-introspection ? null, haskellLib, makeConfigFiles, haddockBuilder, ghcForComponent, hsPkgs, compiler, runCommand, libffi, gmp, zlib, ncurses, numactl, nodejs }:
1+
{ pkgs, stdenv, buildPackages, ghc, lib, gobject-introspection ? null, haskellLib, makeConfigFiles, haddockBuilder, ghcForComponent, hsPkgs, compiler, runCommand, libffi, gmp, zlib, ncurses, numactl, nodejs }:
22
lib.makeOverridable (
33
let self =
44
{ componentId
@@ -295,11 +295,7 @@ let
295295
meta = {
296296
homepage = package.homepage or "";
297297
description = package.synopsis or "";
298-
license =
299-
let
300-
license-map = import ../lib/cabal-licenses.nix lib;
301-
in license-map.${package.license} or
302-
(builtins.trace "WARNING: license \"${package.license}\" not found" license-map.LicenseRef-OtherLicense);
298+
license = haskellLib.cabalToNixpkgsLicense package.license;
303299
platforms = if platforms == null then stdenv.lib.platforms.all else platforms;
304300
};
305301

builder/setup-builder.nix

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{ stdenv, lib, buildPackages, haskellLib, ghc, nonReinstallablePkgs, hsPkgs, makeSetupConfigFiles, pkgconfig }:
1+
{ pkgs, stdenv, lib, buildPackages, haskellLib, ghc, nonReinstallablePkgs, hsPkgs, makeSetupConfigFiles, pkgconfig }:
22

33
{ component, package, name, src, flags ? {}, revision ? null, patches ? [], defaultSetupSrc
44
, preUnpack ? component.preUnpack, postUnpack ? component.postUnpack
@@ -55,11 +55,7 @@ let
5555
meta = {
5656
homepage = package.homepage or "";
5757
description = package.synopsis or "";
58-
license =
59-
let
60-
license-map = import ../lib/cabal-licenses.nix lib;
61-
in license-map.${package.license} or
62-
(builtins.trace "WARNING: license \"${package.license}\" not found" license-map.LicenseRef-OtherLicense);
58+
license = haskellLib.cabalToNixpkgsLicense package.license;
6359
platforms = if component.platforms == null then stdenv.lib.platforms.all else component.platforms;
6460
};
6561

lib/cabal-licenses.nix

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,20 @@
1-
lib: with lib.licenses;
2-
{ BSD-3-Clause = bsd3;
3-
BSD-2-Clause = bsd2;
4-
MIT = mit;
5-
"MPL-2.0" = mpl20;
6-
ISC = isc;
7-
"LGPL-2.1-only" = lgpl21;
8-
"LGPL-3.0-only" = lgpl3;
9-
"GPL-2.0-only" = gpl2;
10-
"GPL-2.0-or-later" = gpl2Plus;
11-
"GPL-3.0-only" = gpl3;
12-
"AGPL-3.0-only" = agpl3;
13-
"AGPL-3.0-or-later" = agpl3Plus;
14-
"Apache-2.0" = asl20;
15-
"GPL-2.0-or-later AND BSD-3-Clause" = [gpl2Plus bsd3];
16-
"NCSA" = ncsa;
1+
pkgs:
2+
let licenses = import spdx/licenses.nix pkgs;
3+
in licenses // {
174
# Generic
18-
LicenseRef-Apache = "Apache";
19-
LicenseRef-GPL = "GPL";
20-
LicenseRef-LGPL = "LGPL";
21-
LicenseRef-PublicDomain = publicDomain;
22-
LicenseRef-OtherLicense = null;
5+
LicenseRef-PublicDomain = {
6+
spdxId = "LicenseRef-PublicDomain";
7+
shortName = "Public Domain";
8+
fullName = "This work is dedicated to the Public Domain";
9+
url = "https://wikipedia.org/wiki/Public_domain";
10+
free = true;
11+
};
12+
LicenseRef-OtherLicense = {
13+
spdxId = "LicenseRef-OtherLicense";
14+
shortName = "Other License";
15+
fullName = "Unidentified Other License";
16+
url = "https://spdx.github.io/spdx-spec/appendix-IV-SPDX-license-expressions/";
17+
free = false;
18+
};
2319
NONE = null;
2420
}

lib/default.nix

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,9 @@ in {
274274
inherit pkgs;
275275
}) parseIndexState parseBlock;
276276

277+
278+
cabalToNixpkgsLicense = import ./spdx/cabal.nix pkgs;
279+
277280
# This function is like
278281
# `src + (if subDir == "" then "" else "/" + subDir)`
279282
# however when `includeSiblings` is set it maintains

lib/spdx/cabal.nix

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
let
2+
spdx = import ./parser.nix;
3+
in pkgs:
4+
with builtins;
5+
let
6+
# For better performance these are not in the
7+
# let block below (probably helps by increasing
8+
# the sharing)
9+
license-map = import ../cabal-licenses.nix pkgs;
10+
otherLicenseWarning = lic:
11+
trace "WARNING: license \"${lic}\" not found"
12+
license-map.LicenseRef-OtherLicense;
13+
in license:
14+
let
15+
licenses = spdx.compound-expression license;
16+
in if licenses == []
17+
then otherLicenseWarning license
18+
else map (lic: license-map.${lic} or (otherLicenseWarning lic))
19+
(pkgs.lib.unique (head licenses)._1)

lib/spdx/licenses.nix

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
pkgs:
2+
with builtins; let
3+
licensesJSON = fromJSON (replaceStrings
4+
[ "\\u0026" "\\u0027" "\\u003d" ]
5+
[ "&" "'" "=" ]
6+
(readFile "${pkgs.evalPackages.spdx-license-list-data}/json/licenses.json")
7+
);
8+
dropFour = s: substring 0 (stringLength s - 4) s;
9+
toSpdx = lic: with lic;
10+
{ spdxId = licenseId
11+
; shortName = licenseId
12+
; fullName = name
13+
; url = dropFour detailsUrl + "html"
14+
; free = isOsiApproved
15+
;
16+
};
17+
toNamedValue = lic: { name = lic.spdxId; value = lic; };
18+
in
19+
20+
listToAttrs (map (l: toNamedValue (toSpdx l)) licensesJSON.licenses)

lib/spdx/parser.nix

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
let String = { mempty = ""; mappend = a: b: a + b; };
2+
List = { singleton = x: [x]; };
3+
pair = a: b: { _1 = a; _2 = b; };
4+
Pair = { pure = s: pair String.mempty s;
5+
mappend = a: b: pair (String.mappend a._1 b._1) b._2;
6+
map = f: p: pair (f p._1) p._2;
7+
};
8+
compose = f: g: x: f (g x);
9+
in with builtins; rec {
10+
# Parser a = String -> [(a, String)]
11+
string = str: s:
12+
let strL = stringLength str;
13+
sL = stringLength s;
14+
in if str == substring 0 strL s
15+
then [ (pair str (substring strL sL s)) ]
16+
else [];
17+
18+
regex = capture_groups: re: s:
19+
let result = match "(${re})(.*)" s;
20+
in if result == null
21+
then []
22+
else [ (pair (elemAt result 0) (elemAt result (1 + capture_groups))) ];
23+
24+
# Left-biased choice
25+
choice = cs: s:
26+
if cs == []
27+
then []
28+
else let c = head cs;
29+
cs' = tail cs;
30+
result = c s;
31+
in if result == []
32+
then choice cs' s
33+
else result;
34+
35+
optional = p: s:
36+
let result = p s;
37+
in if result == []
38+
then [ (Pair.pure s) ]
39+
else result;
40+
41+
chain = ps: s:
42+
if ps == []
43+
then [ (Pair.pure s) ]
44+
else let p = head ps;
45+
ps' = tail ps;
46+
result = p s;
47+
in if result == []
48+
then []
49+
else let r = head result;
50+
rest = chain ps' r._2;
51+
in if rest == []
52+
then []
53+
else let r' = head rest;
54+
in [ (Pair.mappend r r') ];
55+
56+
idstring = regex 0 "[-\.0-9a-zA-Z]+";
57+
58+
license-ref = let documentref = chain [ (string "DocumentRef-")
59+
idstring
60+
(string ":")
61+
];
62+
in chain [
63+
(optional documentref)
64+
(string "LicenseRef-")
65+
idstring
66+
];
67+
68+
simple-expression = choice [ license-ref
69+
(chain [ idstring (string "+") ])
70+
idstring
71+
];
72+
73+
compound-expression = let wrap = compose (map (Pair.map List.singleton));
74+
in choice [
75+
(s: let result = simple-expression s;
76+
in if result == []
77+
then []
78+
else let r = head result;
79+
rest = chain [(string " WITH ") idstring ] r._2;
80+
in if rest == []
81+
then []
82+
else [(pair r._1 (head rest)._2)]
83+
)
84+
(s: let result = simple-expression s;
85+
in if result == []
86+
then []
87+
else let r = head result;
88+
firstLicense = r._1;
89+
operator = choice [ (string " AND ")
90+
(string " OR ")
91+
]
92+
r._2;
93+
in if operator == []
94+
then []
95+
else let s' = (head operator)._2;
96+
licenses = compound-expression s';
97+
in if licenses == []
98+
then []
99+
else let ls = head licenses;
100+
in [(pair ([firstLicense] ++ ls._1) ls._2)]
101+
)
102+
(wrap simple-expression)
103+
(s: let openParen = string "(" s;
104+
in if openParen == []
105+
then []
106+
else let result = compound-expression (head openParen)._2;
107+
in if result == []
108+
then []
109+
else let r = head result;
110+
closeParen = string ")" r._2;
111+
in if closeParen == []
112+
then []
113+
else [(pair r._1 (head closeParen)._2)])
114+
];
115+
}

lib/spdx/test.nix

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
let spdx = import ./parser.nix;
2+
in {
3+
idstringF = [ (spdx.idstring "$@#!$") ];
4+
idstringP = [ (spdx.idstring "blah") ];
5+
6+
license-refF = [ (spdx.license-ref "LicenseRef-$@#!$") ];
7+
license-refP = [ (spdx.license-ref "LicenseRef-blah")
8+
(spdx.license-ref "DocumentRef-beep:LicenseRef-boop")
9+
];
10+
11+
simple-expressionF = [ (spdx.simple-expression "$@#!$") ];
12+
simple-expressionP = [
13+
(spdx.simple-expression "blah")
14+
(spdx.simple-expression "blah+")
15+
(spdx.simple-expression "LicenseRef-blah")
16+
(spdx.simple-expression "DocumentRef-beep:LicenseRef-boop")
17+
];
18+
19+
compound-expressionF = [ (spdx.compound-expression "$@#!$") ];
20+
compound-expressionP = [
21+
(spdx.compound-expression "blah")
22+
(spdx.compound-expression "blah+")
23+
(spdx.compound-expression "LicenseRef-blah")
24+
(spdx.compound-expression "DocumentRef-beep:LicenseRef-boop")
25+
(spdx.compound-expression "(blah)")
26+
(spdx.compound-expression "beep OR boop")
27+
(spdx.compound-expression "beep AND boop")
28+
(spdx.compound-expression "beep WITH boop")
29+
(spdx.compound-expression "beep AND (boop OR blap)")
30+
(spdx.compound-expression "(beep AND ((boop OR ((blap)))))")
31+
];
32+
}

modules/configuration-nix.nix

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,11 @@
1515
pkgs.xorg.libXScrnSaver
1616
pkgs.xorg.libXinerama
1717
];
18+
19+
# These packages have `license: LGPL` in their .cabal file, but
20+
# do not specify the version. Setting the version here on
21+
# examination of the license files included in the packages.
22+
packages.hscolour.package.license = pkgs.lib.mkForce "LGPL-2.1-only";
23+
packages.cpphs.package.license = pkgs.lib.mkForce "LGPL-2.1-only";
24+
packages.polyparse.package.license = pkgs.lib.mkForce "LGPL-2.1-only";
1825
}

0 commit comments

Comments
 (0)