yomichan/dev/build.js
toasted-nutbread e54a2297e8
Build dry run test (#1129)
* Update testMain to support function args

* Add support for dry run and basic validation

* Don't log progress if not a TTY

* Ignore errors from fs.unlinkSync

* Add --dry-run-build-zip option to build zip in memory

* Add test-build
2020-12-18 20:51:22 -05:00

403 lines
14 KiB
JavaScript

/*
* Copyright (C) 2020 Yomichan Authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const fs = require('fs');
const path = require('path');
const assert = require('assert');
const readline = require('readline');
const childProcess = require('child_process');
const util = require('./util');
const {getAllFiles, getDefaultManifestAndVariants, createManifestString, getArgs, testMain} = util;
function clone(value) {
return JSON.parse(JSON.stringify(value));
}
async function createZip(directory, excludeFiles, outputFileName, sevenZipExes, onUpdate, dryRun) {
try {
fs.unlinkSync(outputFileName);
} catch (e) {
// NOP
}
if (!dryRun) {
for (const exe of sevenZipExes) {
try {
const excludeArguments = excludeFiles.map((excludeFilePath) => `-x!${excludeFilePath}`);
childProcess.execFileSync(
exe,
[
'a',
outputFileName,
'.',
...excludeArguments
],
{
cwd: directory
}
);
return;
} catch (e) {
// NOP
}
}
}
return await createJSZip(directory, excludeFiles, outputFileName, onUpdate, dryRun);
}
async function createJSZip(directory, excludeFiles, outputFileName, onUpdate, dryRun) {
const JSZip = util.JSZip;
const files = getAllFiles(directory, directory);
removeItemsFromArray(files, excludeFiles);
const zip = new JSZip();
for (const fileName of files) {
zip.file(
fileName.replace(/\\/g, '/'),
fs.readFileSync(path.join(directory, fileName), {encoding: null, flag: 'r'}),
{}
);
}
if (typeof onUpdate !== 'function') {
onUpdate = () => {}; // NOP
}
const data = await zip.generateAsync({
type: 'nodebuffer',
compression: 'DEFLATE',
compressionOptions: {level: 9}
}, onUpdate);
process.stdout.write('\n');
if (!dryRun) {
fs.writeFileSync(outputFileName, data, {encoding: null, flag: 'w'});
}
}
function removeItemsFromArray(array, removeItems) {
for (const item of removeItems) {
const index = getIndexOfFilePath(array, item);
if (index >= 0) {
array.splice(index, 1);
}
}
}
function getIndexOfFilePath(array, item) {
const pattern = /\\/g;
const separator = '/';
item = item.replace(pattern, separator);
for (let i = 0, ii = array.length; i < ii; ++i) {
if (array[i].replace(pattern, separator) === item) {
return i;
}
}
return -1;
}
function applyModifications(manifest, modifications) {
if (Array.isArray(modifications)) {
for (const modification of modifications) {
const {action, path: path2} = modification;
switch (action) {
case 'set':
{
const {value, before, after} = modification;
const object = getObjectProperties(manifest, path2, path2.length - 1);
const key = path2[path2.length - 1];
let {index} = modification;
if (typeof index !== 'number') {
index = -1;
}
if (typeof before === 'string') {
index = getObjectKeyIndex(object, before);
}
if (typeof after === 'string') {
index = getObjectKeyIndex(object, after);
if (index >= 0) { ++index; }
}
setObjectKeyAtIndex(object, key, value, index);
}
break;
case 'replace':
{
const {pattern, patternFlags, replacement} = modification;
const value = getObjectProperties(manifest, path2, path2.length - 1);
const regex = new RegExp(pattern, patternFlags);
const last = path2[path2.length - 1];
let value2 = value[last];
value2 = `${value2}`.replace(regex, replacement);
value[last] = value2;
}
break;
case 'delete':
{
const value = getObjectProperties(manifest, path2, path2.length - 1);
const last = path2[path2.length - 1];
delete value[last];
}
break;
case 'remove':
{
const {item} = modification;
const value = getObjectProperties(manifest, path2, path2.length);
const index = value.indexOf(item);
if (index >= 0) { value.splice(index, 1); }
}
break;
case 'splice':
{
const {start, deleteCount, items} = modification;
const value = getObjectProperties(manifest, path2, path2.length);
const itemsNew = items.map((v) => clone(v));
value.splice(start, deleteCount, ...itemsNew);
}
break;
case 'copy':
case 'move':
{
const {newPath, before, after} = modification;
const oldKey = path2[path2.length - 1];
const newKey = newPath[newPath.length - 1];
const oldObject = getObjectProperties(manifest, path2, path2.length - 1);
const newObject = getObjectProperties(manifest, newPath, newPath.length - 1);
const oldObjectIsNewObject = arraysAreSame(path2, newPath, -1);
const value = oldObject[oldKey];
let {index} = modification;
if (typeof index !== 'number' || index < 0) {
index = (oldObjectIsNewObject && action !== 'copy') ? getObjectKeyIndex(oldObject, oldKey) : -1;
}
if (typeof before === 'string') {
index = getObjectKeyIndex(newObject, before);
}
if (typeof after === 'string') {
index = getObjectKeyIndex(newObject, after);
if (index >= 0) { ++index; }
}
setObjectKeyAtIndex(newObject, newKey, value, index);
if (action !== 'copy' && (!oldObjectIsNewObject || oldKey !== newKey)) {
delete oldObject[oldKey];
}
}
break;
case 'add':
{
const {items} = modification;
const value = getObjectProperties(manifest, path2, path2.length);
const itemsNew = items.map((v) => clone(v));
value.push(...itemsNew);
}
break;
}
}
}
return manifest;
}
function arraysAreSame(array1, array2, lengthOffset) {
let ii = array1.length;
if (ii !== array2.length) { return false; }
ii += lengthOffset;
for (let i = 0; i < ii; ++i) {
if (array1[i] !== array2[i]) { return false; }
}
return true;
}
function getObjectKeyIndex(object, key) {
return Object.keys(object).indexOf(key);
}
function setObjectKeyAtIndex(object, key, value, index) {
if (index < 0 || typeof key === 'number' || Object.prototype.hasOwnProperty.call(object, key)) {
object[key] = value;
return;
}
const entries = Object.entries(object);
index = Math.min(index, entries.length);
for (let i = index, ii = entries.length; i < ii; ++i) {
const [key2] = entries[i];
delete object[key2];
}
entries.splice(index, 0, [key, value]);
for (let i = index, ii = entries.length; i < ii; ++i) {
const [key2, value2] = entries[i];
object[key2] = value2;
}
}
function getObjectProperties(object, path2, count) {
for (let i = 0; i < count; ++i) {
object = object[path2[i]];
}
return object;
}
function getInheritanceChain(variant, variantMap) {
const visited = new Set();
const inheritance = [];
while (true) {
const {name, inherit} = variant;
if (visited.has(name)) { break; }
visited.add(name);
inheritance.unshift(variant);
if (typeof inherit !== 'string') { break; }
const nextVariant = variantMap.get(inherit);
if (typeof nextVariant === 'undefined') { break; }
variant = nextVariant;
}
return inheritance;
}
function createVariantManifest(manifest, variant, variantMap) {
let modifiedManifest = clone(manifest);
for (const {modifications} of getInheritanceChain(variant, variantMap)) {
modifiedManifest = applyModifications(modifiedManifest, modifications);
}
return modifiedManifest;
}
async function build(manifest, buildDir, extDir, manifestPath, variantMap, variantNames, dryRun, dryRunBuildZip) {
const sevenZipExes = ['7za', '7z'];
// Create build directory
if (!fs.existsSync(buildDir) && !dryRun) {
fs.mkdirSync(buildDir, {recursive: true});
}
const dontLogOnUpdate = !process.stdout.isTTY;
const onUpdate = (metadata) => {
if (dontLogOnUpdate) { return; }
let message = `Progress: ${metadata.percent.toFixed(2)}%`;
if (metadata.currentFile) {
message += ` (${metadata.currentFile})`;
}
readline.clearLine(process.stdout);
readline.cursorTo(process.stdout, 0);
process.stdout.write(message);
};
for (const variantName of variantNames) {
const variant = variantMap.get(variantName);
if (typeof variant === 'undefined') { continue; }
const {name, fileName, fileCopies} = variant;
let {excludeFiles} = variant;
if (!Array.isArray(excludeFiles)) { excludeFiles = []; }
process.stdout.write(`Building ${name}...\n`);
const modifiedManifest = createVariantManifest(manifest, variant, variantMap);
const fileNameSafe = path.basename(fileName);
const fullFileName = path.join(buildDir, fileNameSafe);
ensureFilesExist(extDir, excludeFiles);
if (!dryRun) {
fs.writeFileSync(manifestPath, createManifestString(modifiedManifest));
}
if (!dryRun || dryRunBuildZip) {
await createZip(extDir, excludeFiles, fullFileName, sevenZipExes, onUpdate, dryRun);
}
if (!dryRun) {
if (Array.isArray(fileCopies)) {
for (const fileName2 of fileCopies) {
const fileName2Safe = path.basename(fileName2);
fs.copyFileSync(fullFileName, path.join(buildDir, fileName2Safe));
}
}
}
process.stdout.write('\n');
}
}
function ensureFilesExist(directory, files) {
for (const file of files) {
assert.ok(fs.existsSync(path.join(directory, file)));
}
}
async function main(argv) {
const args = getArgs(argv, new Map([
['all', false],
['default', false],
['manifest', null],
['dry-run', false],
['dry-run-build-zip', false],
[null, []]
]));
const dryRun = args.get('dry-run');
const dryRunBuildZip = args.get('dry-run-build-zip');
const {manifest, variants} = getDefaultManifestAndVariants();
const rootDir = path.join(__dirname, '..');
const extDir = path.join(rootDir, 'ext');
const buildDir = path.join(rootDir, 'builds');
const manifestPath = path.join(extDir, 'manifest.json');
const variantMap = new Map();
for (const variant of variants) {
variantMap.set(variant.name, variant);
}
try {
const variantNames = (argv.length === 0 || args.get('all') ? variants.map(({name}) => name) : args.get(null));
await build(manifest, buildDir, extDir, manifestPath, variantMap, variantNames, dryRun, dryRunBuildZip);
} finally {
// Restore manifest
let restoreManifest = manifest;
if (!args.get('default') && args.get('manifest') !== null) {
const variant = variantMap.get(args.get('manifest'));
if (typeof variant !== 'undefined') {
restoreManifest = createVariantManifest(manifest, variant, variantMap);
}
}
process.stdout.write('Restoring manifest...\n');
if (!dryRun) {
fs.writeFileSync(manifestPath, createManifestString(restoreManifest));
}
}
}
if (require.main === module) {
testMain(main, process.argv.slice(2));
}
module.exports = {
main
};