From 1863d3fd48f986f851739ceb2e6d7561244c6c40 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Thu, 24 May 2018 17:48:45 -0700 Subject: [PATCH] Graph ordering test WIP --- src/compiler/tsbuild.ts | 201 ++++++++++++++++--------------- src/harness/unittests/tsbuild.ts | 42 +++++++ 2 files changed, 144 insertions(+), 99 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 2b08474f019..4bd6c13914d 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -1,4 +1,9 @@ namespace ts { + /** + * Branded string for keeping track of when we've turned an ambiguous path + * specified like "./blah" to an absolute path to an actual + * tsconfig file, e.g. "/root/blah/tsconfig.json" + */ type ResolvedConfigFileName = string & { _isResolvedConfigFileName: never }; const minimumDate = new Date(-8640000000000000); @@ -6,8 +11,16 @@ namespace ts { /** * A BuildContext tracks what's going on during the course of a build. - * The primary thing we track here is which files were written to, - * but unchanged, because this enables fast downstream updates + * + * Callers may invoke any number of build requests within the same context; + * until the context is reset, each project will only be built at most once. + * + * Example: In a standard setup where project B depends on project A, and both are out of date, + * a failed build of A will result in A remaining out of date. When we try to build + * B, we should immediately bail instead of recomputing A's up-to-date status again. + * + * This also matters for performing fast (i.e. fake) downstream builds of projects + * when their upstream .d.ts files haven't changed content (but have newer timestamps) */ export interface BuildContext { options: BuildOptions; @@ -21,6 +34,9 @@ namespace ts { */ projectStatus: FileMap; + /** + * Issue a verbose diagnostic message. No-ops when options.verbose is false. + */ verbose(diag: DiagnosticMessage, ...args: any[]): void; } @@ -74,86 +90,86 @@ namespace ts { } type UpToDateStatus = - | StatusUnbuildable - | StatusUpToDate - | StatusOutputMissing - | StatusOutOfDateWithSelf - | StatusOutOfDateWithUpstream - | StatusUpstreamOutOfDate - | StatusUpstreamBlocked; + | Status.Unbuildable + | Status.UpToDate + | Status.OutputMissing + | Status.OutOfDateWithSelf + | Status.OutOfDateWithUpstream + | Status.UpstreamOutOfDate + | Status.UpstreamBlocked; - /** - * The project can't be built at all in its current state. For example, - * its config file cannot be parsed, or it has a syntax error or missing file - */ - interface StatusUnbuildable { - type: UpToDateStatusType.Unbuildable; - reason: string; - } - - /** - * The project is up to date with respect to its inputs. - * We track what the newest input file is. - */ - interface StatusUpToDate { - type: UpToDateStatusType.UpToDate | UpToDateStatusType.UpToDateWithUpstreamTypes; - newestInputFileTime: Date; - newestDeclarationFileContentChangedTime: Date; - newestOutputFileTime: Date; - } - - /** - * One or more of the outputs of the project does not exist. - */ - interface StatusOutputMissing { - type: UpToDateStatusType.OutputMissing; + namespace Status { /** - * The name of the first output file that didn't exist + * The project can't be built at all in its current state. For example, + * its config file cannot be parsed, or it has a syntax error or missing file */ - missingOutputFileName: string; - } + export interface Unbuildable { + type: UpToDateStatusType.Unbuildable; + reason: string; + } - /** - * One or more of the project's outputs is older than its newest input. - */ - interface StatusOutOfDateWithSelf { - type: UpToDateStatusType.OutOfDateWithSelf; - outOfDateOutputFileName: string; - newerInputFileName: string; - } + /** + * The project is up to date with respect to its inputs. + * We track what the newest input file is. + */ + export interface UpToDate { + type: UpToDateStatusType.UpToDate | UpToDateStatusType.UpToDateWithUpstreamTypes; + newestInputFileTime: Date; + newestDeclarationFileContentChangedTime: Date; + newestOutputFileTime: Date; + } - /** - * This project depends on an out-of-date project, so shouldn't be built yet - */ - interface StatusUpstreamOutOfDate { - type: UpToDateStatusType.UpstreamOutOfDate; - upstreamProjectName: string; - } + /** + * One or more of the outputs of the project does not exist. + */ + export interface OutputMissing { + type: UpToDateStatusType.OutputMissing; + /** + * The name of the first output file that didn't exist + */ + missingOutputFileName: string; + } - /** - * This project depends an upstream project with build errors - */ - interface StatusUpstreamBlocked { - type: UpToDateStatusType.UpstreamBlocked; - upstreamProjectName: string; - } + /** + * One or more of the project's outputs is older than its newest input. + */ + export interface OutOfDateWithSelf { + type: UpToDateStatusType.OutOfDateWithSelf; + outOfDateOutputFileName: string; + newerInputFileName: string; + } - /** - * One or more of the project's outputs is older than the newest output of - * an upstream project. - */ - interface StatusOutOfDateWithUpstream { - type: UpToDateStatusType.OutOfDateWithUpstream; - outOfDateOutputFileName: string; - newerProjectName: string; + /** + * This project depends on an out-of-date project, so shouldn't be built yet + */ + export interface UpstreamOutOfDate { + type: UpToDateStatusType.UpstreamOutOfDate; + upstreamProjectName: string; + } + + /** + * This project depends an upstream project with build errors + */ + export interface UpstreamBlocked { + type: UpToDateStatusType.UpstreamBlocked; + upstreamProjectName: string; + } + + /** + * One or more of the project's outputs is older than the newest output of + * an upstream project. + */ + export interface OutOfDateWithUpstream { + type: UpToDateStatusType.OutOfDateWithUpstream; + outOfDateOutputFileName: string; + newerProjectName: string; + } } interface FileMap { setValue(fileName: string, value: T): void; getValue(fileName: string): T | never; getValueOrUndefined(fileName: string): T | undefined; - getValueOrDefault(fileName: string, defaultValue: T): T; - tryGetValue(fileName: string): [false, undefined] | [true, T]; } /** @@ -167,8 +183,6 @@ namespace ts { setValue, getValue, getValueOrUndefined, - getValueOrDefault, - tryGetValue }; function setValue(fileName: string, value: T) { @@ -194,26 +208,6 @@ namespace ts { return undefined; } } - - function getValueOrDefault(fileName: string, defaultValue: T): T { - const f = normalizePath(fileName); - if (f in lookup) { - return lookup[f]; - } - else { - return defaultValue; - } - } - - function tryGetValue(fileName: string): [false, undefined] | [true, T] { - const f = normalizePath(fileName); - if (f in lookup) { - return [true as true, lookup[f]]; - } - else { - return [false as false, undefined]; - } - } } export function createDependencyMapper() { @@ -402,13 +396,13 @@ namespace ts { } } - export function createSolutionBuilder(host: CompilerHost, reportDiagnostic: DiagnosticReporter, options: BuildOptions) { + export function createSolutionBuilder(host: CompilerHost, reportDiagnostic: DiagnosticReporter, defaultOptions: BuildOptions) { if (!host.getModifiedTime || !host.setModifiedTime) { throw new Error("Host must support timestamp APIs"); } const configFileCache = createConfigFileCache(host); - let context = createBuildContext(options, reportDiagnostic); + let context = createBuildContext(defaultOptions, reportDiagnostic); return { getUpToDateStatus, @@ -418,8 +412,8 @@ namespace ts { resetBuildContext }; - function resetBuildContext() { - context = createBuildContext(options, reportDiagnostic); + function resetBuildContext(opts = defaultOptions) { + context = createBuildContext(opts, reportDiagnostic); } function getUpToDateStatusOfFile(configFileName: ResolvedConfigFileName): UpToDateStatus { @@ -798,7 +792,7 @@ namespace ts { } if (context.options.dry) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join(""))); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join(""))); } else { if (!host.deleteFile) { @@ -852,7 +846,7 @@ namespace ts { const projName = proj.options.configFilePath; if (status.type === UpToDateStatusType.UpToDate && !context.options.force) { // Up to date, skip - if (options.dry) { + if (defaultOptions.dry) { // In a dry build, inform the user of this fact reportDiagnostic(createCompilerDiagnostic(Diagnostics.Project_0_is_up_to_date, projName)); } @@ -889,6 +883,9 @@ namespace ts { } } + /** + * Report the build ordering inferred from the current project graph if we're in verbose mode + */ function reportBuildQueue(graph: DependencyGraph) { if (!context.options.verbose) return; @@ -902,6 +899,9 @@ namespace ts { context.verbose(Diagnostics.Sorted_list_of_input_projects_Colon_0, names.map(s => "\r\n * " + s).join("")); } + /** + * Report the up-to-date status of a project if we're in verbose mode + */ function reportProjectStatus(configFileName: string, status: UpToDateStatus) { if (!context.options.verbose) return; switch (status.type) { @@ -931,9 +931,12 @@ namespace ts { case UpToDateStatusType.UpstreamBlocked: context.verbose(Diagnostics.Project_0_can_t_be_built_because_it_depends_on_a_project_with_errors, configFileName); return; - + case UpToDateStatusType.Unbuildable: + // TODO different error + context.verbose(Diagnostics.Project_0_can_t_be_built_because_it_depends_on_a_project_with_errors, configFileName); + return; default: - throw new Error(`Invalid build status - ${UpToDateStatusType[status.type]}`); + assertTypeIsNever(status); } } } diff --git a/src/harness/unittests/tsbuild.ts b/src/harness/unittests/tsbuild.ts index f1f38bd2f9f..4d2e5febdef 100644 --- a/src/harness/unittests/tsbuild.ts +++ b/src/harness/unittests/tsbuild.ts @@ -206,6 +206,48 @@ namespace ts { }); }); + describe("tsbuild - graph-ordering", () => { + it("orders the graph correctly", () => { + const fs = new vfs.FileSystem(false); + const host = new fakes.CompilerHost(fs); + const deps: [string, string][] = [ + ["A", "B"], + ["B", "C"], + ["A", "C"], + ["B", "D"], + ["C", "D"], + ["C", "E"], + ["F", "E"] + ]; + + writeProjects(fs, ["A", "B", "C", "D", "E", "F", "G"], deps); + const builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false }); + builder.buildProjects(["/project/A", "/project/G"]); + printDiagnostics(); + }); + + function writeProjects(fileSystem: vfs.FileSystem, projectNames: string[], deps: [string, string][]): string[] { + const projFileNames: string[] = []; + for (const dep of deps) { + if (projectNames.indexOf(dep[0]) < 0) throw new Error(`Invalid dependency - project ${dep[0]} does not exist`); + if (projectNames.indexOf(dep[1]) < 0) throw new Error(`Invalid dependency - project ${dep[1]} does not exist`); + } + for (const proj of projectNames) { + fileSystem.mkdirpSync(`/project/${proj}`); + fileSystem.writeFileSync(`/project/${proj}/${proj}.ts`, "export {}"); + const configFileName = `/project/${proj}/tsconfig.json`; + const configContent = JSON.stringify({ + compilerOptions: { composite: true }, + files: [`./${proj}.ts`], + references: deps.filter(d => d[0] === proj).map(d => ({ path: `../${d[1]}` })) + }, undefined, 2); + fileSystem.writeFileSync(configFileName, configContent); + projFileNames.push(configFileName); + } + return projFileNames; + } + }); + function replaceText(fs: vfs.FileSystem, path: string, oldText: string, newText: string) { if (!fs.statSync(path).isFile()) { throw new Error(`File ${path} does not exist`);