Auto-generate third-party license notices (#370)

Closes #294
Closes #371
This commit is contained in:
Keavon Chambers 2021-09-06 06:57:35 -07:00
parent 208e4bbba3
commit fc7d3aa457
8 changed files with 306 additions and 14 deletions

View file

@ -46,21 +46,21 @@ jobs:
strategy: strategy:
matrix: matrix:
checks: checks:
- 'security advisories' - 'crate security advisories'
- 'banned licenses and crates' - 'crate license compatibility'
# Prevent sudden announcement of a new advisory from failing ci: # Prevent sudden announcement of a new advisory from failing ci:
continue-on-error: ${{ matrix.checks == 'security advisories' }} continue-on-error: ${{ matrix.checks == 'crate security advisories' }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: EmbarkStudios/cargo-deny-action@v1 - uses: EmbarkStudios/cargo-deny-action@v1
if: matrix.checks == 'security advisories' if: matrix.checks == 'crate security advisories'
with: with:
command: check advisories command: check advisories
- uses: EmbarkStudios/cargo-deny-action@v1 - uses: EmbarkStudios/cargo-deny-action@v1
if: matrix.checks == 'banned licenses and crates' if: matrix.checks == 'crate license compatibility'
with: with:
command: check bans licenses sources command: check bans licenses sources

21
about.hbs Normal file
View file

@ -0,0 +1,21 @@
// Be careful to prevent auto-formatting from breaking this file's indentation
// Replace this file with JSON output once this is resolved: https://github.com/EmbarkStudios/cargo-about/issues/73
module.exports = [
{{#each licenses}}
{
licenseName: `{{name}}`,
licenseText: `{{text}}`,
packages: [
{{#each used_by}}
{
name: `{{crate.name}}`,
version: `{{crate.version}}`,
author: `{{crate.authors}}`,
repository: `{{crate.repository}}`,
},
{{/each}}
],
},
{{/each}}
];

6
about.toml Normal file
View file

@ -0,0 +1,6 @@
accepted = [
"Apache-2.0",
"MIT",
]
ignore-build-dependencies = true
ignore-dev-dependencies = true

1
frontend/.gitignore vendored
View file

@ -1,3 +1,4 @@
node_modules/ node_modules/
dist/ dist/
wasm/pkg/ wasm/pkg/
rust-licenses.js

View file

@ -1408,6 +1408,12 @@
"integrity": "sha1-bI4obRHtdoMn+OYuzuhzU8o+eLg=", "integrity": "sha1-bI4obRHtdoMn+OYuzuhzU8o+eLg=",
"dev": true "dev": true
}, },
"array-find-index": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
"integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
"dev": true
},
"array-flatten": { "array-flatten": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@ -6882,6 +6888,66 @@
"type-check": "~0.3.2" "type-check": "~0.3.2"
} }
}, },
"license-checker-webpack-plugin": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/license-checker-webpack-plugin/-/license-checker-webpack-plugin-0.2.1.tgz",
"integrity": "sha512-rX8B+mH6fk1vxbnIu/UztqTEonQw95xwOkoRjX3TSrRZA/pbG9CWa3wnSo89KY/ej379JQoq050fsuthy6AU+A==",
"dev": true,
"requires": {
"glob": "^7.1.6",
"lodash.template": "^4.5.0",
"minimatch": "^3.0.4",
"semver": "^6.3.0",
"spdx-expression-validate": "^2.0.0",
"spdx-satisfies": "^5.0.0",
"superstruct": "^0.10.12",
"webpack-sources": "^1.4.3",
"wrap-ansi": "^6.1.0"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
},
"wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dev": true,
"requires": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
}
}
}
},
"lines-and-columns": { "lines-and-columns": {
"version": "1.1.6", "version": "1.1.6",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
@ -6969,6 +7035,12 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true "dev": true
}, },
"lodash._reinterpolate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
"integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=",
"dev": true
},
"lodash.camelcase": { "lodash.camelcase": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
@ -6993,6 +7065,25 @@
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
"dev": true "dev": true
}, },
"lodash.template": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz",
"integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==",
"dev": true,
"requires": {
"lodash._reinterpolate": "^3.0.0",
"lodash.templatesettings": "^4.0.0"
}
},
"lodash.templatesettings": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz",
"integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==",
"dev": true,
"requires": {
"lodash._reinterpolate": "^3.0.0"
}
},
"lodash.transform": { "lodash.transform": {
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.transform/-/lodash.transform-4.6.0.tgz", "resolved": "https://registry.npmjs.org/lodash.transform/-/lodash.transform-4.6.0.tgz",
@ -10047,6 +10138,17 @@
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true "dev": true
}, },
"spdx-compare": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz",
"integrity": "sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==",
"dev": true,
"requires": {
"array-find-index": "^1.0.2",
"spdx-expression-parse": "^3.0.0",
"spdx-ranges": "^2.0.0"
}
},
"spdx-correct": { "spdx-correct": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
@ -10073,12 +10175,38 @@
"spdx-license-ids": "^3.0.0" "spdx-license-ids": "^3.0.0"
} }
}, },
"spdx-expression-validate": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/spdx-expression-validate/-/spdx-expression-validate-2.0.0.tgz",
"integrity": "sha512-b3wydZLM+Tc6CFvaRDBOF9d76oGIHNCLYFeHbftFXUWjnfZWganmDmvtM5sm1cRwJc/VDBMLyGGrsLFd1vOxbg==",
"dev": true,
"requires": {
"spdx-expression-parse": "^3.0.0"
}
},
"spdx-license-ids": { "spdx-license-ids": {
"version": "3.0.7", "version": "3.0.7",
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz",
"integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==", "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==",
"dev": true "dev": true
}, },
"spdx-ranges": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.1.1.tgz",
"integrity": "sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==",
"dev": true
},
"spdx-satisfies": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-5.0.1.tgz",
"integrity": "sha512-Nwor6W6gzFp8XX4neaKQ7ChV4wmpSh2sSDemMFSzHxpTw460jxFYeOn+jq4ybnSSw/5sc3pjka9MQPouksQNpw==",
"dev": true,
"requires": {
"spdx-compare": "^1.0.0",
"spdx-expression-parse": "^3.0.0",
"spdx-ranges": "^2.0.0"
}
},
"spdy": { "spdy": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz",
@ -10369,6 +10497,12 @@
} }
} }
}, },
"superstruct": {
"version": "0.10.13",
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.10.13.tgz",
"integrity": "sha512-W4SitSZ9MOyMPbHreoZVEneSZyPEeNGbdfJo/7FkJyRs/M3wQRFzq+t3S/NBwlrFSWdx1ONLjLb9pB+UKe4IqQ==",
"dev": true
},
"supports-color": { "supports-color": {
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",

View file

@ -4,10 +4,10 @@
"description": "Graphite's web app frontend. Planned to be replaced by a native GUI written in Rust in the future.", "description": "Graphite's web app frontend. Planned to be replaced by a native GUI written in Rust in the future.",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve || npm install && vue-cli-service serve", "serve": "vue-cli-service serve || (npm install && vue-cli-service serve)",
"build": "vue-cli-service build || npm install && vue-cli-service build", "build": "cd .. && cargo install cargo-about && cargo about generate about.hbs > frontend/rust-licenses.js && cd frontend && (vue-cli-service build || (npm install && vue-cli-service build))",
"lint": "vue-cli-service lint || (npm install && vue-cli-service lint)", "lint": "vue-cli-service lint || (npm install && vue-cli-service lint)",
"lint-no-fix": "vue-cli-service lint --no-fix || (echo 'Please run `npm run lint`. If the linter execution fails, try running `npm install` first.' && false)" "lint-no-fix": "vue-cli-service lint --no-fix || (echo 'There were lint errors. Please run `npm run lint` to fix auto-them. If the linter execution fails, try running `npm install` first.' && false)"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -34,6 +34,7 @@
"eslint-plugin-import": "^2.24.2", "eslint-plugin-import": "^2.24.2",
"eslint-plugin-prettier-vue": "^3.1.0", "eslint-plugin-prettier-vue": "^3.1.0",
"eslint-plugin-vue": "^7.17.0", "eslint-plugin-vue": "^7.17.0",
"license-checker-webpack-plugin": "^0.2.1",
"prettier": "^2.3.2", "prettier": "^2.3.2",
"sass": "^1.39.0", "sass": "^1.39.0",
"sass-loader": "^8.0.2", "sass-loader": "^8.0.2",

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="menu-bar-input"> <div class="menu-bar-input">
<div class="entry-container"> <div class="entry-container">
<div @click="handleLogoClick(entry)" class="entry"> <div @click="() => window.open('https://www.graphite.design', '_blank')" class="entry">
<IconLabel :icon="'GraphiteLogo'" /> <IconLabel :icon="'GraphiteLogo'" />
</div> </div>
</div> </div>
@ -152,7 +152,16 @@ const menuEntries: MenuListEntries = [
{ {
label: "Help", label: "Help",
ref: undefined, ref: undefined,
children: [[{ label: "Menu entries coming soon" }]], children: [
[
{ label: "Report a Bug", action: () => window.open("https://github.com/GraphiteEditor/Graphite/issues/new", "_blank") },
{ label: "Visit on GitHub", action: () => window.open("https://github.com/GraphiteEditor/Graphite", "_blank") },
],
[
{ label: "Graphite License", action: () => window.open("https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/LICENSE.txt", "_blank") },
{ label: "Third-Party Licenses", action: () => window.open("/third-party-licenses.txt", "_blank") },
],
],
}, },
]; ];
@ -165,9 +174,6 @@ export default defineComponent({
if (menuEntry.ref) menuEntry.ref.setOpen(); if (menuEntry.ref) menuEntry.ref.setOpen();
else throw new Error("The menu bar floating menu has no associated ref"); else throw new Error("The menu bar floating menu has no associated ref");
}, },
handleLogoClick() {
window.open("https://www.graphite.design", "_blank");
},
}, },
data() { data() {
return { return {

View file

@ -1,6 +1,19 @@
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
const path = require("path"); const path = require("path");
const { unlink } = require("fs");
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
const LicenseCheckerWebpackPlugin = require("license-checker-webpack-plugin");
let rustLicenses = [];
let debugMode = false;
try {
// eslint-disable-next-line global-require, import/extensions, import/no-unresolved
rustLicenses = require("./rust-licenses");
} catch (_) {
// Rust licenses are not generated by Cargo About except in release mode (`npm run build`)
debugMode = true;
}
module.exports = { module.exports = {
lintOnSave: "warning", lintOnSave: "warning",
@ -18,7 +31,7 @@ module.exports = {
(Plugin) => (Plugin) =>
new Plugin({ new Plugin({
crateDirectory: path.resolve(__dirname, "wasm"), crateDirectory: path.resolve(__dirname, "wasm"),
// Remove when this issue is resolved https://github.com/wasm-tool/wasm-pack-plugin/issues/93 // Remove when this issue is resolved: https://github.com/wasm-tool/wasm-pack-plugin/issues/93
outDir: path.resolve(__dirname, "wasm/pkg"), outDir: path.resolve(__dirname, "wasm/pkg"),
watchDirectories: [ watchDirectories: [
path.resolve(__dirname, "../editor"), path.resolve(__dirname, "../editor"),
@ -30,6 +43,22 @@ module.exports = {
) )
.end(); .end();
// License Checker Webpack Plugin validates the license compatibility of all dependencies which are compiled into the webpack bundle
// It also writes the third-party license notices to a file which is displayed in the application
// https://github.com/microsoft/license-checker-webpack-plugin
config
.plugin("license-checker")
.use(LicenseCheckerWebpackPlugin)
.init(
(Plugin) =>
new Plugin({
allow: "(Apache-2.0 OR BSD-2-Clause OR BSD-3-Clause OR MIT)",
emitError: true,
outputFilename: "third-party-licenses.txt",
outputWriter: formatThirdPartyLicenses,
})
);
// Vue SVG Loader enables importing .svg files into .vue single-file components and using them directly in the HTML // Vue SVG Loader enables importing .svg files into .vue single-file components and using them directly in the HTML
// https://vue-svg-loader.js.org/ // https://vue-svg-loader.js.org/
config.module config.module
@ -47,3 +76,97 @@ module.exports = {
.end(); .end();
}, },
}; };
function formatThirdPartyLicenses(jsLicenses) {
// Remove the HTML character encoding caused by Handlebars
const licenses = rustLicenses.map((rustLicense) => ({
licenseName: htmlDecode(rustLicense.licenseName),
licenseText: htmlDecode(rustLicense.licenseText),
packages: rustLicense.packages.map((package) => ({
name: htmlDecode(package.name),
version: htmlDecode(package.version),
author: htmlDecode(package.author).replace(/\[(.*), \]/, "$1"),
repository: htmlDecode(package.repository),
})),
}));
// Augment the imported Rust license list with the provided JS license list
jsLicenses.dependencies.forEach((jsLicense) => {
const { name, version, author, repository, licenseName, licenseText } = jsLicense;
// Remove the `git+` or `git://` prefix and `.git` suffix
const repo = repository ? repository.replace(/^.*(github.com\/.*?\/.*?)(?:.git)/, "https://$1") : repository;
const matchedLicense = licenses.find((license) => license.licenseName.trim() === licenseName.trim() && license.licenseText.trim() === licenseText.trim());
const packages = { name, version, author, repository: repo };
if (matchedLicense) matchedLicense.packages.push(packages);
else licenses.push({ licenseName, licenseText, packages: [packages] });
});
// Sort the licenses, and the packages using each license, alphabetically
licenses.sort((a, b) => a.licenseName.localeCompare(b.licenseName));
licenses.forEach((license) => {
license.packages.sort((a, b) => a.name.localeCompare(b.name));
});
// Generate the formatted text file
let formattedLicenseNotice = "THIRD-PARTY SOFTWARE LICENSE NOTICES\n\n";
if (debugMode) formattedLicenseNotice += "WARNING: Licenses for Rust packages are excluded in debug mode to improve performance — do not release without their inclusion!\n\n";
licenses.forEach((license) => {
let packagesWithSameLicense = "";
license.packages.forEach((package) => {
const { name, version, author, repository } = package;
packagesWithSameLicense += `${name} ${version}${author ? ` - ${author}` : ""}${repository ? ` - ${repository}` : ""}\n`;
});
formattedLicenseNotice += `--------------------------------------------------------------------------------
The following packages are licensed under the terms of the ${license.licenseName} license:
${packagesWithSameLicense}
${license.licenseText}
`;
});
// Clean up by deleting the `rust-licenses.js` Rust licenses data file generated by Cargo About
unlink("./rust-licenses.js", (_) => _);
return formattedLicenseNotice;
}
const htmlEntities = {
nbsp: " ",
copy: "©",
reg: "®",
lt: "<",
gt: ">",
amp: "&",
apos: "'",
// eslint-disable-next-line quotes
quot: '"',
};
function htmlDecode(str) {
if (!str) return str;
return str.replace(/&([^;]+);/g, (entity, entityCode) => {
let match;
if (entityCode in htmlEntities) {
return htmlEntities[entityCode];
}
// eslint-disable-next-line no-cond-assign
if ((match = entityCode.match(/^#x([\da-fA-F]+)$/))) {
return String.fromCharCode(parseInt(match[1], 16));
}
// eslint-disable-next-line no-cond-assign
if ((match = entityCode.match(/^#(\d+)$/))) {
// eslint-disable-next-line no-bitwise
return String.fromCharCode(~~match[1]);
}
return entity;
});
}