forked from streamlit/streamlit
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathaudit_frontend_licenses.py
executable file
·197 lines (175 loc) · 6.66 KB
/
audit_frontend_licenses.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
#!/usr/bin/env python
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2024)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Audit the licenses of all our frontend dependencies (as defined by our
`yarn.lock` file). If any dependency has an unacceptable license, print it
out and exit with an error code. If all dependencies have acceptable licenses,
exit normally.
"""
import json
import subprocess
import sys
from pathlib import Path
from typing import NoReturn, Set, Tuple, cast
from typing_extensions import TypeAlias
PackageInfo: TypeAlias = Tuple[str, str, str, str, str, str]
SCRIPT_DIR = Path(__file__).resolve().parent
FRONTEND_DIR_LIB = SCRIPT_DIR.parent / "frontend/lib"
FRONTEND_DIR_APP = SCRIPT_DIR.parent / "frontend/app"
# Set of acceptable licenses. If a library uses one of these licenses,
# we can include it as a dependency.
ACCEPTABLE_LICENSES = {
"MIT", # https://opensource.org/licenses/MIT
"Apache-2.0", # https://opensource.org/licenses/Apache-2.0
"Apache-2.0 WITH LLVM-exception", # https://spdx.org/licenses/LLVM-exception.html
"0BSD", # https://opensource.org/licenses/0BSD
"BSD-2-Clause", # https://opensource.org/licenses/BSD-2-Clause
"BSD-3-Clause", # https://opensource.org/licenses/BSD-3-Clause
"ISC", # https://opensource.org/licenses/ISC
"CC0-1.0", # https://creativecommons.org/publicdomain/zero/1.0/
"CC-BY-3.0", # https://creativecommons.org/licenses/by/3.0/
"CC-BY-4.0", # https://creativecommons.org/licenses/by/4.0/
"Python-2.0", # https://www.python.org/download/releases/2.0/license/
"Zlib", # https://opensource.org/licenses/Zlib
"Unlicense", # https://unlicense.org/
"WTFPL", # http://www.wtfpl.net/about/
# Dual-licenses are acceptable if at least one of the two licenses is
# acceptable.
"(MIT OR Apache-2.0)",
"(MPL-2.0 OR Apache-2.0)",
"(MIT OR CC0-1.0)",
"(Apache-2.0 OR MPL-1.1)",
"(BSD-3-Clause OR GPL-2.0)",
"(MIT AND BSD-3-Clause)",
"(MIT AND Zlib)",
"(WTFPL OR MIT)",
"(AFL-2.1 OR BSD-3-Clause)",
}
# Some of our dependencies have licenses that yarn fails to parse, but that
# are still acceptable. This set contains all those exceptions. Each entry
# should include a comment about why it's an exception.
PACKAGE_EXCEPTIONS: Set[PackageInfo] = {
(
# MIT license: https://github.com/mapbox/jsonlint
"@mapbox/jsonlint-lines-primitives",
"2.0.2",
"UNKNOWN",
"git://github.com/mapbox/jsonlint.git",
"http://zaa.ch",
"Zach Carter",
),
(
# Apache 2.0 license: https://github.com/google/flatbuffers
"flatbuffers",
"23.5.26",
"SEE LICENSE IN LICENSE",
"git+https://github.com/google/flatbuffers.git",
"https://google.github.io/flatbuffers/",
"The FlatBuffers project",
),
(
# Mapbox Web SDK license: https://github.com/mapbox/mapbox-gl-js/blob/main/LICENSE.txt
"mapbox-gl",
"1.13.3",
"SEE LICENSE IN LICENSE.txt",
"git://github.com/mapbox/mapbox-gl-js.git",
"Unknown",
"Unknown",
),
(
# Mapbox Web SDK license: https://github.com/mapbox/mapbox-gl-js/blob/main/LICENSE.txt
"mapbox-gl",
"1.10.1",
"SEE LICENSE IN LICENSE.txt",
"git://github.com/mapbox/mapbox-gl-js.git",
"Unknown",
"Unknown",
),
(
# CC-BY-3.0 license: https://github.com/cartodb/cartocolor#licensing
"cartocolor",
"4.0.2",
"UNKNOWN",
"https://github.com/cartodb/cartocolor",
"http://carto.com/",
"Unknown",
),
(
# Apache-2.0 license: https://github.com/saikocat/colorbrewer/blob/master/LICENSE.txt
"colorbrewer",
"1.0.0",
"Apache*",
"https://github.com/saikocat/colorbrewer",
"http://colorbrewer2.org/",
"Cynthia Brewer",
),
}
def get_license_type(package: PackageInfo) -> str:
"""Return the license type string for a dependency entry."""
return package[2]
def check_licenses(licenses) -> NoReturn:
# `yarn licenses` outputs a bunch of lines.
# The last line contains the JSON object we care about
licenses_json = json.loads(licenses[len(licenses) - 1])
assert licenses_json["type"] == "table"
# Pull out the list of package infos from the JSON.
packages = [
cast(PackageInfo, tuple(package)) for package in licenses_json["data"]["body"]
]
# Discover dependency exceptions that are no longer used and can be
# jettisoned, and print them out with a warning.
unused_exceptions = PACKAGE_EXCEPTIONS.difference(set(packages))
if len(unused_exceptions) > 0:
for exception in sorted(list(unused_exceptions)):
print(f"Unused package exception, please remove: {exception}")
# Discover packages that don't have an acceptable license, and that don't
# have an explicit exception. If we have any, we print them out and exit
# with an error.
bad_packages = [
package
for package in packages
if (get_license_type(package) not in ACCEPTABLE_LICENSES)
and (package not in PACKAGE_EXCEPTIONS)
# workspace aggregator is yarn workspaces
and "workspace-aggregator" not in package[0]
]
if len(bad_packages) > 0:
for package in bad_packages:
print(f"Unacceptable license: '{get_license_type(package)}' (in {package})")
print(f"{len(bad_packages)} unacceptable licenses")
sys.exit(1)
print(f"No unacceptable licenses")
sys.exit(0)
def main() -> NoReturn:
# Run `yarn licenses` for lib.
licenses_output = (
subprocess.check_output(
["yarn", "licenses", "list", "--json", "--production", "--ignore-platform"],
cwd=str(FRONTEND_DIR_LIB),
)
.decode()
.splitlines()
)
# Run `yarn licenses` for app.
licenses_output = licenses_output + (
subprocess.check_output(
["yarn", "licenses", "list", "--json", "--production", "--ignore-platform"],
cwd=str(FRONTEND_DIR_APP),
)
.decode()
.splitlines()
)
check_licenses(licenses_output)
if __name__ == "__main__":
main()