"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.OperationBuildCache = void 0;
const path = __importStar(require("node:path"));
const crypto = __importStar(require("node:crypto"));
const node_core_library_1 = require("@rushstack/node-core-library");
const TarExecutable_1 = require("../../utilities/TarExecutable");
const EnvironmentConfiguration_1 = require("../../api/EnvironmentConfiguration");
/**
 * @internal
 */
class OperationBuildCache {
    constructor(cacheId, options) {
        const { buildCacheConfiguration: { localCacheProvider, cloudCacheProvider, buildCacheEnabled, cacheWriteEnabled }, project, projectOutputFolderNames } = options;
        this._project = project;
        this._localBuildCacheProvider = localCacheProvider;
        this._cloudBuildCacheProvider = cloudCacheProvider;
        this._buildCacheEnabled = buildCacheEnabled;
        this._cacheWriteEnabled = cacheWriteEnabled;
        this._projectOutputFolderNames = projectOutputFolderNames || [];
        this._cacheId = cacheId;
    }
    static _tryGetTarUtility(terminal) {
        if (!OperationBuildCache._tarUtilityPromise) {
            OperationBuildCache._tarUtilityPromise = TarExecutable_1.TarExecutable.tryInitializeAsync(terminal);
        }
        return OperationBuildCache._tarUtilityPromise;
    }
    get cacheId() {
        return this._cacheId;
    }
    static getOperationBuildCache(options) {
        const cacheId = OperationBuildCache._getCacheId(options);
        return new OperationBuildCache(cacheId, options);
    }
    static forOperation(executionResult, options) {
        var _a, _b;
        const outputFolders = [...((_b = (_a = executionResult.operation.settings) === null || _a === void 0 ? void 0 : _a.outputFolderNames) !== null && _b !== void 0 ? _b : [])];
        if (executionResult.metadataFolderPath) {
            outputFolders.push(executionResult.metadataFolderPath);
        }
        const buildCacheOptions = {
            buildCacheConfiguration: options.buildCacheConfiguration,
            terminal: options.terminal,
            project: executionResult.operation.associatedProject,
            phaseName: executionResult.operation.associatedPhase.name,
            projectOutputFolderNames: outputFolders,
            operationStateHash: executionResult.getStateHash()
        };
        const cacheId = OperationBuildCache._getCacheId(buildCacheOptions);
        return new OperationBuildCache(cacheId, buildCacheOptions);
    }
    async tryRestoreFromCacheAsync(terminal, specifiedCacheId) {
        const cacheId = specifiedCacheId || this._cacheId;
        if (!cacheId) {
            terminal.writeWarningLine('Unable to get cache ID. Ensure Git is installed.');
            return false;
        }
        if (!this._buildCacheEnabled) {
            // Skip reading local and cloud build caches, without any noise
            return false;
        }
        let localCacheEntryPath = await this._localBuildCacheProvider.tryGetCacheEntryPathByIdAsync(terminal, cacheId);
        let cacheEntryBuffer;
        let updateLocalCacheSuccess;
        if (!localCacheEntryPath && this._cloudBuildCacheProvider) {
            terminal.writeVerboseLine('This project was not found in the local build cache. Querying the cloud build cache.');
            cacheEntryBuffer = await this._cloudBuildCacheProvider.tryGetCacheEntryBufferByIdAsync(terminal, cacheId);
            if (cacheEntryBuffer) {
                try {
                    localCacheEntryPath = await this._localBuildCacheProvider.trySetCacheEntryBufferAsync(terminal, cacheId, cacheEntryBuffer);
                    updateLocalCacheSuccess = true;
                }
                catch (e) {
                    updateLocalCacheSuccess = false;
                }
            }
        }
        if (!localCacheEntryPath && !cacheEntryBuffer) {
            terminal.writeVerboseLine('This project was not found in the build cache.');
            return false;
        }
        terminal.writeLine('Build cache hit.');
        terminal.writeVerboseLine(`Cache key: ${cacheId}`);
        const projectFolderPath = this._project.projectFolder;
        // Purge output folders
        terminal.writeVerboseLine(`Clearing cached folders: ${this._projectOutputFolderNames.join(', ')}`);
        await Promise.all(this._projectOutputFolderNames.map((outputFolderName) => node_core_library_1.FileSystem.deleteFolderAsync(`${projectFolderPath}/${outputFolderName}`)));
        const tarUtility = await OperationBuildCache._tryGetTarUtility(terminal);
        let restoreSuccess = false;
        if (tarUtility && localCacheEntryPath) {
            const logFilePath = this._getTarLogFilePath(cacheId, 'untar');
            const tarExitCode = await tarUtility.tryUntarAsync({
                archivePath: localCacheEntryPath,
                outputFolderPath: projectFolderPath,
                logFilePath
            });
            if (tarExitCode === 0) {
                restoreSuccess = true;
                terminal.writeLine('Successfully restored output from the build cache.');
            }
            else {
                terminal.writeWarningLine('Unable to restore output from the build cache. ' +
                    `See "${logFilePath}" for logs from the tar process.`);
            }
        }
        if (updateLocalCacheSuccess === false) {
            terminal.writeWarningLine('Unable to update the local build cache with data from the cloud cache.');
        }
        return restoreSuccess;
    }
    async trySetCacheEntryAsync(terminal, specifiedCacheId) {
        var _a, _b, _c;
        if (!this._cacheWriteEnabled) {
            // Skip writing local and cloud build caches, without any noise
            return true;
        }
        const cacheId = specifiedCacheId || this._cacheId;
        if (!cacheId) {
            terminal.writeWarningLine('Unable to get cache ID. Ensure Git is installed.');
            return false;
        }
        const filesToCache = await this._tryCollectPathsToCacheAsync(terminal);
        if (!filesToCache) {
            return false;
        }
        terminal.writeVerboseLine(`Caching build output folders: ${filesToCache.filteredOutputFolderNames.join(', ')}`);
        let localCacheEntryPath;
        const tarUtility = await OperationBuildCache._tryGetTarUtility(terminal);
        if (tarUtility) {
            const finalLocalCacheEntryPath = this._localBuildCacheProvider.getCacheEntryPath(cacheId);
            // Derive the temp file from the destination path to ensure they are on the same volume
            // In the case of a shared network drive containing the build cache, we also need to make
            // sure the the temp path won't be shared by two parallel rush builds.
            const randomSuffix = crypto.randomBytes(8).toString('hex');
            const tempLocalCacheEntryPath = `${finalLocalCacheEntryPath}-${randomSuffix}.temp`;
            const logFilePath = this._getTarLogFilePath(cacheId, 'tar');
            const tarExitCode = await tarUtility.tryCreateArchiveFromProjectPathsAsync({
                archivePath: tempLocalCacheEntryPath,
                paths: filesToCache.outputFilePaths,
                project: this._project,
                logFilePath
            });
            if (tarExitCode === 0) {
                // Move after the archive is finished so that if the process is interrupted we aren't left with an invalid file
                try {
                    await node_core_library_1.Async.runWithRetriesAsync({
                        action: () => node_core_library_1.FileSystem.moveAsync({
                            sourcePath: tempLocalCacheEntryPath,
                            destinationPath: finalLocalCacheEntryPath,
                            overwrite: true
                        }),
                        maxRetries: 2,
                        retryDelayMs: 500
                    });
                }
                catch (moveError) {
                    try {
                        await node_core_library_1.FileSystem.deleteFileAsync(tempLocalCacheEntryPath);
                    }
                    catch (deleteError) {
                        // Ignored
                    }
                    throw moveError;
                }
                localCacheEntryPath = finalLocalCacheEntryPath;
            }
            else {
                terminal.writeWarningLine(`"tar" exited with code ${tarExitCode} while attempting to create the cache entry. ` +
                    `See "${logFilePath}" for logs from the tar process.`);
                return false;
            }
        }
        else {
            terminal.writeWarningLine(`Unable to locate "tar". Please ensure that "tar" is on your PATH environment variable, or set the ` +
                `${EnvironmentConfiguration_1.EnvironmentVariableNames.RUSH_TAR_BINARY_PATH} environment variable to the full path to the "tar" binary.`);
            return false;
        }
        let cacheEntryBuffer;
        let setCloudCacheEntryPromise;
        // Note that "writeAllowed" settings (whether in config or environment) always apply to
        // the configured CLOUD cache. If the cache is enabled, rush is always allowed to read from and
        // write to the local build cache.
        if ((_a = this._cloudBuildCacheProvider) === null || _a === void 0 ? void 0 : _a.isCacheWriteAllowed) {
            if (localCacheEntryPath) {
                cacheEntryBuffer = await node_core_library_1.FileSystem.readFileToBufferAsync(localCacheEntryPath);
            }
            else {
                throw new node_core_library_1.InternalError('Expected the local cache entry path to be set.');
            }
            setCloudCacheEntryPromise = (_b = this._cloudBuildCacheProvider) === null || _b === void 0 ? void 0 : _b.trySetCacheEntryBufferAsync(terminal, cacheId, cacheEntryBuffer);
        }
        const updateCloudCacheSuccess = (_c = (await setCloudCacheEntryPromise)) !== null && _c !== void 0 ? _c : true;
        const success = updateCloudCacheSuccess && !!localCacheEntryPath;
        if (success) {
            terminal.writeLine('Successfully set cache entry.');
            terminal.writeVerboseLine(`Cache key: ${cacheId}`);
        }
        else if (!localCacheEntryPath && updateCloudCacheSuccess) {
            terminal.writeWarningLine('Unable to set local cache entry.');
        }
        else if (localCacheEntryPath && !updateCloudCacheSuccess) {
            terminal.writeWarningLine('Unable to set cloud cache entry.');
        }
        else {
            terminal.writeWarningLine('Unable to set both cloud and local cache entries.');
        }
        return success;
    }
    /**
     * Walks the declared output folders of the project and collects a list of files.
     * @returns The list of output files as project-relative paths, or `undefined` if a
     *   symbolic link was encountered.
     */
    async _tryCollectPathsToCacheAsync(terminal) {
        const projectFolderPath = this._project.projectFolder;
        const outputFilePaths = [];
        const queue = [];
        const filteredOutputFolderNames = [];
        let hasSymbolicLinks = false;
        // Adds child directories to the queue, files to the path list, and bails on symlinks
        function processChildren(relativePath, diskPath, children) {
            for (const child of children) {
                const childRelativePath = `${relativePath}/${child.name}`;
                if (child.isSymbolicLink()) {
                    terminal.writeError(`Unable to include "${childRelativePath}" in build cache. It is a symbolic link.`);
                    hasSymbolicLinks = true;
                }
                else if (child.isDirectory()) {
                    queue.push([childRelativePath, `${diskPath}/${child.name}`]);
                }
                else {
                    outputFilePaths.push(childRelativePath);
                }
            }
        }
        // Handle declared output folders.
        for (const outputFolder of this._projectOutputFolderNames) {
            const diskPath = `${projectFolderPath}/${outputFolder}`;
            try {
                const children = await node_core_library_1.FileSystem.readFolderItemsAsync(diskPath);
                processChildren(outputFolder, diskPath, children);
                // The folder exists, record it
                filteredOutputFolderNames.push(outputFolder);
            }
            catch (error) {
                if (!node_core_library_1.FileSystem.isNotExistError(error)) {
                    throw error;
                }
                // If the folder does not exist, ignore it.
            }
        }
        for (const [relativePath, diskPath] of queue) {
            const children = await node_core_library_1.FileSystem.readFolderItemsAsync(diskPath);
            processChildren(relativePath, diskPath, children);
        }
        if (hasSymbolicLinks) {
            // Symbolic links do not round-trip safely.
            return undefined;
        }
        // Ensure stable output path order.
        outputFilePaths.sort();
        return {
            outputFilePaths,
            filteredOutputFolderNames
        };
    }
    _getTarLogFilePath(cacheId, mode) {
        return path.join(this._project.projectRushTempFolder, `${cacheId}.${mode}.log`);
    }
    static _getCacheId(options) {
        const { buildCacheConfiguration, project: { packageName }, operationStateHash, phaseName } = options;
        return buildCacheConfiguration.getCacheEntryId({
            projectName: packageName,
            projectStateHash: operationStateHash,
            phaseName
        });
    }
}
exports.OperationBuildCache = OperationBuildCache;
//# sourceMappingURL=OperationBuildCache.js.map