Skip to content

Commit e872807

Browse files
committed
Scripts: with node-license-check with yarn plugin
Yarn auto inserts dependencies to `node-gyp` when it sees a `.gyp` file in the package; however, we actually use `node-gyp-build` instead, so that gets avoided (it's still used at build time, but not when running). So we need to customize the plugin to remove that link before checking the licenses. Note that we only do so for known packages, as we need to ensure that we do not bring in a hard dependency on `node-gyp` from elsewhere. Signed-off-by: Mark Yen <[email protected]>
1 parent 6ea4d58 commit e872807

File tree

6 files changed

+1604
-47
lines changed

6 files changed

+1604
-47
lines changed

.github/workflows/test.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@ jobs:
3636
with:
3737
persist-credentials: false
3838
- uses: ./.github/actions/yarn-install
39-
- run: ./scripts/node-license-check.sh
40-
shell: bash
39+
- run: yarn license-check
4140
- run: ./scripts/go-license-check.sh
4241
shell: bash
4342
- run: yarn lint:nofix
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
/*
2+
Copyright © 2025 SUSE LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
// @ts-check
17+
module.exports = {
18+
name: 'plugin-rancher-desktop-license-checker',
19+
/** @type (_: NodeJS.Require) => any */
20+
factory: require => {
21+
const { BaseCommand, WorkspaceRequiredError } = require('@yarnpkg/cli');
22+
const { Cache, Configuration, Project, ThrowReport, structUtils, Workspace } = require('@yarnpkg/core');
23+
const { Filename } = require('@yarnpkg/fslib');
24+
25+
const UNKNOWN_LICENSE = '<unknown>';
26+
// https://github.com/cncf/foundation/blob/main/policies-guidance/allowed-third-party-license-policy.md#approved-licenses-for-allowlist
27+
const ALLOWED_LICENSES = new Set([
28+
'0BSD',
29+
'BSD-2-Clause',
30+
'BSD-2-Clause-FreeBSD',
31+
'BSD-3-Clause',
32+
'MIT',
33+
'MIT-0',
34+
'ISC',
35+
'OpenSSL',
36+
'OpenSSL-standalone',
37+
'PSF-2.0',
38+
'Python-2.0',
39+
'Python-2.0.1',
40+
'PostgreSQL',
41+
'SSLeay-standalone', // spellcheck-ignore-line
42+
'UPL-1.0', // spellcheck-ignore-line
43+
'X11',
44+
'Zlib',
45+
// The default CNCF license, not in the above list.
46+
'Apache-2.0',
47+
// Extra accepted licenses.
48+
'Unlicense',
49+
]);
50+
51+
/**
52+
* Hard-coded list of license overrides, for licenses that are not correctly
53+
* specified in package.json (verified by browsing the source code).
54+
* @type Record<string, string>
55+
*/
56+
const overrides = {
57+
'esprima@npm:1.2.2': 'BSD-2-Clause', // https://github.com/jquery/esprima/pull/1181
58+
};
59+
60+
/**
61+
* LicenseParser is used to determine if a license is acceptable for use.
62+
*/
63+
class LicenseParser {
64+
/**
65+
* @param { string | string[] | { type: string } | { type: string }[]} input - The license text.
66+
*/
67+
constructor(input) {
68+
/** @type Set<string> */
69+
this.licenses = new Set ((Array.isArray(input) ? input : [input]).flatMap(entry => {
70+
return LicenseParser.parseLicenseString(typeof entry === 'string' ? entry : entry.type);
71+
}));
72+
}
73+
74+
/**
75+
* Parse an SPDX license identifier expression; currently only OR is allowed.
76+
* @param {string} input - The SPDX license identifier.
77+
* @returns {string[]} - The parsed result.
78+
*/
79+
static parseLicenseString(input) {
80+
const parenMatch = /^\((.*)\)$/.exec(input);
81+
82+
if (!parenMatch) {
83+
return [input];
84+
}
85+
return parenMatch[1].split(/\s+OR\s+/);
86+
}
87+
88+
toString() {
89+
return Array.from(this.licenses).join(' OR ');
90+
}
91+
92+
acceptable() {
93+
return this.licenses.intersection(ALLOWED_LICENSES).size > 0;
94+
}
95+
}
96+
97+
class LicenseCheckCommand extends BaseCommand {
98+
static paths = [['license-check']];
99+
100+
/** @override */
101+
async execute() {
102+
const { cwd, plugins } = this.context;
103+
const configuration = await Configuration.find(cwd, plugins);
104+
const { project, workspace } = await Project.find(configuration, cwd);
105+
const cache = await Cache.find(project.configuration);
106+
const report = new ThrowReport();
107+
108+
if (!workspace) {
109+
throw new WorkspaceRequiredError(project.cwd, cwd);
110+
}
111+
112+
/** @type { Iterable<import('@yarnpkg/core').Package> } */
113+
const dependencies = await this.getDependenciesForWorkspace(configuration, workspace);
114+
115+
const fetcher = configuration.makeFetcher();
116+
/** @type {(locator: import('@yarnpkg/core').Locator) => Promise<import('@yarnpkg/core').FetchResult>} */
117+
const wrappedFetch = locator => {
118+
return fetcher.fetch(locator, {cache, project, fetcher, report, checksums: project.storedChecksums});
119+
}
120+
121+
let hasErrors = false;
122+
for (const dependency of dependencies) {
123+
try {
124+
const licenses = await this.getLicensesForPackage(dependency, wrappedFetch);
125+
if (!licenses.acceptable()) {
126+
console.log(`${ structUtils.prettyLocator(configuration, dependency)} has disallowed license ${ licenses }`);
127+
hasErrors = true;
128+
}
129+
} catch (ex) {
130+
console.log(`Error fetching license for ${ structUtils.prettyLocator(configuration, dependency) }: ${ ex }`);
131+
}
132+
}
133+
134+
if (hasErrors) {
135+
process.exit(1);
136+
}
137+
138+
console.log('All NPM modules have acceptable licenses.');
139+
}
140+
141+
/**
142+
* Find all packages required by the given workspace, excluding development
143+
* dependencies.
144+
* @note Some dependencies to node-gyp are explicitly ignored because they
145+
* were automatically added by Yarn and do not actually exist.
146+
* @param {Configuration} configuration - The project configuration
147+
* @param {Workspace} workspace - The workspace to examine
148+
* @returns {Promise<Iterable<import('@yarnpkg/core').Package>>}
149+
*/
150+
async getDependenciesForWorkspace(configuration, workspace) {
151+
const blacklistedNodeGypDependencies = ['native-reg', 'node-addon-api'];
152+
const { project } = workspace;
153+
/** @type { Map<import('@yarnpkg/core').DescriptorHash, import('@yarnpkg/core').Package> } */
154+
const knownDependencies = new Map();
155+
156+
await project.restoreInstallState();
157+
158+
for (const workspace of project.workspaces) {
159+
workspace.manifest.devDependencies.clear();
160+
}
161+
162+
const cache = await Cache.find(project.configuration);
163+
await project.resolveEverything({ report: new ThrowReport(), cache });
164+
165+
const queue = project.workspaces.map(w => w.anchoredDescriptor);
166+
167+
while (queue.length > 0) {
168+
const descriptor = queue.pop();
169+
170+
if (!descriptor) {
171+
throw new Error('Popped empty item off queue');
172+
}
173+
if (knownDependencies.has(descriptor.descriptorHash)) {
174+
continue;
175+
}
176+
const locatorHash = project.storedResolutions.get(descriptor.descriptorHash);
177+
178+
if (!locatorHash) {
179+
throw new Error(`Failed to find locator for ${ structUtils.prettyDescriptor(configuration, descriptor) }`);
180+
}
181+
182+
const pkg = project.storedPackages.get(locatorHash);
183+
184+
if (!pkg) {
185+
throw new Error(`Failed to find package for ${ structUtils.prettyDescriptor(configuration, descriptor) }`);
186+
}
187+
188+
knownDependencies.set(descriptor.descriptorHash, pkg);
189+
for (const dep of pkg.dependencies.values()) {
190+
if (dep.name === 'node-gyp') {
191+
if (blacklistedNodeGypDependencies.includes(descriptor.name)) {
192+
// Yarn manually adds a dependency in this case, but it should be a devDependency instead.
193+
continue;
194+
}
195+
console.log(`Warning: Adding node-gyp dependency via ${ structUtils.prettyDescriptor(configuration, descriptor)}`);
196+
}
197+
queue.push(dep);
198+
}
199+
}
200+
201+
// Remove the anchors, as that's not a "dependency".
202+
for (const workspace of project.workspaces) {
203+
knownDependencies.delete(workspace.anchoredDescriptor.descriptorHash);
204+
}
205+
206+
return knownDependencies.values();
207+
}
208+
209+
/**
210+
* Given a descriptor, return the licenses used by the package.
211+
* @param {import('@yarnpkg/core').Package} pkg - The descriptor for the package to fetch.
212+
* @param {(locator: import('@yarnpkg/core').Locator) => Promise<import('@yarnpkg/core').FetchResult>} fetcher - A function to fetch files from packages.
213+
* @returns {Promise<LicenseParser>} - The licenses in use.
214+
*/
215+
async getLicensesForPackage(pkg, fetcher) {
216+
const { packageFs } = await fetcher(pkg);
217+
const { pathUtils } = packageFs;
218+
const packageNameAndVersion = structUtils.stringifyLocator(pkg);
219+
220+
if (packageNameAndVersion in overrides) {
221+
return new LicenseParser(overrides[packageNameAndVersion]);
222+
}
223+
224+
/** @type any */
225+
const packageName = structUtils.stringifyIdent(pkg);
226+
const manifestPath = pathUtils.join(Filename.nodeModules, packageName, Filename.manifest);
227+
const manifest = await packageFs.readJsonPromise(manifestPath);
228+
return new LicenseParser(manifest.license ?? manifest.licenses ?? UNKNOWN_LICENSE);
229+
}
230+
}
231+
232+
return { commands: [ LicenseCheckCommand ] };
233+
},
234+
};

.yarnrc.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
nodeLinker: node-modules
22

33
plugins:
4-
- checksum: ffd9b2dbbe2efe008987559cb4cfb8feeee45eae9b550bbd6154a535919362466f4a09a9de56f1e1502a804592b95848839bc9c5bdd7d4e2891d8532e7045258
5-
path: .yarn/plugins/@yarnpkg/plugin-licenses.cjs
6-
spec: "https://github.com/mhassan1/yarn-plugin-licenses/raw/c75a8fbe0b00a1dc332868772777c0bb06c69d50/bundles/@yarnpkg/plugin-licenses.js"
4+
- path: .yarn/plugins/plugin-rancher-desktop-license-checker.cjs

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@
124124
"@vue/eslint-config-standard-with-typescript": "9.2.0",
125125
"@vue/eslint-config-typescript": "14.6.0",
126126
"@vue/test-utils": "2.4.6",
127+
"@yarnpkg/cli": "^4.9.4",
128+
"@yarnpkg/core": "^4.4.3",
129+
"@yarnpkg/types": "^4.0.1",
127130
"babel-core": "7.0.0-bridge.0",
128131
"babel-jest": "30.1.2",
129132
"babel-loader": "10.0.0",

scripts/node-license-check.sh

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)