"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const spawn_rx_1 = require("spawn-rx");
const crypto = require("crypto");
const debug = require("debug");
const detectLibc = require("detect-libc");
const events_1 = require("events");
const fs = require("fs-extra");
const nodeAbi = require("node-abi");
const os = require("os");
const path = require("path");
const read_package_json_1 = require("./read-package-json");
const cache_1 = require("./cache");
const search_module_1 = require("./search-module");
const d = debug('electron-rebuild');
const defaultMode = process.platform === 'win32' ? 'sequential' : 'parallel';
const defaultTypes = ['prod', 'optional'];
// Update this number if you change the caching logic to ensure no bad cache hits
const ELECTRON_REBUILD_CACHE_ID = 1;
const locateBinary = (basePath, suffix) => __awaiter(void 0, void 0, void 0, function* () {
    let testPath = basePath;
    for (let upDir = 0; upDir <= 20; upDir++) {
        const checkPath = path.resolve(testPath, suffix);
        if (yield fs.pathExists(checkPath)) {
            return checkPath;
        }
        testPath = path.resolve(testPath, '..');
    }
    return null;
});
const locateNodeGyp = () => __awaiter(void 0, void 0, void 0, function* () {
    return yield locateBinary(__dirname, `node_modules/.bin/node-gyp${process.platform === 'win32' ? '.cmd' : ''}`);
});
const locatePrebuild = (modulePath) => __awaiter(void 0, void 0, void 0, function* () {
    return yield locateBinary(modulePath, 'node_modules/prebuild-install/bin.js');
});
class Rebuilder {
    constructor(options) {
        this.hashDirectory = (dir, relativeTo = dir) => __awaiter(this, void 0, void 0, function* () {
            d('hashing dir', dir);
            const dirTree = {};
            yield Promise.all((yield fs.readdir(dir)).map((child) => __awaiter(this, void 0, void 0, function* () {
                d('found child', child, 'in dir', dir);
                // Ignore output directories
                if (dir === relativeTo && (child === 'build' || child === 'bin'))
                    return;
                // Don't hash nested node_modules
                if (child === 'node_modules')
                    return;
                const childPath = path.resolve(dir, child);
                const relative = path.relative(relativeTo, childPath);
                if ((yield fs.stat(childPath)).isDirectory()) {
                    dirTree[relative] = yield this.hashDirectory(childPath, relativeTo);
                }
                else {
                    dirTree[relative] = crypto.createHash('SHA256').update(yield fs.readFile(childPath)).digest('hex');
                }
            })));
            return dirTree;
        });
        this.dHashTree = (tree, hash) => {
            for (const key of Object.keys(tree).sort()) {
                hash.update(key);
                if (typeof tree[key] === 'string') {
                    hash.update(tree[key]);
                }
                else {
                    this.dHashTree(tree[key], hash);
                }
            }
        };
        this.generateCacheKey = (opts) => __awaiter(this, void 0, void 0, function* () {
            const tree = yield this.hashDirectory(opts.modulePath);
            const hasher = crypto.createHash('SHA256')
                .update(`${ELECTRON_REBUILD_CACHE_ID}`)
                .update(path.basename(opts.modulePath))
                .update(this.ABI)
                .update(this.arch)
                .update(this.debug ? 'debug' : 'not debug')
                .update(this.headerURL)
                .update(this.electronVersion);
            this.dHashTree(tree, hasher);
            const hash = hasher.digest('hex');
            d('calculated hash of', opts.modulePath, 'to be', hash);
            return hash;
        });
        this.lifecycle = options.lifecycle;
        this.buildPath = options.buildPath;
        this.electronVersion = options.electronVersion;
        this.arch = options.arch || process.arch;
        this.extraModules = options.extraModules || [];
        this.onlyModules = options.onlyModules || null;
        this.force = options.force || false;
        this.headerURL = options.headerURL || 'https://www.electronjs.org/headers';
        this.types = options.types || defaultTypes;
        this.mode = options.mode || defaultMode;
        this.debug = options.debug || false;
        this.useCache = options.useCache || false;
        this.cachePath = options.cachePath || path.resolve(os.homedir(), '.electron-rebuild-cache');
        this.prebuildTagPrefix = options.prebuildTagPrefix || 'v';
        if (this.useCache && this.force) {
            console.warn('[WARNING]: Electron Rebuild has force enabled and cache enabled, force take precedence and the cache will not be used.');
            this.useCache = false;
        }
        this.projectRootPath = options.projectRootPath;
        if (typeof this.electronVersion === 'number') {
            if (`${this.electronVersion}`.split('.').length === 1) {
                this.electronVersion = `${this.electronVersion}.0.0`;
            }
            else {
                this.electronVersion = `${this.electronVersion}.0`;
            }
        }
        if (typeof this.electronVersion !== 'string') {
            throw new Error(`Expected a string version for electron version, got a "${typeof this.electronVersion}"`);
        }
        this.ABI = nodeAbi.getAbi(this.electronVersion, 'electron');
        this.prodDeps = this.extraModules.reduce((acc, x) => acc.add(x), new Set());
        this.rebuilds = [];
        this.realModulePaths = new Set();
        this.realNodeModulesPaths = new Set();
    }
    rebuild() {
        return __awaiter(this, void 0, void 0, function* () {
            if (!path.isAbsolute(this.buildPath)) {
                throw new Error('Expected buildPath to be an absolute path');
            }
            d('rebuilding with args:', this.buildPath, this.electronVersion, this.arch, this.extraModules, this.force, this.headerURL, this.types, this.debug);
            this.lifecycle.emit('start');
            const rootPackageJson = yield read_package_json_1.readPackageJson(this.buildPath);
            const markWaiters = [];
            const depKeys = [];
            if (this.types.indexOf('prod') !== -1 || this.onlyModules) {
                depKeys.push(...Object.keys(rootPackageJson.dependencies || {}));
            }
            if (this.types.indexOf('optional') !== -1 || this.onlyModules) {
                depKeys.push(...Object.keys(rootPackageJson.optionalDependencies || {}));
            }
            if (this.types.indexOf('dev') !== -1 || this.onlyModules) {
                depKeys.push(...Object.keys(rootPackageJson.devDependencies || {}));
            }
            for (const key of depKeys) {
                this.prodDeps[key] = true;
                const modulePaths = yield search_module_1.searchForModule(this.buildPath, key, this.projectRootPath);
                for (const modulePath of modulePaths) {
                    markWaiters.push(this.markChildrenAsProdDeps(modulePath));
                }
            }
            yield Promise.all(markWaiters);
            d('identified prod deps:', this.prodDeps);
            const nodeModulesPaths = yield search_module_1.searchForNodeModules(this.buildPath, this.projectRootPath);
            for (const nodeModulesPath of nodeModulesPaths) {
                yield this.rebuildAllModulesIn(nodeModulesPath);
            }
            this.rebuilds.push(() => this.rebuildModuleAt(this.buildPath));
            if (this.mode !== 'sequential') {
                yield Promise.all(this.rebuilds.map(fn => fn()));
            }
            else {
                for (const rebuildFn of this.rebuilds) {
                    yield rebuildFn();
                }
            }
        });
    }
    rebuildModuleAt(modulePath) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!(yield fs.pathExists(path.resolve(modulePath, 'binding.gyp')))) {
                return;
            }
            const nodeGypPath = yield locateNodeGyp();
            if (!nodeGypPath) {
                throw new Error('Could not locate node-gyp');
            }
            const buildType = this.debug ? 'Debug' : 'Release';
            const metaPath = path.resolve(modulePath, 'build', buildType, '.forge-meta');
            const metaData = `${this.arch}--${this.ABI}`;
            this.lifecycle.emit('module-found', path.basename(modulePath));
            if (!this.force && (yield fs.pathExists(metaPath))) {
                const meta = yield fs.readFile(metaPath, 'utf8');
                if (meta === metaData) {
                    d(`skipping: ${path.basename(modulePath)} as it is already built`);
                    this.lifecycle.emit('module-done');
                    this.lifecycle.emit('module-skip');
                    return;
                }
            }
            // prebuild already exists
            if (yield fs.pathExists(path.resolve(modulePath, 'prebuilds', `${process.platform}-${this.arch}`, `electron-${this.ABI}.node`))) {
                d(`skipping: ${path.basename(modulePath)} as it was prebuilt`);
                return;
            }
            let cacheKey;
            if (this.useCache) {
                cacheKey = yield this.generateCacheKey({
                    modulePath,
                });
                const applyDiffFn = yield cache_1.lookupModuleState(this.cachePath, cacheKey);
                if (typeof applyDiffFn === 'function') {
                    yield applyDiffFn(modulePath);
                    this.lifecycle.emit('module-done');
                    return;
                }
            }
            const modulePackageJson = yield read_package_json_1.readPackageJson(modulePath);
            if ((modulePackageJson.dependencies || {})['prebuild-install']) {
                d(`assuming is prebuild powered: ${path.basename(modulePath)}`);
                const prebuildInstallPath = yield locatePrebuild(modulePath);
                if (prebuildInstallPath) {
                    d(`triggering prebuild download step: ${path.basename(modulePath)}`);
                    let success = false;
                    const shimExt = process.env.ELECTRON_REBUILD_TESTS ? 'ts' : 'js';
                    const executable = process.env.ELECTRON_REBUILD_TESTS ? path.resolve(__dirname, '..', 'node_modules', '.bin', 'ts-node') : process.execPath;
                    try {
                        yield spawn_rx_1.spawnPromise(executable, [
                            path.resolve(__dirname, `prebuild-shim.${shimExt}`),
                            prebuildInstallPath,
                            `--arch=${this.arch}`,
                            `--platform=${process.platform}`,
                            '--runtime=electron',
                            `--target=${this.electronVersion}`,
                            `--tag-prefix=${this.prebuildTagPrefix}`
                        ], {
                            cwd: modulePath,
                        });
                        success = true;
                    }
                    catch (err) {
                        d('failed to use prebuild-install:', err);
                    }
                    if (success) {
                        d('built:', path.basename(modulePath));
                        yield fs.mkdirs(path.dirname(metaPath));
                        yield fs.writeFile(metaPath, metaData);
                        if (this.useCache) {
                            yield cache_1.cacheModuleState(modulePath, this.cachePath, cacheKey);
                        }
                        this.lifecycle.emit('module-done');
                        return;
                    }
                }
                else {
                    d(`could not find prebuild-install relative to: ${modulePath}`);
                }
            }
            if (modulePath.indexOf(' ') !== -1) {
                console.error('Attempting to build a module with a space in the path');
                console.error('See https://github.com/nodejs/node-gyp/issues/65#issuecomment-368820565 for reasons why this may not work');
                // FIXME: Re-enable the throw when more research has been done
                // throw new Error(`node-gyp does not support building modules with spaces in their path, tried to build: ${modulePath}`);
            }
            d('rebuilding:', path.basename(modulePath));
            const rebuildArgs = [
                'rebuild',
                `--target=${this.electronVersion}`,
                `--arch=${this.arch}`,
                `--dist-url=${this.headerURL}`,
                '--build-from-source',
            ];
            if (this.debug) {
                rebuildArgs.push('--debug');
            }
            for (const binaryKey of Object.keys(modulePackageJson.binary || {})) {
                if (binaryKey === 'napi_versions') {
                    continue;
                }
                let value = modulePackageJson.binary[binaryKey];
                if (binaryKey === 'module_path') {
                    value = path.resolve(modulePath, value);
                }
                value = value.replace('{configuration}', buildType)
                    .replace('{node_abi}', `electron-v${this.electronVersion.split('.').slice(0, 2).join('.')}`)
                    .replace('{platform}', process.platform)
                    .replace('{arch}', this.arch)
                    .replace('{version}', modulePackageJson.version)
                    .replace('{libc}', detectLibc.family || 'unknown');
                for (const binaryReplaceKey of Object.keys(modulePackageJson.binary)) {
                    value = value.replace(`{${binaryReplaceKey}}`, modulePackageJson.binary[binaryReplaceKey]);
                }
                rebuildArgs.push(`--${binaryKey}=${value}`);
            }
            if (process.env.GYP_MSVS_VERSION) {
                rebuildArgs.push(`--msvs_version=${process.env.GYP_MSVS_VERSION}`);
            }
            d('rebuilding', path.basename(modulePath), 'with args', rebuildArgs);
            yield spawn_rx_1.spawnPromise(nodeGypPath, rebuildArgs, {
                cwd: modulePath,
                /* eslint-disable @typescript-eslint/camelcase */
                env: Object.assign({}, process.env, {
                    USERPROFILE: path.resolve(os.homedir(), '.electron-gyp'),
                    npm_config_disturl: 'https://www.electronjs.org/headers',
                    npm_config_runtime: 'electron',
                    npm_config_arch: this.arch,
                    npm_config_target_arch: this.arch,
                    npm_config_build_from_source: 'true',
                    npm_config_debug: this.debug ? 'true' : '',
                    npm_config_devdir: path.resolve(os.homedir(), '.electron-gyp'),
                }),
            });
            d('built:', path.basename(modulePath));
            yield fs.mkdirs(path.dirname(metaPath));
            yield fs.writeFile(metaPath, metaData);
            const moduleName = path.basename(modulePath);
            const buildLocation = 'build/' + buildType;
            d('searching for .node file', path.resolve(modulePath, buildLocation));
            d('testing files', (yield fs.readdir(path.resolve(modulePath, buildLocation))));
            const nodeFile = (yield fs.readdir(path.resolve(modulePath, buildLocation)))
                .find((file) => file !== '.node' && file.endsWith('.node'));
            const nodePath = nodeFile ? path.resolve(modulePath, buildLocation, nodeFile) : undefined;
            const abiPath = path.resolve(modulePath, `bin/${process.platform}-${this.arch}-${this.ABI}`);
            if (nodePath && (yield fs.pathExists(nodePath))) {
                d('found .node file', nodePath);
                d('copying to prebuilt place:', abiPath);
                yield fs.mkdirs(abiPath);
                yield fs.copy(nodePath, path.resolve(abiPath, `${moduleName}.node`));
            }
            if (this.useCache) {
                yield cache_1.cacheModuleState(modulePath, this.cachePath, cacheKey);
            }
            this.lifecycle.emit('module-done');
        });
    }
    rebuildAllModulesIn(nodeModulesPath, prefix = '') {
        return __awaiter(this, void 0, void 0, function* () {
            // Some package managers use symbolic links when installing node modules
            // we need to be sure we've never tested the a package before by resolving
            // all symlinks in the path and testing against a set
            const realNodeModulesPath = yield fs.realpath(nodeModulesPath);
            if (this.realNodeModulesPaths.has(realNodeModulesPath)) {
                return;
            }
            this.realNodeModulesPaths.add(realNodeModulesPath);
            d('scanning:', realNodeModulesPath);
            for (const modulePath of yield fs.readdir(realNodeModulesPath)) {
                // Ignore the magical .bin directory
                if (modulePath === '.bin')
                    continue;
                // Ensure that we don't mark modules as needing to be rebuilt more than once
                // by ignoring / resolving symlinks
                const realPath = yield fs.realpath(path.resolve(nodeModulesPath, modulePath));
                if (this.realModulePaths.has(realPath)) {
                    continue;
                }
                this.realModulePaths.add(realPath);
                if (this.prodDeps[`${prefix}${modulePath}`] && (!this.onlyModules || this.onlyModules.includes(modulePath))) {
                    this.rebuilds.push(() => this.rebuildModuleAt(realPath));
                }
                if (modulePath.startsWith('@')) {
                    yield this.rebuildAllModulesIn(realPath, `${modulePath}/`);
                }
                if (yield fs.pathExists(path.resolve(nodeModulesPath, modulePath, 'node_modules'))) {
                    yield this.rebuildAllModulesIn(path.resolve(realPath, 'node_modules'));
                }
            }
        });
    }
    findModule(moduleName, fromDir, foundFn) {
        return __awaiter(this, void 0, void 0, function* () {
            const testPaths = yield search_module_1.searchForModule(fromDir, moduleName, this.projectRootPath);
            const foundFns = testPaths.map(testPath => foundFn(testPath));
            return Promise.all(foundFns);
        });
    }
    markChildrenAsProdDeps(modulePath) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!(yield fs.pathExists(modulePath))) {
                return;
            }
            d('exploring', modulePath);
            let childPackageJson;
            try {
                childPackageJson = yield read_package_json_1.readPackageJson(modulePath, true);
            }
            catch (err) {
                return;
            }
            const moduleWait = [];
            const callback = this.markChildrenAsProdDeps.bind(this);
            for (const key of Object.keys(childPackageJson.dependencies || {}).concat(Object.keys(childPackageJson.optionalDependencies || {}))) {
                if (this.prodDeps[key]) {
                    continue;
                }
                this.prodDeps[key] = true;
                moduleWait.push(this.findModule(key, modulePath, callback));
            }
            yield Promise.all(moduleWait);
        });
    }
}
function rebuildWithOptions(options) {
    // eslint-disable-next-line prefer-rest-params
    d('rebuilding with args:', arguments);
    const lifecycle = new events_1.EventEmitter();
    const rebuilderOptions = Object.assign({}, options, { lifecycle });
    const rebuilder = new Rebuilder(rebuilderOptions);
    const ret = rebuilder.rebuild();
    ret.lifecycle = lifecycle;
    return ret;
}
function createOptions(buildPath, electronVersion, arch, extraModules, force, headerURL, types, mode, onlyModules, debug) {
    return {
        buildPath,
        electronVersion,
        arch,
        extraModules,
        onlyModules,
        force,
        headerURL,
        types,
        mode,
        debug
    };
}
exports.createOptions = createOptions;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function doRebuild(options, ...args) {
    if (typeof options === 'object') {
        return rebuildWithOptions(options);
    }
    console.warn('You are using the deprecated electron-rebuild API, please switch to using the options object instead');
    return rebuildWithOptions(createOptions(options, ...args));
}
exports.rebuild = doRebuild;
function rebuildNativeModules(electronVersion, modulePath, whichModule = '', _headersDir = null, arch = process.arch, _command, _ignoreDevDeps = false, _ignoreOptDeps = false, _verbose = false) {
    if (path.basename(modulePath) === 'node_modules') {
        modulePath = path.dirname(modulePath);
    }
    d('rebuilding in:', modulePath);
    console.warn('You are using the old API, please read the new docs and update to the new API');
    return exports.rebuild(modulePath, electronVersion, arch, whichModule.split(','));
}
exports.rebuildNativeModules = rebuildNativeModules;
//# sourceMappingURL=rebuild.js.map