blob: 642a74946c2f69ded3b62e0794fa4cdd2e2e21e3 [file] [log] [blame]
Dmitrii Filippov047240b2020-01-14 20:33:05 +01001/**
2 * @license
3 * Copyright (C) 2020 The Android Open Source Project
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18import * as path from "path";
19import * as fs from "fs";
20import {isSharedFileLicenseInfo, LicenseInfo, PackageInfo} from "./package-license-info";
21import {LicenseName, PackageName, FilePath, PackageVersion} from "./base-types";
22import { InstalledPackage, InsalledPackagesBuilder } from "./installed-node-modules-map";
23import {SharedLicensesProvider} from "./shared-licenses-provider";
24import {fail} from "./utils";
25
26interface FilteredFiles {
27 installedPackage: InstalledPackage;
28 /** Path relative to installedPackage */
29 includedFiles: FilePath[];
30 /** Path relative to installedPackage */
31 excludedFiles: FilePath[];
32}
33interface PackageLicensedFiles {
34 packageInfo: PackageInfo;
35 filteredFiles: FilteredFiles;
36}
37
38enum LicensedFilesDisplayInfoType {
39 AllFiles = "AllFiles",
40 OnlySpecificFiles = "OnlySpecificFiles",
41 AllFilesExceptSpecific = "AllFilesExceptSpecific",
42}
43
44interface AllFiles {
45 kind: LicensedFilesDisplayInfoType.AllFiles;
46}
47
48interface AllFilesExceptSpecificFiles {
49 kind: LicensedFilesDisplayInfoType.AllFilesExceptSpecific;
50 /** Path relative to installedPackage */
51 files: FilePath[]
52}
53
54interface OnlySpecificFiles {
55 kind: LicensedFilesDisplayInfoType.OnlySpecificFiles;
56 /** Path relative to installedPackage */
57 files: FilePath[];
58}
59
60export type FilteredFilesDisplayInfo = AllFiles | AllFilesExceptSpecificFiles | OnlySpecificFiles;
61
62export interface LicenseMapPackageInfo {
63 name: PackageName;
64 version: PackageVersion;
65 licensedFiles: FilteredFilesDisplayInfo;
66}
67
68export interface LicenseMapItem {
69 licenseName: LicenseName,
70 licenseText: string;
71 packages: LicenseMapPackageInfo[];
72}
73
74export type LicensesMap = { [licenseName: string]: LicenseMapItem }
75
76/** LicenseMapGenerator calculates license map for nodeModulesFiles.
77 */
78export class LicenseMapGenerator {
79 /**
80 * packages - information about licenses for packages
81 * sharedLicensesProvider - returns text for shared licenses by name
82 */
83 public constructor(private readonly packages: ReadonlyArray<PackageInfo>, private readonly sharedLicensesProvider: SharedLicensesProvider) {
84 }
85
86 /** generateMap calculates LicenseMap for nodeModulesFiles
87 * Each (key, value) pair in this map has the following information:
88 * The key is a license name.
89 * The value contains information about packages and files for which this license
90 * is applied.
91 *
92 * The method tries to provide information in a compact way:
93 * 1) If all files in the package have the same license, then it stores a name of the package
94 * without list of files
95 * 2) If different files in the package has different licenses, then the method calculates
96 * which list is shorter - list of included files or list of excluded files.
97 */
98 public generateMap(nodeModulesFiles: ReadonlyArray<string>): LicensesMap {
99 const installedPackages = this.getInstalledPackages(nodeModulesFiles);
Ben Rohlfs1da030b2023-01-31 13:09:53 +0100100 // Static packages that are not inside `node_modules` directories.
101 // gr-page.ts was derived from page.js, so we reproduce the original LICENSE.
102 installedPackages.push({name: 'polygerrit-gr-page', version: 'current', rootPath: 'polygerrit-ui/app/elements/core/gr-router/', files: ['gr-page.ts']});
Dmitrii Filippov047240b2020-01-14 20:33:05 +0100103 const licensedFilesGroupedByLicense = this.getLicensedFilesGroupedByLicense(installedPackages);
104
105 const result: LicensesMap = {};
106 licensedFilesGroupedByLicense.forEach((packageLicensedFiles, licenseName) => {
107 result[licenseName] = this.getLicenseMapItem(licenseName, packageLicensedFiles);
108 });
109 return result;
110 }
111
112 private getLicenseMapItem(licenseName: string, packagesLicensedFiles: PackageLicensedFiles[]): LicenseMapItem {
113 const packages: LicenseMapPackageInfo[] = [];
114 let licenseText: string = "";
115 packagesLicensedFiles.forEach((packageLicensedFiles) => {
116 const packageLicenseText = this.getLicenseText(packageLicensedFiles);
117 if (licenseText.length !== 0 && packageLicenseText !== licenseText) {
118 fail(`There are different license texts for license '${licenseName}'.\n` +
119 "Probably, you have multiple version of the same package.\n" +
120 "In this case you must define different license name for each version"
121 );
122 }
123 licenseText = packageLicenseText;
124 packages.push({
125 name: packageLicensedFiles.packageInfo.name,
126 version: packageLicensedFiles.filteredFiles.installedPackage.version,
127 licensedFiles: this.getFilteredFilesDisplayInfo(packageLicensedFiles.filteredFiles)
128 });
129 });
130 return {
131 licenseName,
132 licenseText,
133 packages
134 };
135 }
136
137 /** getFilteredFilesDisplayInfo calculates the best method to display information about
138 * filteredFiles in filteredFiles.installedPackage
139 * In the current implementation - the best method is a method with a shorter list of files
140 *
141 * Each {@link PackageInfo} has a filter for files (by default all files
142 * are included). Applying filter to files from an installedPackage returns 2 lists of files:
143 * includedFiles (i.e. files with {@link PackageInfo.license} license) and excludedFiles
144 * (i.e. files which have different license(s)).
145 *
146 * A text representaion of license-map must have full information about per-file licenses if
147 * needed, but we want to produce files lists there as short as possible
148 */
149 private getFilteredFilesDisplayInfo(filteredFiles: FilteredFiles): FilteredFilesDisplayInfo {
150 if (filteredFiles.includedFiles.length > 0 && filteredFiles.excludedFiles.length === 0) {
151 /** All files from package are included (i.e. all files have the same license).
152 * It is enough to print only installedPackage name.
153 * */
154 return {
155 kind: LicensedFilesDisplayInfoType.AllFiles
156 }
157 } else if (filteredFiles.includedFiles.length <= filteredFiles.excludedFiles.length) {
158 /** After applying filter, the number of files with filteredFiles.license is less than
159 * the number of excluded files.
160 * It is more convenient to print information about license in the format:
161 * GIT license - fontPackage (only files A,B,C)*/
162 return {
163 kind: LicensedFilesDisplayInfoType.OnlySpecificFiles,
164 files: filteredFiles.includedFiles
165 };
166 } else {
167 /** Only few files from filteredFiles.installedPackage has filteredFiles.license.
168 * It is more convenient to print information about license in the format:
169 * Apache license - fontPackage (except files A,B,C)
170 */
171 return {
172 kind: LicensedFilesDisplayInfoType.AllFilesExceptSpecific,
173 files: filteredFiles.excludedFiles
174 }
175 }
176 }
177
178 private getLicensedFilesGroupedByLicense(installedPackages: InstalledPackage[]): Map<LicenseName, PackageLicensedFiles[]> {
179 const result: Map<LicenseName, PackageLicensedFiles[]> = new Map();
180 installedPackages.forEach(installedPackage => {
181 // It is possible that different files in package have different licenses.
182 // See the getPackageInfosForInstalledPackage method for details.
183 const packageInfos = this.findPackageInfosForInstalledPackage(installedPackage);
184 if(packageInfos.length === 0) {
185 fail(`License for package '${installedPackage.name}-${installedPackage.version}' was not found`);
186 }
187
188 const allPackageLicensedFiles: Set<string> = new Set();
189 packageInfos.forEach(packInfo => {
190 const licensedFiles = this.filterFilesByPackageInfo(installedPackage, packInfo);
191 if(licensedFiles.includedFiles.length === 0) {
192 return;
193 }
194 const license = packInfo.license;
195 if (!license.type.allowed) {
196 fail(`Polygerrit bundle use files with invalid licence ${license.name} from` +
197 ` the package ${installedPackage.name}`);
198 }
199 if(!result.has(license.name)) {
200 result.set(license.name, []);
201 }
202 result.get(license.name)!.push({
203 packageInfo: packInfo,
204 filteredFiles: licensedFiles
205 });
206 licensedFiles.includedFiles.forEach((fileName) => {
207 if(allPackageLicensedFiles.has(fileName)) {
208 fail(`File '${fileName}' from '${installedPackage.name}' has multiple licenses.`)
209 }
210 allPackageLicensedFiles.add(fileName);
211 });
212 });
213 if(allPackageLicensedFiles.size !== installedPackage.files.length) {
214 fail(`Some files from '${installedPackage.name}' doesn't have a license, but they are used during build`);
215 }
216 });
217 return result;
218 }
219
220 /** getInstalledPackages Collects information about all installed packages */
221 private getInstalledPackages(nodeModulesFiles: ReadonlyArray<string>): InstalledPackage[] {
Dmitrii Filippov1b1ba782020-11-12 17:47:12 +0100222 const fullNonPackageNames: string[] = [];
223 for (const p of this.packages) {
224 if (p.nonPackages) {
225 fullNonPackageNames.push(...p.nonPackages.map(name => `${p.name}/${name}`));
226 }
227 }
228 const builder = new InsalledPackagesBuilder(new Set(fullNonPackageNames));
Dmitrii Filippov047240b2020-01-14 20:33:05 +0100229 // Register all package.json files - such files exists in the root folder of each module
230 nodeModulesFiles.filter(f => path.basename(f) === "package.json")
231 .forEach(packageJsonFile => builder.addPackageJson(packageJsonFile));
232 // Iterate through all files. builder adds each file to appropriate package
233 nodeModulesFiles.forEach(f => builder.addFile(f));
234 return builder.build();
235 }
236
237 /**
238 * findPackageInfosForInstalledPackage finds all possible licenses for the installedPackage.
239 * It is possible, that different files in package have different licenses.
240 * For example, @polymer/font-roboto-local package has files with 2 different licenses
241 * - .js files - Polymer license (i.e. BSD),
242 * - font files - Apache 2.0 license
243 * In this case, the package is defined several times with different filesFilter
244 */
245 private findPackageInfosForInstalledPackage(installedPackage: InstalledPackage): PackageInfo[] {
246 return this.packages.filter(packInfo => {
247 if(packInfo.name !== installedPackage.name) {
248 return false;
249 }
250 return !packInfo.versions || packInfo.versions.indexOf(installedPackage.version) >= 0;
251 });
252 }
253
254 /** Returns list of files which have license defined in packageInfo */
255 private filterFilesByPackageInfo(installedPackage: InstalledPackage, packageInfo: PackageInfo): FilteredFiles {
256 const filter = packageInfo.filesFilter;
257 if(!filter) {
258 return {
259 installedPackage,
260 includedFiles: installedPackage.files,
261 excludedFiles: [],
262 }
263 }
264 return installedPackage.files.reduce((result: FilteredFiles, f) => {
265 if(filter(f)) {
266 result.includedFiles.push(f);
267 } else {
268 result.excludedFiles.push(f);
269 }
270 return result;
271 }, {
272 installedPackage,
273 includedFiles: [],
274 excludedFiles: []
275 });
276 }
277
278 private getLicenseText(packageInfo: PackageLicensedFiles) {
279 if(isSharedFileLicenseInfo(packageInfo.packageInfo.license)) {
280 return this.sharedLicensesProvider.getText(packageInfo.packageInfo.license.sharedLicenseFile);
281 } else {
282 const filePath = path.join(packageInfo.filteredFiles.installedPackage.rootPath,
283 packageInfo.packageInfo.license.packageLicenseFile)
284 return fs.readFileSync(filePath, {encoding: 'utf-8'});
285 }
286 }
287}
288