/*
* Copyright (C) 2023-2024 Yomitan Authors
* Copyright (C) 2020-2022 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/>.
*/
import assert from 'assert';
import childProcess from 'child_process';
import fs from 'fs';
import JSZip from 'jszip';
import {fileURLToPath} from 'node:url';
import path from 'path';
import readline from 'readline';
import {parseArgs} from 'util';
import {buildLibs} from '../build-libs.js';
import {ManifestUtil} from '../manifest-util.js';
import {getAllFiles} from '../util.js';
const dirname = path.dirname(fileURLToPath(import.meta.url));
/**
* @param {string} directory
* @param {string[]} excludeFiles
* @param {string} outputFileName
* @param {string[]} sevenZipExes
* @param {?import('jszip').OnUpdateCallback} onUpdate
* @param {boolean} dryRun
*/
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
}
}
}
await createJSZip(directory, excludeFiles, outputFileName, onUpdate, dryRun);
}
/**
* @param {string} directory
* @param {string[]} excludeFiles
* @param {string} outputFileName
* @param {?import('jszip').OnUpdateCallback} onUpdate
* @param {boolean} dryRun
*/
async function createJSZip(directory, excludeFiles, outputFileName, onUpdate, dryRun) {
const files = getAllFiles(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'});
}
}
/**
* @param {string[]} array
* @param {string[]} removeItems
*/
function removeItemsFromArray(array, removeItems) {
for (const item of removeItems) {
const index = getIndexOfFilePath(array, item);
if (index >= 0) {
array.splice(index, 1);
}
}
}
/**
* @param {string[]} array
* @param {string} item
* @returns {number}
*/
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;
}
/**
* @param {string} buildDir
* @param {string} extDir
* @param {ManifestUtil} manifestUtil
* @param {string[]} variantNames
* @param {string} manifestPath
* @param {boolean} dryRun
* @param {boolean} dryRunBuildZip
* @param {string} yomitanVersion
*/
async function build(buildDir, extDir, manifestUtil, variantNames, manifestPath, dryRun, dryRunBuildZip, yomitanVersion) {
const sevenZipExes = ['7za', '7z'];
// Create build directory
if (!fs.existsSync(buildDir) && !dryRun) {
fs.mkdirSync(buildDir, {recursive: true});
}
const dontLogOnUpdate = !process.stdout.isTTY;
/** @type {import('jszip').OnUpdateCallback} */
const onUpdate = (metadata) => {
if (dontLogOnUpdate) { return; }
let message = `Progress: ${metadata.percent.toFixed(2)}%`;
if (metadata.currentFile) {
message += ` (${metadata.currentFile})`;
}
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
process.stdout.write(message);
};
process.stdout.write(`Version: ${yomitanVersion}...\n`);
for (const variantName of variantNames) {
const variant = manifestUtil.getVariant(variantName);
if (typeof variant === 'undefined' || variant.buildable === false) { continue; }
const {name, fileName, fileCopies} = variant;
let {excludeFiles} = variant;
if (!Array.isArray(excludeFiles)) { excludeFiles = []; }
process.stdout.write(`Building ${name}...\n`);
const modifiedManifest = manifestUtil.getManifest(variant.name);
ensureFilesExist(extDir, excludeFiles);
if (typeof fileName === 'string') {
const fileNameSafe = path.basename(fileName);
const fullFileName = path.join(buildDir, fileNameSafe);
if (!dryRun) {
fs.writeFileSync(manifestPath, ManifestUtil.createManifestString(modifiedManifest).replace('$YOMITAN_VERSION', yomitanVersion));
}
if (!dryRun || dryRunBuildZip) {
await createZip(extDir, excludeFiles, fullFileName, sevenZipExes, onUpdate, dryRun);
}
if (!dryRun && Array.isArray(fileCopies)) {
for (const fileName2 of fileCopies) {
const fileName2Safe = path.basename(fileName2);
fs.copyFileSync(fullFileName, path.join(buildDir, fileName2Safe));
}
}
}
process.stdout.write('\n');
}
}
/**
* @param {string} directory
* @param {string[]} files
*/
function ensureFilesExist(directory, files) {
for (const file of files) {
assert.ok(fs.existsSync(path.join(directory, file)));
}
}
/** */
export async function main() {
/** @type {import('util').ParseArgsConfig['options']} */
const parseArgsConfigOptions = {
all: {
type: 'boolean',
default: false,
},
default: {
type: 'boolean',
default: false,
},
manifest: {
type: 'string',
},
dryRun: {
type: 'boolean',
default: false,
},
dryRunBuildZip: {
type: 'boolean',
default: false,
},
version: {
type: 'string',
default: '0.0.0.0',
},
};
const argv = process.argv.slice(2);
const {values: args, positionals: targets} = parseArgs({args: argv, options: parseArgsConfigOptions, allowPositionals: true});
const dryRun = /** @type {boolean} */ (args.dryRun);
const dryRunBuildZip = /** @type {boolean} */ (args.dryRunBuildZip);
const yomitanVersion = /** @type {string} */ (args.version);
const manifestUtil = new ManifestUtil();
const rootDir = path.join(dirname, '..', '..');
const extDir = path.join(rootDir, 'ext');
const buildDir = path.join(rootDir, 'builds');
const manifestPath = path.join(extDir, 'manifest.json');
try {
await buildLibs();
const variantNames = /** @type {string[]} */ ((
argv.length === 0 || args.all ?
manifestUtil.getVariants().filter(({buildable}) => buildable !== false).map(({name}) => name) :
targets
));
await build(buildDir, extDir, manifestUtil, variantNames, manifestPath, dryRun, dryRunBuildZip, yomitanVersion);
} finally {
// Restore manifest
const manifestName = /** @type {?string} */ ((!args.default && typeof args.manifest !== 'undefined') ? args.manifest : null);
const restoreManifest = manifestUtil.getManifest(manifestName);
process.stdout.write('Restoring manifest...\n');
if (!dryRun) {
fs.writeFileSync(manifestPath, ManifestUtil.createManifestString(restoreManifest).replace('$YOMITAN_VERSION', yomitanVersion));
}
}
}
await main();