Dmitrii Filippov | 047240b | 2020-01-14 20:33:05 +0100 | [diff] [blame] | 1 | /** |
| 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 | |
| 18 | import * as path from "path"; |
| 19 | import * as fs from "fs"; |
| 20 | import {isSharedFileLicenseInfo, LicenseInfo, PackageInfo} from "./package-license-info"; |
| 21 | import {LicenseName, PackageName, FilePath, PackageVersion} from "./base-types"; |
| 22 | import { InstalledPackage, InsalledPackagesBuilder } from "./installed-node-modules-map"; |
| 23 | import {SharedLicensesProvider} from "./shared-licenses-provider"; |
| 24 | import {fail} from "./utils"; |
| 25 | |
| 26 | interface FilteredFiles { |
| 27 | installedPackage: InstalledPackage; |
| 28 | /** Path relative to installedPackage */ |
| 29 | includedFiles: FilePath[]; |
| 30 | /** Path relative to installedPackage */ |
| 31 | excludedFiles: FilePath[]; |
| 32 | } |
| 33 | interface PackageLicensedFiles { |
| 34 | packageInfo: PackageInfo; |
| 35 | filteredFiles: FilteredFiles; |
| 36 | } |
| 37 | |
| 38 | enum LicensedFilesDisplayInfoType { |
| 39 | AllFiles = "AllFiles", |
| 40 | OnlySpecificFiles = "OnlySpecificFiles", |
| 41 | AllFilesExceptSpecific = "AllFilesExceptSpecific", |
| 42 | } |
| 43 | |
| 44 | interface AllFiles { |
| 45 | kind: LicensedFilesDisplayInfoType.AllFiles; |
| 46 | } |
| 47 | |
| 48 | interface AllFilesExceptSpecificFiles { |
| 49 | kind: LicensedFilesDisplayInfoType.AllFilesExceptSpecific; |
| 50 | /** Path relative to installedPackage */ |
| 51 | files: FilePath[] |
| 52 | } |
| 53 | |
| 54 | interface OnlySpecificFiles { |
| 55 | kind: LicensedFilesDisplayInfoType.OnlySpecificFiles; |
| 56 | /** Path relative to installedPackage */ |
| 57 | files: FilePath[]; |
| 58 | } |
| 59 | |
| 60 | export type FilteredFilesDisplayInfo = AllFiles | AllFilesExceptSpecificFiles | OnlySpecificFiles; |
| 61 | |
| 62 | export interface LicenseMapPackageInfo { |
| 63 | name: PackageName; |
| 64 | version: PackageVersion; |
| 65 | licensedFiles: FilteredFilesDisplayInfo; |
| 66 | } |
| 67 | |
| 68 | export interface LicenseMapItem { |
| 69 | licenseName: LicenseName, |
| 70 | licenseText: string; |
| 71 | packages: LicenseMapPackageInfo[]; |
| 72 | } |
| 73 | |
| 74 | export type LicensesMap = { [licenseName: string]: LicenseMapItem } |
| 75 | |
| 76 | /** LicenseMapGenerator calculates license map for nodeModulesFiles. |
| 77 | */ |
| 78 | export 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 Rohlfs | 1da030b | 2023-01-31 13:09:53 +0100 | [diff] [blame] | 100 | // 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 Filippov | 047240b | 2020-01-14 20:33:05 +0100 | [diff] [blame] | 103 | 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 Filippov | 1b1ba78 | 2020-11-12 17:47:12 +0100 | [diff] [blame] | 222 | 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 Filippov | 047240b | 2020-01-14 20:33:05 +0100 | [diff] [blame] | 229 | // 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 | |