Skip to content

Commit 349eab1

Browse files
committed
refactor(project): Implement basic incremental build functionality
Cherry-picked from: SAP/ui5-project@cb4e858 JIRA: CPOUI5FOUNDATION-1174
1 parent 796f38c commit 349eab1

File tree

18 files changed

+1695
-190
lines changed

18 files changed

+1695
-190
lines changed

packages/project/lib/build/ProjectBuilder.js

Lines changed: 124 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,11 @@ class ProjectBuilder {
139139
async build({
140140
destPath, cleanDest = false,
141141
includedDependencies = [], excludedDependencies = [],
142-
dependencyIncludes
142+
dependencyIncludes,
143+
cacheDir,
144+
watch,
143145
}) {
144-
if (!destPath) {
146+
if (!destPath && !watch) {
145147
throw new Error(`Missing parameter 'destPath'`);
146148
}
147149
if (dependencyIncludes) {
@@ -177,12 +179,15 @@ class ProjectBuilder {
177179
}
178180
}
179181

180-
const projectBuildContexts = await this._createRequiredBuildContexts(requestedProjects);
182+
const projectBuildContexts = await this._createRequiredBuildContexts(requestedProjects, cacheDir);
181183
const cleanupSigHooks = this._registerCleanupSigHooks();
182-
const fsTarget = resourceFactory.createAdapter({
183-
fsBasePath: destPath,
184-
virBasePath: "/"
185-
});
184+
let fsTarget;
185+
if (destPath) {
186+
fsTarget = resourceFactory.createAdapter({
187+
fsBasePath: destPath,
188+
virBasePath: "/"
189+
});
190+
}
186191

187192
const queue = [];
188193
const alreadyBuilt = [];
@@ -196,7 +201,7 @@ class ProjectBuilder {
196201
// => This project needs to be built or, in case it has already
197202
// been built, it's build result needs to be written out (if requested)
198203
queue.push(projectBuildContext);
199-
if (!projectBuildContext.requiresBuild()) {
204+
if (!await projectBuildContext.requiresBuild()) {
200205
alreadyBuilt.push(projectName);
201206
}
202207
}
@@ -220,8 +225,12 @@ class ProjectBuilder {
220225
let msg;
221226
if (alreadyBuilt.includes(projectName)) {
222227
const buildMetadata = projectBuildContext.getBuildMetadata();
223-
const ts = new Date(buildMetadata.timestamp).toUTCString();
224-
msg = `*> ${projectName} /// already built at ${ts}`;
228+
let buildAt = "";
229+
if (buildMetadata) {
230+
const ts = new Date(buildMetadata.timestamp).toUTCString();
231+
buildAt = ` at ${ts}`;
232+
}
233+
msg = `*> ${projectName} /// already built${buildAt}`;
225234
} else {
226235
msg = `=> ${projectName}`;
227236
}
@@ -231,24 +240,27 @@ class ProjectBuilder {
231240
}
232241
}
233242

234-
if (cleanDest) {
243+
if (destPath && cleanDest) {
235244
this.#log.info(`Cleaning target directory...`);
236245
await rmrf(destPath);
237246
}
238247
const startTime = process.hrtime();
239248
try {
240249
const pWrites = [];
241250
for (const projectBuildContext of queue) {
242-
const projectName = projectBuildContext.getProject().getName();
243-
const projectType = projectBuildContext.getProject().getType();
251+
const project = projectBuildContext.getProject();
252+
const projectName = project.getName();
253+
const projectType = project.getType();
244254
this.#log.verbose(`Processing project ${projectName}...`);
245255

246256
// Only build projects that are not already build (i.e. provide a matching build manifest)
247257
if (alreadyBuilt.includes(projectName)) {
248258
this.#log.skipProjectBuild(projectName, projectType);
249259
} else {
250260
this.#log.startProjectBuild(projectName, projectType);
261+
project.newVersion();
251262
await projectBuildContext.getTaskRunner().runTasks();
263+
project.sealWorkspace();
252264
this.#log.endProjectBuild(projectName, projectType);
253265
}
254266
if (!requestedProjects.includes(projectName)) {
@@ -257,8 +269,15 @@ class ProjectBuilder {
257269
continue;
258270
}
259271

260-
this.#log.verbose(`Writing out files...`);
261-
pWrites.push(this._writeResults(projectBuildContext, fsTarget));
272+
if (fsTarget) {
273+
this.#log.verbose(`Writing out files...`);
274+
pWrites.push(this._writeResults(projectBuildContext, fsTarget));
275+
}
276+
277+
if (cacheDir && !alreadyBuilt.includes(projectName)) {
278+
this.#log.verbose(`Serializing cache...`);
279+
pWrites.push(projectBuildContext.getBuildCache().serializeToDisk());
280+
}
262281
}
263282
await Promise.all(pWrites);
264283
this.#log.info(`Build succeeded in ${this._getElapsedTime(startTime)}`);
@@ -269,9 +288,91 @@ class ProjectBuilder {
269288
this._deregisterCleanupSigHooks(cleanupSigHooks);
270289
await this._executeCleanupTasks();
271290
}
291+
292+
if (watch) {
293+
const relevantProjects = queue.map((projectBuildContext) => {
294+
return projectBuildContext.getProject();
295+
});
296+
const watchHandler = this._buildContext.initWatchHandler(relevantProjects, async () => {
297+
await this.#update(projectBuildContexts, requestedProjects, fsTarget, cacheDir);
298+
});
299+
return watchHandler;
300+
301+
// Register change handler
302+
// this._buildContext.onSourceFileChange(async (event) => {
303+
// await this.#update(projectBuildContexts, requestedProjects,
304+
// fsTarget,
305+
// targetWriterProject, targetWriterDependencies);
306+
// updateOnChange(event);
307+
// }, (err) => {
308+
// updateOnChange(err);
309+
// });
310+
311+
// // Start watching
312+
// for (const projectBuildContext of queue) {
313+
// await projectBuildContext.watchFileChanges();
314+
// }
315+
}
316+
}
317+
318+
async #update(projectBuildContexts, requestedProjects, fsTarget, cacheDir) {
319+
const queue = [];
320+
await this._graph.traverseDepthFirst(async ({project}) => {
321+
const projectName = project.getName();
322+
const projectBuildContext = projectBuildContexts.get(projectName);
323+
if (projectBuildContext) {
324+
// Build context exists
325+
// => This project needs to be built or, in case it has already
326+
// been built, it's build result needs to be written out (if requested)
327+
// if (await projectBuildContext.requiresBuild()) {
328+
queue.push(projectBuildContext);
329+
// }
330+
}
331+
});
332+
333+
this.#log.setProjects(queue.map((projectBuildContext) => {
334+
return projectBuildContext.getProject().getName();
335+
}));
336+
337+
const pWrites = [];
338+
for (const projectBuildContext of queue) {
339+
const project = projectBuildContext.getProject();
340+
const projectName = project.getName();
341+
const projectType = project.getType();
342+
this.#log.verbose(`Updating project ${projectName}...`);
343+
344+
if (!await projectBuildContext.requiresBuild()) {
345+
this.#log.skipProjectBuild(projectName, projectType);
346+
continue;
347+
}
348+
349+
this.#log.startProjectBuild(projectName, projectType);
350+
project.newVersion();
351+
await projectBuildContext.runTasks();
352+
project.sealWorkspace();
353+
this.#log.endProjectBuild(projectName, projectType);
354+
if (!requestedProjects.includes(projectName)) {
355+
// Project has not been requested
356+
// => Its resources shall not be part of the build result
357+
continue;
358+
}
359+
360+
if (fsTarget) {
361+
this.#log.verbose(`Writing out files...`);
362+
pWrites.push(this._writeResults(projectBuildContext, fsTarget));
363+
}
364+
365+
if (cacheDir) {
366+
this.#log.verbose(`Updating cache...`);
367+
// TODO: Only serialize if cache has changed
368+
// TODO: Serialize lazily, or based on memory pressure
369+
pWrites.push(projectBuildContext.getBuildCache().serializeToDisk());
370+
}
371+
}
372+
await Promise.all(pWrites);
272373
}
273374

274-
async _createRequiredBuildContexts(requestedProjects) {
375+
async _createRequiredBuildContexts(requestedProjects, cacheDir) {
275376
const requiredProjects = new Set(this._graph.getProjectNames().filter((projectName) => {
276377
return requestedProjects.includes(projectName);
277378
}));
@@ -280,13 +381,14 @@ class ProjectBuilder {
280381

281382
for (const projectName of requiredProjects) {
282383
this.#log.verbose(`Creating build context for project ${projectName}...`);
283-
const projectBuildContext = this._buildContext.createProjectContext({
284-
project: this._graph.getProject(projectName)
384+
const projectBuildContext = await this._buildContext.createProjectContext({
385+
project: this._graph.getProject(projectName),
386+
cacheDir,
285387
});
286388

287389
projectBuildContexts.set(projectName, projectBuildContext);
288390

289-
if (projectBuildContext.requiresBuild()) {
391+
if (await projectBuildContext.requiresBuild()) {
290392
const taskRunner = projectBuildContext.getTaskRunner();
291393
const requiredDependencies = await taskRunner.getRequiredDependencies();
292394

@@ -389,7 +491,9 @@ class ProjectBuilder {
389491
const {
390492
default: createBuildManifest
391493
} = await import("./helpers/createBuildManifest.js");
392-
const metadata = await createBuildManifest(project, buildConfig, this._buildContext.getTaskRepository());
494+
const metadata = await createBuildManifest(
495+
project, this._graph, buildConfig, this._buildContext.getTaskRepository(),
496+
projectBuildContext.getBuildCache());
393497
await target.write(resourceFactory.createResource({
394498
path: `/.ui5/build-manifest.json`,
395499
string: JSON.stringify(metadata, null, "\t")

packages/project/lib/build/TaskRunner.js

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {getLogger} from "@ui5/logger";
22
import composeTaskList from "./helpers/composeTaskList.js";
3-
import {createReaderCollection} from "@ui5/fs/resourceFactory";
3+
import {createReaderCollection, createTracker} from "@ui5/fs/resourceFactory";
44

55
/**
66
* TaskRunner
@@ -21,8 +21,8 @@ class TaskRunner {
2121
* @param {@ui5/project/build/ProjectBuilder~BuildConfiguration} parameters.buildConfig
2222
* Build configuration
2323
*/
24-
constructor({graph, project, log, taskUtil, taskRepository, buildConfig}) {
25-
if (!graph || !project || !log || !taskUtil || !taskRepository || !buildConfig) {
24+
constructor({graph, project, log, cache, taskUtil, taskRepository, buildConfig}) {
25+
if (!graph || !project || !log || !cache || !taskUtil || !taskRepository || !buildConfig) {
2626
throw new Error("TaskRunner: One or more mandatory parameters not provided");
2727
}
2828
this._project = project;
@@ -31,6 +31,7 @@ class TaskRunner {
3131
this._taskRepository = taskRepository;
3232
this._buildConfig = buildConfig;
3333
this._log = log;
34+
this._cache = cache;
3435

3536
this._directDependencies = new Set(this._taskUtil.getDependencies());
3637
}
@@ -190,20 +191,62 @@ class TaskRunner {
190191
options.projectName = this._project.getName();
191192
options.projectNamespace = this._project.getNamespace();
192193

194+
// TODO: Apply cache and stage handling for custom tasks as well
195+
this._project.useStage(taskName);
196+
197+
// Check whether any of the relevant resources have changed
198+
if (this._cache.hasCacheForTask(taskName)) {
199+
await this._cache.validateChangedProjectResources(
200+
taskName, this._project.getReader(), this._allDependenciesReader);
201+
if (this._cache.hasValidCacheForTask(taskName)) {
202+
this._log.skipTask(taskName);
203+
return;
204+
}
205+
}
206+
this._log.info(
207+
`Executing task ${taskName} for project ${this._project.getName()}`);
208+
const workspace = createTracker(this._project.getWorkspace());
193209
const params = {
194-
workspace: this._project.getWorkspace(),
210+
workspace,
195211
taskUtil: this._taskUtil,
196-
options
212+
options,
213+
buildCache: {
214+
// TODO: Create a proper interface for this
215+
hasCache: () => {
216+
return this._cache.hasCacheForTask(taskName);
217+
},
218+
getChangedProjectResourcePaths: () => {
219+
return this._cache.getChangedProjectResourcePaths(taskName);
220+
},
221+
getChangedDependencyResourcePaths: () => {
222+
return this._cache.getChangedDependencyResourcePaths(taskName);
223+
},
224+
}
197225
};
226+
// const invalidatedResources = this._cache.getDepsOfInvalidatedResourcesForTask(taskName);
227+
// if (invalidatedResources) {
228+
// params.invalidatedResources = invalidatedResources;
229+
// }
198230

231+
let dependencies;
199232
if (requiresDependencies) {
200-
params.dependencies = this._allDependenciesReader;
233+
dependencies = createTracker(this._allDependenciesReader);
234+
params.dependencies = dependencies;
201235
}
202236

203237
if (!taskFunction) {
204238
taskFunction = (await this._taskRepository.getTask(taskName)).task;
205239
}
206-
return taskFunction(params);
240+
241+
this._log.startTask(taskName);
242+
this._taskStart = performance.now();
243+
await taskFunction(params);
244+
if (this._log.isLevelEnabled("perf")) {
245+
this._log.perf(
246+
`Task ${taskName} finished in ${Math.round((performance.now() - this._taskStart))} ms`);
247+
}
248+
this._log.endTask(taskName);
249+
await this._cache.updateTaskResult(taskName, workspace, dependencies);
207250
};
208251
}
209252
this._tasks[taskName] = {
@@ -445,13 +488,15 @@ class TaskRunner {
445488
* @returns {Promise} Resolves when task has finished
446489
*/
447490
async _executeTask(taskName, taskFunction, taskParams) {
448-
this._log.startTask(taskName);
491+
if (this._cache.hasValidCacheForTask(taskName)) {
492+
this._log.skipTask(taskName);
493+
return;
494+
}
449495
this._taskStart = performance.now();
450496
await taskFunction(taskParams, this._log);
451497
if (this._log.isLevelEnabled("perf")) {
452498
this._log.perf(`Task ${taskName} finished in ${Math.round((performance.now() - this._taskStart))} ms`);
453499
}
454-
this._log.endTask(taskName);
455500
}
456501

457502
async _createDependenciesReader(requiredDirectDependencies) {

0 commit comments

Comments
 (0)