UNPKG

electron-builder-lib

Version:
385 lines (347 loc) 17.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RemoteBuildManager = undefined; var _bluebirdLst; function _load_bluebirdLst() { return _bluebirdLst = require("bluebird-lst"); } var _bluebirdLst2; function _load_bluebirdLst2() { return _bluebirdLst2 = _interopRequireDefault(require("bluebird-lst")); } exports.getConnectOptions = getConnectOptions; exports.checkStatus = checkStatus; var _zipBin; function _load_zipBin() { return _zipBin = require("7zip-bin"); } var _builderUtil; function _load_builderUtil() { return _builderUtil = require("builder-util"); } var _builderUtilRuntime; function _load_builderUtilRuntime() { return _builderUtilRuntime = require("builder-util-runtime"); } var _child_process; function _load_child_process() { return _child_process = require("child_process"); } var _fsExtraP; function _load_fsExtraP() { return _fsExtraP = require("fs-extra-p"); } var _http; function _load_http() { return _http = require("http2"); } var _path = _interopRequireWildcard(require("path")); var _url; function _load_url() { return _url = require("url"); } var _core; function _load_core() { return _core = require("../core"); } var _tools; function _load_tools() { return _tools = require("../targets/tools"); } var _JsonStreamParser; function _load_JsonStreamParser() { return _JsonStreamParser = require("../util/JsonStreamParser"); } var _pathManager; function _load_pathManager() { return _pathManager = require("../util/pathManager"); } var _timer; function _load_timer() { return _timer = require("../util/timer"); } var _remoteBuilderCerts; function _load_remoteBuilderCerts() { return _remoteBuilderCerts = require("./remote-builder-certs"); } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const { HTTP2_HEADER_PATH, HTTP2_METHOD_POST, HTTP2_METHOD_GET, HTTP2_HEADER_METHOD, HTTP2_HEADER_CONTENT_TYPE, HTTP2_HEADER_STATUS, HTTP_STATUS_OK, HTTP_STATUS_BAD_REQUEST } = (_http || _load_http()).constants; const isUseLocalCert = (0, (_builderUtil || _load_builderUtil()).isEnvTrue)(process.env.USE_ELECTRON_BUILD_SERVICE_LOCAL_CA); function getConnectOptions() { const options = {}; const caCert = process.env.ELECTRON_BUILD_SERVICE_CA_CERT; if (caCert !== "false") { if (isUseLocalCert) { (_builderUtil || _load_builderUtil()).log.debug(null, "local certificate authority is used"); } options.ca = caCert || (isUseLocalCert ? (_remoteBuilderCerts || _load_remoteBuilderCerts()).ELECTRON_BUILD_SERVICE_LOCAL_CA_CERT : (_remoteBuilderCerts || _load_remoteBuilderCerts()).ELECTRON_BUILD_SERVICE_CA_CERT); // we cannot issue cert per IP because build agent can be started on demand (and for security reasons certificate authority is offline). // Since own certificate authority is used, it is ok to skip server name verification. options.checkServerIdentity = () => undefined; } return options; } class RemoteBuildManager { constructor(buildServiceEndpoint, projectInfoManager, unpackedDirectory, outDir, packager) { this.buildServiceEndpoint = buildServiceEndpoint; this.projectInfoManager = projectInfoManager; this.unpackedDirectory = unpackedDirectory; this.outDir = outDir; this.packager = packager; this.files = null; this.finishedStreamCount = 0; (_builderUtil || _load_builderUtil()).log.debug({ endpoint: buildServiceEndpoint }, "connect to remote build service"); this.client = (0, (_http || _load_http()).connect)(buildServiceEndpoint, getConnectOptions()); } build(customHeaders) { return new (_bluebirdLst2 || _load_bluebirdLst2()).default((resolve, reject) => { const client = this.client; client.on("socketError", reject); client.on("error", reject); let handled = false; client.once("close", () => { if (!handled) { reject(new Error("Closed unexpectedly")); } }); client.once("timeout", () => { reject(new Error("Timeout")); }); this.doBuild(customHeaders).then(result => { handled = true; resolve(result); }).catch(reject); }).finally(() => { this.client.destroy(); }); } doBuild(customHeaders) { var _this = this; return (0, (_bluebirdLst || _load_bluebirdLst()).coroutine)(function* () { const id = yield _this.upload(customHeaders); const result = yield _this.listenEvents(id); yield new (_bluebirdLst2 || _load_bluebirdLst2()).default(function (resolve, reject) { const stream = _this.client.request({ [HTTP2_HEADER_PATH]: `/v1/complete/${id}`, [HTTP2_HEADER_METHOD]: HTTP2_METHOD_GET }); stream.on("error", reject); stream.on("response", function (headers) { try { const status = headers[HTTP2_HEADER_STATUS]; if (!checkStatus(status, reject)) { (_builderUtil || _load_builderUtil()).log.warn(`Not critical server error: ${status}`); } } finally { resolve(); } }); }); return result; })(); } upload(customHeaders) { return new (_bluebirdLst2 || _load_bluebirdLst2()).default((resolve, reject) => { const zstdCompressionLevel = getZstdCompressionLevel(this.buildServiceEndpoint); const stream = this.client.request(Object.assign({ [HTTP2_HEADER_PATH]: "/v1/upload", [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST, [HTTP2_HEADER_CONTENT_TYPE]: "application/octet-stream" }, customHeaders, { // only for stats purpose, not required for build "x-zstd-compression-level": zstdCompressionLevel })); stream.on("error", reject); // this.handleStreamEvent(resolve, reject) this.uploadUnpackedAppArchive(stream, zstdCompressionLevel, reject); stream.on("response", headers => { const status = headers[HTTP2_HEADER_STATUS]; if (status !== HTTP_STATUS_OK && status !== HTTP_STATUS_BAD_REQUEST) { reject(new (_builderUtilRuntime || _load_builderUtilRuntime()).HttpError(status)); return; } let data = ""; stream.setEncoding("utf8"); stream.on("data", chunk => { data += chunk; }); stream.on("end", () => { const result = data.length === 0 ? {} : JSON.parse(data); (_builderUtil || _load_builderUtil()).log.debug({ result: JSON.stringify(result, null, 2) }, `remote builder result`); if (status === HTTP_STATUS_BAD_REQUEST) { reject(new (_builderUtilRuntime || _load_builderUtilRuntime()).HttpError(status, JSON.stringify(result, null, 2))); return; } const id = result.id; if (id == null) { reject(new Error("Server didn't return id")); return; } // cannot connect immediately because channel status is not yet created resolve(id); }); }); }); } listenEvents(id) { return new (_bluebirdLst2 || _load_bluebirdLst2()).default((resolve, reject) => { const stream = this.client.request({ [HTTP2_HEADER_PATH]: `/v1/status/${id}`, [HTTP2_HEADER_METHOD]: HTTP2_METHOD_GET }); stream.on("error", reject); stream.on("response", headers => { if (!checkStatus(headers[HTTP2_HEADER_STATUS], reject)) { return; } stream.setEncoding("utf8"); const eventSource = new (_JsonStreamParser || _load_JsonStreamParser()).JsonStreamParser(data => { if ((_builderUtil || _load_builderUtil()).log.isDebugEnabled) { (_builderUtil || _load_builderUtil()).log.debug({ event: JSON.stringify(data, null, 2) }, "remote builder event"); } const error = data.error; if (error != null) { stream.destroy(); resolve(data); return; } if (data.state != null) { let message = data.state; switch (data.state) { case "added": message = "job added to build queue"; break; case "started": message = "job started"; break; } (_builderUtil || _load_builderUtil()).log.info({ status: message }, "remote building"); return; } if (!("files" in data)) { (_builderUtil || _load_builderUtil()).log.warn(`Unknown builder event: ${JSON.stringify(data)}`); return; } // close, no more events are expected stream.destroy(); this.files = data.files; for (const artifact of this.files) { (_builderUtil || _load_builderUtil()).log.info({ file: artifact.file }, `downloading remote build artifact`); this.downloadFile(id, artifact, resolve, reject); } }); stream.on("data", chunk => eventSource.parseIncoming(chunk)); }); }); } downloadFile(id, artifact, resolve, reject) { const downloadTimer = new (_timer || _load_timer()).DevTimer("compress and upload"); const localFile = _path.join(this.outDir, artifact.file); const artifactCreatedEvent = this.artifactInfoToArtifactCreatedEvent(artifact, localFile); // use URL to encode path properly (critical for filenames with unicode symbols, e.g. "boo-Test App ßW") const fileUrlPath = `/v1/download${new (_url || _load_url()).URL(`f:/${id}/${artifact.file}`).pathname}`; const fileWritten = () => { this.finishedStreamCount++; (_builderUtil || _load_builderUtil()).log.info({ time: downloadTimer.endAndGet(), file: artifact.file }, "downloaded remote build artifact"); (_builderUtil || _load_builderUtil()).log.debug({ file: localFile }, "saved remote build artifact"); // PublishManager uses outDir and options, real (the same as for local build) values must be used this.projectInfoManager.packager.dispatchArtifactCreated(artifactCreatedEvent); if (this.files != null && this.finishedStreamCount >= this.files.length) { resolve(null); } }; const isShort = artifact.file.endsWith(".yml") || artifact.file.endsWith(".json"); if (!isShort) { // --ca-certificate This option is only available when aria2 was compiled against GnuTLS or OpenSSL. WinTLS and AppleTLS will always use the system certificate store. Instead of `--ca-certificate install the certificate in that store. // so, we have to use --check-certificate false (0, (_tools || _load_tools()).getAria)().then(aria2c => { return (0, (_builderUtil || _load_builderUtil()).spawn)(aria2c, ["--max-connection-per-server=4", "--min-split-size=5M", "--retry-wait=3", `--ca-certificate=${(0, (_pathManager || _load_pathManager()).getTemplatePath)(isUseLocalCert ? "local-ca.crt" : "ca.crt")}`, "--check-certificate=false", "--min-tls-version=TLSv1.2", "--console-log-level=warn", "--download-result=full", `--dir=${this.outDir}`, `${this.buildServiceEndpoint}${fileUrlPath}`], { cwd: this.outDir, stdio: ["ignore", "inherit", "inherit"] }); }).then(fileWritten).catch(reject); return; } const stream = this.client.request({ [HTTP2_HEADER_PATH]: fileUrlPath, [HTTP2_HEADER_METHOD]: HTTP2_METHOD_GET }); stream.on("error", reject); stream.on("response", headers => { if (!checkStatus(headers[HTTP2_HEADER_STATUS], reject)) { return; } const buffers = []; stream.on("end", () => { const fileContent = buffers.length === 1 ? buffers[0] : Buffer.concat(buffers); artifactCreatedEvent.fileContent = fileContent; (0, (_fsExtraP || _load_fsExtraP()).outputFile)(localFile, fileContent).then(fileWritten).catch(reject); }); stream.on("data", chunk => { buffers.push(chunk); }); }); } artifactInfoToArtifactCreatedEvent(artifact, localFile) { const target = artifact.target; // noinspection SpellCheckingInspection return Object.assign({}, artifact, { file: localFile, target: target == null ? null : new FakeTarget(target, this.outDir, this.packager.config[target]), packager: this.packager }); } // compress and upload in the same time, directly to remote without intermediate local file uploadUnpackedAppArchive(stream, zstdCompressionLevel, reject) { const packager = this.projectInfoManager.packager; const buildResourcesDir = packager.buildResourcesDir; if (buildResourcesDir === packager.projectDir) { reject(new Error(`Build resources dir equals to project dir and so, not sent to remote build agent. It will lead to incorrect results.\nPlease set "directories.buildResources" to separate dir or leave default ("build" directory in the project root)`)); return; } (_bluebirdLst2 || _load_bluebirdLst2()).default.all([this.projectInfoManager.infoFile.value, (0, (_tools || _load_tools()).getZstd)()]).then(results => { const infoFile = results[0]; (_builderUtil || _load_builderUtil()).log.info("compressing and uploading to remote builder"); const compressAndUploadTimer = new (_timer || _load_timer()).DevTimer("compress and upload"); // noinspection SpellCheckingInspection const tarProcess = (0, (_child_process || _load_child_process()).spawn)((_zipBin || _load_zipBin()).path7za, ["a", "dummy", "-ttar", "-so", this.unpackedDirectory, infoFile, buildResourcesDir], { stdio: ["pipe", "pipe", process.stderr] }); tarProcess.stdout.on("error", reject); const zstdProcess = (0, (_child_process || _load_child_process()).spawn)(results[1], [`-${zstdCompressionLevel}`, "--long"], { stdio: ["pipe", "pipe", process.stderr] }); zstdProcess.on("error", reject); tarProcess.stdout.pipe(zstdProcess.stdin); zstdProcess.stdout.pipe(stream); zstdProcess.stdout.on("end", () => { (_builderUtil || _load_builderUtil()).log.info({ time: compressAndUploadTimer.endAndGet() }, "uploaded to remote builder"); }); }).catch(reject); } } exports.RemoteBuildManager = RemoteBuildManager; function getZstdCompressionLevel(endpoint) { const result = process.env.ELECTRON_BUILD_SERVICE_ZSTD_COMPRESSION; if (result != null) { return result; } // 18 - 40s // 17 - 30s // 16 - 20s return endpoint.startsWith("https://127.0.0.1:") || endpoint.startsWith("https://localhost:") || endpoint.startsWith("[::1]:") ? "3" : "16"; } function checkStatus(status, reject) { if (status === HTTP_STATUS_OK) { return true; } else { reject(new (_builderUtilRuntime || _load_builderUtilRuntime()).HttpError(status)); return false; } } class FakeTarget extends (_core || _load_core()).Target { constructor(name, outDir, options) { super(name); this.outDir = outDir; this.options = options; } build(appOutDir, arch) { // no build return (0, (_bluebirdLst || _load_bluebirdLst()).coroutine)(function* () {})(); } } //# sourceMappingURL=RemoteBuildManager.js.map