|
7 | 7 | */
|
8 | 8 |
|
9 | 9 | /// <reference types='node'/>
|
10 |
| -import {spawn, spawnSync} from 'child_process'; |
11 |
| -import {Observable, Subject} from 'rxjs'; |
| 10 | + |
| 11 | +import {Path, basename, dirname, getSystemPath, join} from '@angular-devkit/core'; |
| 12 | +import {resolve} from '@angular-devkit/core/node'; |
| 13 | +import {Host} from '@angular-devkit/core/src/virtual-fs/host'; |
| 14 | +import {spawn} from 'child_process'; |
12 | 15 |
|
13 | 16 | export type Executable = 'bazel' | 'ibazel';
|
14 | 17 | export type Command = 'build' | 'test' | 'run' | 'coverage' | 'query';
|
15 | 18 |
|
| 19 | +/** |
| 20 | + * Spawn the Bazel process. Trap SINGINT to make sure Bazel process is killed. |
| 21 | + */ |
16 | 22 | export function runBazel(
|
17 |
| - projectDir: string, executable: Executable, command: Command, workspaceTarget: string, |
18 |
| - flags: string[]): Observable<void> { |
19 |
| - const doneSubject = new Subject<void>(); |
20 |
| - const bin = require.resolve(`@bazel/${executable}`); |
21 |
| - const buildProcess = spawn(bin, [command, workspaceTarget, ...flags], { |
22 |
| - cwd: projectDir, |
23 |
| - stdio: 'inherit', |
24 |
| - shell: false, |
25 |
| - }); |
| 23 | + projectDir: Path, binary: Path, command: Command, workspaceTarget: string, flags: string[]) { |
| 24 | + return new Promise((resolve, reject) => { |
| 25 | + const buildProcess = spawn(getSystemPath(binary), [command, workspaceTarget, ...flags], { |
| 26 | + cwd: getSystemPath(projectDir), |
| 27 | + stdio: 'inherit', |
| 28 | + shell: false, |
| 29 | + }); |
26 | 30 |
|
27 |
| - buildProcess.once('close', (code: number) => { |
28 |
| - if (code === 0) { |
29 |
| - doneSubject.next(); |
30 |
| - } else { |
31 |
| - doneSubject.error(`${executable} failed with code ${code}.`); |
32 |
| - } |
33 |
| - }); |
| 31 | + process.on('SIGINT', (signal) => { |
| 32 | + if (!buildProcess.killed) { |
| 33 | + buildProcess.kill(); |
| 34 | + reject(new Error(`Bazel process received ${signal}.`)); |
| 35 | + } |
| 36 | + }); |
34 | 37 |
|
35 |
| - return doneSubject.asObservable(); |
| 38 | + buildProcess.once('close', (code: number) => { |
| 39 | + if (code === 0) { |
| 40 | + resolve(); |
| 41 | + } else { |
| 42 | + reject(new Error(`${basename(binary)} failed with code ${code}.`)); |
| 43 | + } |
| 44 | + }); |
| 45 | + }); |
36 | 46 | }
|
37 | 47 |
|
38 |
| -export function checkInstallation(executable: Executable, projectDir: string) { |
39 |
| - let bin: string; |
| 48 | +/** |
| 49 | + * Resolves the path to `@bazel/bazel` or `@bazel/ibazel`. |
| 50 | + */ |
| 51 | +export function checkInstallation(name: Executable, projectDir: Path): string { |
| 52 | + const packageName = `@bazel/${name}`; |
40 | 53 | try {
|
41 |
| - bin = require.resolve(`@bazel/${executable}`); |
42 |
| - } catch { |
43 |
| - return false; |
| 54 | + return resolve(packageName, { |
| 55 | + basedir: projectDir, |
| 56 | + }); |
| 57 | + } catch (error) { |
| 58 | + if (error.code === 'MODULE_NOT_FOUND') { |
| 59 | + throw new Error( |
| 60 | + `Could not run ${name}. Please make sure that the ` + |
| 61 | + `"${name}" command is installed by running ` + |
| 62 | + `"npm install ${packageName}" or "yarn install ${packageName}".`); |
| 63 | + } |
| 64 | + throw error; |
44 | 65 | }
|
45 |
| - const child = spawnSync(bin, ['version'], { |
46 |
| - cwd: projectDir, |
47 |
| - shell: false, |
| 66 | +} |
| 67 | + |
| 68 | +/** |
| 69 | + * Returns the absolute path to the template directory in `@angular/bazel`. |
| 70 | + */ |
| 71 | +export async function getTemplateDir(host: Host, root: Path): Promise<Path> { |
| 72 | + const packageJson = resolve('@angular/bazel', { |
| 73 | + basedir: root, |
| 74 | + resolvePackageJson: true, |
48 | 75 | });
|
49 |
| - return child.status === 0; |
| 76 | + const packageDir = dirname(packageJson as Path); |
| 77 | + const templateDir = join(packageDir, 'src', 'builders', 'files'); |
| 78 | + if (!await host.isDirectory(templateDir).toPromise()) { |
| 79 | + throw new Error('Could not find Bazel template directory in "@angular/bazel".'); |
| 80 | + } |
| 81 | + return templateDir; |
| 82 | +} |
| 83 | + |
| 84 | +/** |
| 85 | + * Recursively list the specified 'dir' using depth-first approach. Paths |
| 86 | + * returned are relative to 'dir'. |
| 87 | + */ |
| 88 | +function listR(host: Host, dir: Path): Promise<Path[]> { |
| 89 | + async function list(dir: Path, root: Path, results: Path[]) { |
| 90 | + const paths = await host.list(dir).toPromise(); |
| 91 | + for (const path of paths) { |
| 92 | + const absPath = join(dir, path); |
| 93 | + const relPath = join(root, path); |
| 94 | + if (await host.isFile(absPath).toPromise()) { |
| 95 | + results.push(relPath); |
| 96 | + } else { |
| 97 | + await list(absPath, relPath, results); |
| 98 | + } |
| 99 | + } |
| 100 | + return results; |
| 101 | + } |
| 102 | + |
| 103 | + return list(dir, '' as Path, []); |
| 104 | +} |
| 105 | + |
| 106 | +/** |
| 107 | + * Copy the file from 'source' to 'dest'. |
| 108 | + */ |
| 109 | +async function copyFile(host: Host, source: Path, dest: Path) { |
| 110 | + const buffer = await host.read(source).toPromise(); |
| 111 | + await host.write(dest, buffer).toPromise(); |
| 112 | +} |
| 113 | + |
| 114 | +/** |
| 115 | + * Copy Bazel files (WORKSPACE, BUILD.bazel, etc) from the template directory to |
| 116 | + * the project `root` directory, and return the absolute paths of the files |
| 117 | + * copied, so that they can be deleted later. |
| 118 | + * Existing files in `root` will not be replaced. |
| 119 | + */ |
| 120 | +export async function copyBazelFiles(host: Host, root: Path, templateDir: Path) { |
| 121 | + const bazelFiles: Path[] = []; |
| 122 | + const templates = await listR(host, templateDir); |
| 123 | + |
| 124 | + await Promise.all(templates.map(async(template) => { |
| 125 | + const name = template.replace('__dot__', '.').replace('.template', ''); |
| 126 | + const source = join(templateDir, template); |
| 127 | + const dest = join(root, name); |
| 128 | + try { |
| 129 | + const exists = await host.exists(dest).toPromise(); |
| 130 | + if (!exists) { |
| 131 | + await copyFile(host, source, dest); |
| 132 | + bazelFiles.push(dest); |
| 133 | + } |
| 134 | + } catch { |
| 135 | + } |
| 136 | + })); |
| 137 | + |
| 138 | + return bazelFiles; |
| 139 | +} |
| 140 | + |
| 141 | +/** |
| 142 | + * Delete the specified 'files' and return a promise that always resolves. |
| 143 | + */ |
| 144 | +export function deleteBazelFiles(host: Host, files: Path[]) { |
| 145 | + return Promise.all(files.map(async(file) => { |
| 146 | + try { |
| 147 | + await host.delete(file).toPromise(); |
| 148 | + } catch { |
| 149 | + } |
| 150 | + })); |
50 | 151 | }
|
0 commit comments