UNPKG

appcenter-cli

Version:

Command line tool for Visual Studio App Center

246 lines (216 loc) 10.3 kB
import { AppCenterClient } from "../../util/apis"; import { AppCommand, CommandResult } from "../../util/commandline"; import { ErrorCodes, failure, success } from "../../util/commandline"; import { hasArg, help, longName, shortName } from "../../util/commandline"; import { out } from "../../util/interaction"; import { inspect } from "util"; import { DefaultApp } from "../../util/profile"; import UploadSymbolsHelper from "./lib/symbols-uploading-helper"; import { getSymbolsZipFromXcarchive, packDsymParentFolderContents, getChildrenDsymFolderPaths } from "./lib/subfolder-symbols-helper"; import { createTempFileFromZip } from "./lib/temp-zip-file-helper"; import { SymbolType } from "./lib/symbols-uploading-helper"; import * as Fs from "fs"; import * as Pfs from "../../util/misc/promisfied-fs"; import * as Path from "path"; import * as JsZip from "jszip"; import * as JsZipHelper from "../../util/misc/jszip-helper"; import * as _ from "lodash"; const debug = require("debug")("appcenter-cli:commands:apps:crashes:upload-symbols"); enum SymbolFsEntryType { Unknown, DsymFolder, DsymParentFolder, XcArchive, ZipFile } @help("Upload the crash symbols for the application") export default class UploadSymbols extends AppCommand { @help("Path to a dSYM package, a directory containing dSYM packages, or a zip file containing the dSYM packages.") @shortName("s") @longName("symbol") @hasArg public symbolsPath: string; @help("Path to a xcarchive package") @shortName("x") @longName("xcarchive") @hasArg public xcarchivePath: string; @help("Path to a React Native sourcemap file. Only supported in combination with --symbol or --xcarchive") @shortName("m") @longName("sourcemap") @hasArg public sourceMapPath: string; @help("Path to a zip file containing Breakpad symbols.") @shortName("b") @longName("breakpad") @hasArg public breakpadPath: string; public async run(client: AppCenterClient): Promise<CommandResult> { const app: DefaultApp = this.app; this.validateParameters(); let zip: JsZip | string; // it is either JsZip object or path to ZIP file let symbolType: SymbolType; if (!_.isNil(this.symbolsPath)) { // processing -s switch value zip = await out.progress("Preparing ZIP with symbols...", this.prepareZipFromSymbols(this.symbolsPath)); symbolType = SymbolType.Apple; } else if (!_.isNil(this.breakpadPath)) { zip = await out.progress("Preparing ZIP with Breakpad symbols...", this.prepareZipFromSymbols(this.breakpadPath)); symbolType = SymbolType.Breakpad; } else { // process -x switch value zip = await out.progress("Preparing ZIP with symbols from xcarchive...", this.prepareZipFromXcArchive(this.xcarchivePath)); symbolType = SymbolType.Apple; } // process -m switch if specified if (!_.isNil(this.sourceMapPath)) { // load current ZIP, add/replace symbol file, return stream to new zip zip = await out.progress("Adding source map file to ZIP...", this.addSourceMapFileToZip(this.sourceMapPath, zip)); } let pathToZipToUpload: string; if (typeof(zip) === "string") { // path to zip can be passed as it is pathToZipToUpload = zip; } else { // JsZip object should be written to temp file first pathToZipToUpload = await createTempFileFromZip(zip); } // upload symbols await out.progress("Uploading symbols...", new UploadSymbolsHelper(client, app, debug).uploadSymbolsZip(pathToZipToUpload, symbolType)); return success(); } private getStatsForFsPath(filePath: string): Fs.Stats | null { // take fs entry stats (and check it's existence BTW) try { debug(`Getting FS statistics for ${filePath}`); return Fs.statSync(filePath); } catch (error) { if (error.code === "ENOENT") { // path points to non-existing file system entry throw failure(ErrorCodes.InvalidParameter, `path ${filePath} points to non-existent item`); } else { // other errors debug(`Failed to get statistics for file system entry ${filePath} - ${inspect(error)}`); throw failure(ErrorCodes.Exception, `failed to get statistics for file system entry ${filePath}`); } } } private detectSymbolsFsEntryType(filePath: string, fsEntryStats: Fs.Stats): SymbolFsEntryType { if (fsEntryStats.isDirectory()) { // check if it is a dSYM or xcarchive directory switch (this.getLowerCasedFileExtension(filePath)) { case ".dsym": return SymbolFsEntryType.DsymFolder; case ".xcarchive": return SymbolFsEntryType.XcArchive; default: // test if folder contains .dsym sub-folders return getChildrenDsymFolderPaths(filePath, debug).length > 0 ? SymbolFsEntryType.DsymParentFolder : SymbolFsEntryType.Unknown; } } else if (fsEntryStats.isFile()) { // check if it is a ZIP file return this.getLowerCasedFileExtension(filePath) === ".zip" ? SymbolFsEntryType.ZipFile : SymbolFsEntryType.Unknown; } // everything else return SymbolFsEntryType.Unknown; } private getLowerCasedFileExtension(filePath: string): string { return Path.extname(filePath).toLowerCase(); } private async packDsymFolder(pathToFolder: string): Promise<JsZip> { debug(`Compressing the specified folder ${pathToFolder} to the in-memory ZIP archive`); const zipArchive = new JsZip(); try { await JsZipHelper.addFolderToZipRecursively(pathToFolder, zipArchive); } catch (error) { debug(`Unable to add folder ${pathToFolder} to the ZIP archive - ${inspect(error)}`); throw failure(ErrorCodes.Exception, `unable to add folder ${pathToFolder} to the ZIP archive`); } return zipArchive; } private async prepareZipFromSymbols(path: string): Promise<JsZip | string> { debug("Trying to prepare ZIP file from symbols"); const fsEntryStats = this.getStatsForFsPath(path); const symbolsType = this.detectSymbolsFsEntryType(path, fsEntryStats); switch (symbolsType) { case SymbolFsEntryType.DsymFolder: // dSYM Folder needs to be packed to the temp ZIP before uploading return await this.packDsymFolder(path); case SymbolFsEntryType.DsymParentFolder: // only child DSYM folders should be compressed return await packDsymParentFolderContents(path, debug); case SymbolFsEntryType.ZipFile: // *.ZIP file can be uploaded as it is return path; default: // file doesn't points to correct symbols throw failure(ErrorCodes.InvalidParameter, `${path} is not a valid symbols file/directory`); } } private validateParameters() { // check that user have selected either --symbol or --xcarchive if (_.isNil(this.symbolsPath) && _.isNil(this.xcarchivePath) && _.isNil(this.breakpadPath)) { throw failure(ErrorCodes.InvalidParameter, "specify either '--symbol', '--xcarchive', or '--breakpad' switch"); } else if (!_.isNil(this.symbolsPath) && !_.isNil(this.xcarchivePath)) { throw failure(ErrorCodes.InvalidParameter, "'--symbol' and '--xcarchive' switches are mutually exclusive"); } else if (!_.isNil(this.symbolsPath) && !_.isNil(this.breakpadPath)) { throw failure(ErrorCodes.InvalidParameter, "'--symbol' and '--breakpad' switches are mutually exclusive"); } else if (!_.isNil(this.xcarchivePath) && !_.isNil(this.breakpadPath)) { throw failure(ErrorCodes.InvalidParameter, "'--xcarchive' and '--breakpad' switches are mutually exclusive"); } else if (!_.isNil(this.breakpadPath) && !_.isNil(this.sourceMapPath)) { throw failure(ErrorCodes.InvalidParameter, "'--breakpad' and '--sourcemap' switches are mutually exclusive"); } } private async addSourceMapFileToZip(path: string, zip: JsZip | string): Promise<JsZip> { debug("Checking if the specified mappings file is valid"); // checking if it points to the *.map file if (this.getLowerCasedFileExtension(path) !== ".map") { throw failure(ErrorCodes.InvalidParameter, `${path} is not a map file`); } // getting statistics for the map file const sourceMapFileStats: Fs.Stats = this.getStatsForFsPath(path); // checking if source map file is actually a file if (!sourceMapFileStats.isFile()) { throw failure(ErrorCodes.InvalidParameter, `${path} is not a file`); } let zipToChange: JsZip; if (typeof(zip) === "string") { // loading ZIP to add file to it debug("Loading ZIP into the memory to add files"); try { const mapFileContent = await Pfs.readFile(zip); zipToChange = await new JsZip().loadAsync(mapFileContent); } catch (error) { debug(`Failed to load ZIP ${zip} - ${inspect(error)}`); throw failure(ErrorCodes.Exception, `failed to load ZIP ${zip}`); } } else { // ZIP is already loaded, working with it zipToChange = zip; } // adding (or replacing) source map file const sourceMapFileBaseName = Path.basename(path); debug(zipToChange.file(sourceMapFileBaseName) ? "Replacing existing mappings file with the same name in the ZIP" : "Adding the specified mappings file to the ZIP"); try { const sourceMapFileBuffer = await Pfs.readFile(path); zipToChange.file(sourceMapFileBaseName, sourceMapFileBuffer); return zipToChange; } catch (error) { debug(`Unable to add file ${path} to the ZIP - ${inspect(error)}`); throw failure(ErrorCodes.Exception, `unable to add file ${path} to the ZIP`); } } private async prepareZipFromXcArchive(path: string): Promise<JsZip> { debug(`Trying to prepare the ZIP archive with symbols from .xcarchive folder`); const fsEntryStats = this.getStatsForFsPath(path); const symbolsType = this.detectSymbolsFsEntryType(path, fsEntryStats); switch (symbolsType) { case SymbolFsEntryType.XcArchive: // the DSYM folders from "*.xcarchive/dSYMs" should be compressed return await getSymbolsZipFromXcarchive(path, debug); default: // file doesn't points to correct .xcarchive throw failure(ErrorCodes.InvalidParameter, `${path} is not a valid XcArchive folder`); } } }