| /** | |
| * @license | |
| * Copyright (C) 2020 The Android Open Source Project | |
| * | |
| * 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. | |
| */ | |
| import * as path from "path"; | |
| import * as fs from "fs"; | |
| import {isSharedFileLicenseInfo, LicenseInfo, PackageInfo} from "./package-license-info"; | |
| import {LicenseName, PackageName, FilePath, PackageVersion} from "./base-types"; | |
| import { InstalledPackage, InsalledPackagesBuilder } from "./installed-node-modules-map"; | |
| import {SharedLicensesProvider} from "./shared-licenses-provider"; | |
| import {fail} from "./utils"; | |
| interface FilteredFiles { | |
| installedPackage: InstalledPackage; | |
| /** Path relative to installedPackage */ | |
| includedFiles: FilePath[]; | |
| /** Path relative to installedPackage */ | |
| excludedFiles: FilePath[]; | |
| } | |
| interface PackageLicensedFiles { | |
| packageInfo: PackageInfo; | |
| filteredFiles: FilteredFiles; | |
| } | |
| enum LicensedFilesDisplayInfoType { | |
| AllFiles = "AllFiles", | |
| OnlySpecificFiles = "OnlySpecificFiles", | |
| AllFilesExceptSpecific = "AllFilesExceptSpecific", | |
| } | |
| interface AllFiles { | |
| kind: LicensedFilesDisplayInfoType.AllFiles; | |
| } | |
| interface AllFilesExceptSpecificFiles { | |
| kind: LicensedFilesDisplayInfoType.AllFilesExceptSpecific; | |
| /** Path relative to installedPackage */ | |
| files: FilePath[] | |
| } | |
| interface OnlySpecificFiles { | |
| kind: LicensedFilesDisplayInfoType.OnlySpecificFiles; | |
| /** Path relative to installedPackage */ | |
| files: FilePath[]; | |
| } | |
| export type FilteredFilesDisplayInfo = AllFiles | AllFilesExceptSpecificFiles | OnlySpecificFiles; | |
| export interface LicenseMapPackageInfo { | |
| name: PackageName; | |
| version: PackageVersion; | |
| licensedFiles: FilteredFilesDisplayInfo; | |
| } | |
| export interface LicenseMapItem { | |
| licenseName: LicenseName, | |
| licenseText: string; | |
| packages: LicenseMapPackageInfo[]; | |
| } | |
| export type LicensesMap = { [licenseName: string]: LicenseMapItem } | |
| /** LicenseMapGenerator calculates license map for nodeModulesFiles. | |
| */ | |
| export class LicenseMapGenerator { | |
| /** | |
| * packages - information about licenses for packages | |
| * sharedLicensesProvider - returns text for shared licenses by name | |
| */ | |
| public constructor(private readonly packages: ReadonlyArray<PackageInfo>, private readonly sharedLicensesProvider: SharedLicensesProvider) { | |
| } | |
| /** generateMap calculates LicenseMap for nodeModulesFiles | |
| * Each (key, value) pair in this map has the following information: | |
| * The key is a license name. | |
| * The value contains information about packages and files for which this license | |
| * is applied. | |
| * | |
| * The method tries to provide information in a compact way: | |
| * 1) If all files in the package have the same license, then it stores a name of the package | |
| * without list of files | |
| * 2) If different files in the package has different licenses, then the method calculates | |
| * which list is shorter - list of included files or list of excluded files. | |
| */ | |
| public generateMap(nodeModulesFiles: ReadonlyArray<string>): LicensesMap { | |
| const installedPackages = this.getInstalledPackages(nodeModulesFiles); | |
| // Static packages that are not inside `node_modules` directories. | |
| // gr-page.ts was derived from page.js, so we reproduce the original LICENSE. | |
| installedPackages.push({name: 'polygerrit-gr-page', version: 'current', rootPath: 'polygerrit-ui/app/elements/core/gr-router/', files: ['gr-page.ts']}); | |
| const licensedFilesGroupedByLicense = this.getLicensedFilesGroupedByLicense(installedPackages); | |
| const result: LicensesMap = {}; | |
| licensedFilesGroupedByLicense.forEach((packageLicensedFiles, licenseName) => { | |
| result[licenseName] = this.getLicenseMapItem(licenseName, packageLicensedFiles); | |
| }); | |
| return result; | |
| } | |
| private getLicenseMapItem(licenseName: string, packagesLicensedFiles: PackageLicensedFiles[]): LicenseMapItem { | |
| const packages: LicenseMapPackageInfo[] = []; | |
| let licenseText: string = ""; | |
| packagesLicensedFiles.forEach((packageLicensedFiles) => { | |
| const packageLicenseText = this.getLicenseText(packageLicensedFiles); | |
| if (licenseText.length !== 0 && packageLicenseText !== licenseText) { | |
| fail(`There are different license texts for license '${licenseName}'.\n` + | |
| "Probably, you have multiple version of the same package.\n" + | |
| "In this case you must define different license name for each version" | |
| ); | |
| } | |
| licenseText = packageLicenseText; | |
| packages.push({ | |
| name: packageLicensedFiles.packageInfo.name, | |
| version: packageLicensedFiles.filteredFiles.installedPackage.version, | |
| licensedFiles: this.getFilteredFilesDisplayInfo(packageLicensedFiles.filteredFiles) | |
| }); | |
| }); | |
| return { | |
| licenseName, | |
| licenseText, | |
| packages | |
| }; | |
| } | |
| /** getFilteredFilesDisplayInfo calculates the best method to display information about | |
| * filteredFiles in filteredFiles.installedPackage | |
| * In the current implementation - the best method is a method with a shorter list of files | |
| * | |
| * Each {@link PackageInfo} has a filter for files (by default all files | |
| * are included). Applying filter to files from an installedPackage returns 2 lists of files: | |
| * includedFiles (i.e. files with {@link PackageInfo.license} license) and excludedFiles | |
| * (i.e. files which have different license(s)). | |
| * | |
| * A text representaion of license-map must have full information about per-file licenses if | |
| * needed, but we want to produce files lists there as short as possible | |
| */ | |
| private getFilteredFilesDisplayInfo(filteredFiles: FilteredFiles): FilteredFilesDisplayInfo { | |
| if (filteredFiles.includedFiles.length > 0 && filteredFiles.excludedFiles.length === 0) { | |
| /** All files from package are included (i.e. all files have the same license). | |
| * It is enough to print only installedPackage name. | |
| * */ | |
| return { | |
| kind: LicensedFilesDisplayInfoType.AllFiles | |
| } | |
| } else if (filteredFiles.includedFiles.length <= filteredFiles.excludedFiles.length) { | |
| /** After applying filter, the number of files with filteredFiles.license is less than | |
| * the number of excluded files. | |
| * It is more convenient to print information about license in the format: | |
| * GIT license - fontPackage (only files A,B,C)*/ | |
| return { | |
| kind: LicensedFilesDisplayInfoType.OnlySpecificFiles, | |
| files: filteredFiles.includedFiles | |
| }; | |
| } else { | |
| /** Only few files from filteredFiles.installedPackage has filteredFiles.license. | |
| * It is more convenient to print information about license in the format: | |
| * Apache license - fontPackage (except files A,B,C) | |
| */ | |
| return { | |
| kind: LicensedFilesDisplayInfoType.AllFilesExceptSpecific, | |
| files: filteredFiles.excludedFiles | |
| } | |
| } | |
| } | |
| private getLicensedFilesGroupedByLicense(installedPackages: InstalledPackage[]): Map<LicenseName, PackageLicensedFiles[]> { | |
| const result: Map<LicenseName, PackageLicensedFiles[]> = new Map(); | |
| installedPackages.forEach(installedPackage => { | |
| // It is possible that different files in package have different licenses. | |
| // See the getPackageInfosForInstalledPackage method for details. | |
| const packageInfos = this.findPackageInfosForInstalledPackage(installedPackage); | |
| if(packageInfos.length === 0) { | |
| fail(`License for package '${installedPackage.name}-${installedPackage.version}' was not found`); | |
| } | |
| const allPackageLicensedFiles: Set<string> = new Set(); | |
| packageInfos.forEach(packInfo => { | |
| const licensedFiles = this.filterFilesByPackageInfo(installedPackage, packInfo); | |
| if(licensedFiles.includedFiles.length === 0) { | |
| return; | |
| } | |
| const license = packInfo.license; | |
| if (!license.type.allowed) { | |
| fail(`Polygerrit bundle use files with invalid licence ${license.name} from` + | |
| ` the package ${installedPackage.name}`); | |
| } | |
| if(!result.has(license.name)) { | |
| result.set(license.name, []); | |
| } | |
| result.get(license.name)!.push({ | |
| packageInfo: packInfo, | |
| filteredFiles: licensedFiles | |
| }); | |
| licensedFiles.includedFiles.forEach((fileName) => { | |
| if(allPackageLicensedFiles.has(fileName)) { | |
| fail(`File '${fileName}' from '${installedPackage.name}' has multiple licenses.`) | |
| } | |
| allPackageLicensedFiles.add(fileName); | |
| }); | |
| }); | |
| if(allPackageLicensedFiles.size !== installedPackage.files.length) { | |
| fail(`Some files from '${installedPackage.name}' doesn't have a license, but they are used during build`); | |
| } | |
| }); | |
| return result; | |
| } | |
| /** getInstalledPackages Collects information about all installed packages */ | |
| private getInstalledPackages(nodeModulesFiles: ReadonlyArray<string>): InstalledPackage[] { | |
| const fullNonPackageNames: string[] = []; | |
| for (const p of this.packages) { | |
| if (p.nonPackages) { | |
| fullNonPackageNames.push(...p.nonPackages.map(name => `${p.name}/${name}`)); | |
| } | |
| } | |
| const builder = new InsalledPackagesBuilder(new Set(fullNonPackageNames)); | |
| // Register all package.json files - such files exists in the root folder of each module | |
| nodeModulesFiles.filter(f => path.basename(f) === "package.json") | |
| .forEach(packageJsonFile => builder.addPackageJson(packageJsonFile)); | |
| // Iterate through all files. builder adds each file to appropriate package | |
| nodeModulesFiles.forEach(f => builder.addFile(f)); | |
| return builder.build(); | |
| } | |
| /** | |
| * findPackageInfosForInstalledPackage finds all possible licenses for the installedPackage. | |
| * It is possible, that different files in package have different licenses. | |
| * For example, @polymer/font-roboto-local package has files with 2 different licenses | |
| * - .js files - Polymer license (i.e. BSD), | |
| * - font files - Apache 2.0 license | |
| * In this case, the package is defined several times with different filesFilter | |
| */ | |
| private findPackageInfosForInstalledPackage(installedPackage: InstalledPackage): PackageInfo[] { | |
| return this.packages.filter(packInfo => { | |
| if(packInfo.name !== installedPackage.name) { | |
| return false; | |
| } | |
| return !packInfo.versions || packInfo.versions.indexOf(installedPackage.version) >= 0; | |
| }); | |
| } | |
| /** Returns list of files which have license defined in packageInfo */ | |
| private filterFilesByPackageInfo(installedPackage: InstalledPackage, packageInfo: PackageInfo): FilteredFiles { | |
| const filter = packageInfo.filesFilter; | |
| if(!filter) { | |
| return { | |
| installedPackage, | |
| includedFiles: installedPackage.files, | |
| excludedFiles: [], | |
| } | |
| } | |
| return installedPackage.files.reduce((result: FilteredFiles, f) => { | |
| if(filter(f)) { | |
| result.includedFiles.push(f); | |
| } else { | |
| result.excludedFiles.push(f); | |
| } | |
| return result; | |
| }, { | |
| installedPackage, | |
| includedFiles: [], | |
| excludedFiles: [] | |
| }); | |
| } | |
| private getLicenseText(packageInfo: PackageLicensedFiles) { | |
| if(isSharedFileLicenseInfo(packageInfo.packageInfo.license)) { | |
| return this.sharedLicensesProvider.getText(packageInfo.packageInfo.license.sharedLicenseFile); | |
| } else { | |
| const filePath = path.join(packageInfo.filteredFiles.installedPackage.rootPath, | |
| packageInfo.packageInfo.license.packageLicenseFile) | |
| return fs.readFileSync(filePath, {encoding: 'utf-8'}); | |
| } | |
| } | |
| } | |