From ddec41eddfd434c836c77f377b25cf207cc32151 Mon Sep 17 00:00:00 2001 From: Derek Cormier Date: Fri, 10 Dec 2021 09:43:08 -0800 Subject: [PATCH] build: add parallel script to build using bazel --- package.json | 1 + scripts/build-bazel.ts | 180 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 scripts/build-bazel.ts diff --git a/package.json b/package.json index 92a0b17731..0437d44a64 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "admin": "node ./bin/devkit-admin", "bazel:test": "bazel test //packages/...", "build": "node ./bin/devkit-admin build", + "build:bazel": "node ./bin/devkit-admin build-bazel", "build-tsc": "tsc -p tsconfig.json", "lint": "eslint --cache --max-warnings=0 \"**/*.ts\"", "templates": "node ./bin/devkit-admin templates", diff --git a/scripts/build-bazel.ts b/scripts/build-bazel.ts new file mode 100644 index 0000000000..8a62137bda --- /dev/null +++ b/scripts/build-bazel.ts @@ -0,0 +1,180 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { logging } from '@angular-devkit/core'; +import { spawn } from 'child_process'; +import fs from 'fs'; +import { dirname, join, relative, resolve } from 'path'; + +const baseDir = resolve(`${__dirname}/..`); +const bazelCmd = process.env.BAZEL ?? `yarn --cwd "${baseDir}" --silent bazel`; +const distRoot = join(baseDir, '/dist'); + +type BuildMode = 'local' | 'snapshot' | 'release'; + +function _copy(from: string, to: string) { + // Create parent folder if necessary. + if (!fs.existsSync(dirname(to))) { + fs.mkdirSync(dirname(to), { recursive: true }); + } + + // Error out if destination already exists. + if (fs.existsSync(to)) { + throw new Error(`Path ${to} already exist...`); + } + + from = relative(process.cwd(), from); + to = relative(process.cwd(), to); + + const buffer = fs.readFileSync(from); + fs.writeFileSync(to, buffer); +} + +function _recursiveCopy(from: string, to: string, logger: logging.Logger) { + if (!fs.existsSync(from)) { + logger.error(`File "${from}" does not exist.`); + process.exit(4); + } + if (fs.statSync(from).isDirectory()) { + fs.readdirSync(from).forEach((fileName) => { + _recursiveCopy(join(from, fileName), join(to, fileName), logger); + }); + } else { + _copy(from, to); + } +} + +function rimraf(location: string) { + // The below should be removed and replace with just `rmSync` when support for Node.Js 12 is removed. + const { rmSync, rmdirSync } = fs as typeof fs & { + rmSync?: ( + path: fs.PathLike, + options?: { + force?: boolean; + maxRetries?: number; + recursive?: boolean; + retryDelay?: number; + }, + ) => void; + }; + + if (rmSync) { + rmSync(location, { force: true, recursive: true, maxRetries: 3 }); + } else { + rmdirSync(location, { recursive: true, maxRetries: 3 }); + } +} + +function _clean(logger: logging.Logger) { + logger.info('Cleaning...'); + logger.info(' Removing dist/...'); + rimraf(join(__dirname, '../dist')); +} + +function _exec(cmd: string, captureStdout: boolean, logger: logging.Logger): Promise { + return new Promise((resolve, reject) => { + const proc = spawn(cmd, { + stdio: 'pipe', + shell: true, + }); + + let output = ''; + proc.stdout.on('data', (data) => { + logger.info(data.toString().trim()); + if (captureStdout) { + output += data.toString().trim(); + } + }); + proc.stderr.on('data', (data) => logger.info(data.toString().trim())); + + proc.on('error', (error) => { + logger.error(error.message); + }); + + proc.on('exit', (status) => { + if (status !== 0) { + reject(`Command failed: ${cmd}`); + } else { + resolve(output); + } + }); + }); +} + +async function _build(logger: logging.Logger, mode: BuildMode): Promise { + logger.info(`Building (mode=${mode})...`); + + const queryLogger = logger.createChild('query'); + const queryTargetsCmd = `${bazelCmd} query --output=label "attr(name, npm_package_archive, //packages/...)"`; + const targets = (await _exec(queryTargetsCmd, true, queryLogger)).split(/\r?\n/); + + let configArg = ''; + if (mode === 'snapshot') { + configArg = '--config=snapshot'; + } else if (mode === 'release') { + configArg = '--config=release'; + } + + const buildLogger = logger.createChild('build'); + + // If we are in release mode, run `bazel clean` to ensure the execroot and action cache + // are not populated. This is necessary because targets using `npm_package` rely on + // workspace status variables for the package version. Such NPM package targets are not + // rebuilt if only the workspace status variables change. This could result in accidental + // re-use of previously built package output with a different `version` in the `package.json`. + if (mode == 'release') { + buildLogger.info('Building in release mode. Resetting the Bazel execroot and action cache..'); + await _exec(`${bazelCmd} clean`, false, buildLogger); + } + + await _exec(`${bazelCmd} build ${configArg} ${targets.join(' ')}`, false, buildLogger); + + return targets; +} + +export default async function ( + argv: { local?: boolean; snapshot?: boolean } = {}, + logger: logging.Logger = new logging.Logger('build-logger'), +): Promise<{ name: string; outputPath: string }[]> { + const bazelBin = await _exec(`${bazelCmd} info bazel-bin`, true, logger); + + _clean(logger); + + let buildMode: BuildMode; + if (argv.local) { + buildMode = 'local'; + } else if (argv.snapshot) { + buildMode = 'snapshot'; + } else { + buildMode = 'release'; + } + + const targets = await _build(logger, buildMode); + const output: { name: string; outputPath: string }[] = []; + + logger.info('Moving packages and tars to dist/'); + const packageLogger = logger.createChild('packages'); + + for (const target of targets) { + const packageDir = target.replace(/\/\/packages\/(.*):npm_package_archive/, '$1'); + const bazelOutDir = join(bazelBin, 'packages', packageDir, 'npm_package'); + const tarPath = `${bazelBin}/packages/${packageDir}/npm_package_archive.tar.gz`; + const packageJsonPath = `${bazelOutDir}/package.json`; + const packageName = require(packageJsonPath).name; + const destDir = `${distRoot}/${packageName}`; + + packageLogger.info(packageName); + + _recursiveCopy(bazelOutDir, destDir, logger); + _copy(tarPath, `${distRoot}/${packageName.replace('@', '_').replace('/', '_')}.tgz`); + + output.push({ name: packageDir, outputPath: destDir }); + } + + return output; +}