|
3 | 3 |
|
4 | 4 | import * as path from 'path'; |
5 | 5 | import * as crypto from 'crypto'; |
6 | | -import * as fs from 'fs'; |
7 | 6 |
|
8 | 7 | import { FileSystem, type FolderItem, InternalError, Async } from '@rushstack/node-core-library'; |
9 | 8 | import type { ITerminal } from '@rushstack/terminal'; |
10 | | -import { packWorkerAsync, type IZipSyncPackWorkerResult } from '@rushstack/zipsync/lib/packWorkerAsync'; |
11 | | -import { unpackWorkerAsync, type IZipSyncUnpackWorkerResult } from '@rushstack/zipsync/lib/unpackWorkerAsync'; |
12 | 9 |
|
13 | 10 | import type { RushConfigurationProject } from '../../api/RushConfigurationProject'; |
14 | 11 | import type { BuildCacheConfiguration } from '../../api/BuildCacheConfiguration'; |
15 | 12 | import type { ICloudBuildCacheProvider } from './ICloudBuildCacheProvider'; |
16 | 13 | import type { FileSystemBuildCacheProvider } from './FileSystemBuildCacheProvider'; |
| 14 | +import { TarExecutable } from '../../utilities/TarExecutable'; |
| 15 | +import { EnvironmentVariableNames } from '../../api/EnvironmentConfiguration'; |
17 | 16 | import type { IOperationExecutionResult } from '../operations/IOperationExecutionResult'; |
18 | 17 |
|
19 | 18 | /** |
@@ -61,6 +60,8 @@ interface IPathsToCache { |
61 | 60 | * @internal |
62 | 61 | */ |
63 | 62 | export class OperationBuildCache { |
| 63 | + private static _tarUtilityPromise: Promise<TarExecutable | undefined> | undefined; |
| 64 | + |
64 | 65 | private readonly _project: RushConfigurationProject; |
65 | 66 | private readonly _localBuildCacheProvider: FileSystemBuildCacheProvider; |
66 | 67 | private readonly _cloudBuildCacheProvider: ICloudBuildCacheProvider | undefined; |
@@ -89,6 +90,14 @@ export class OperationBuildCache { |
89 | 90 | this._cacheId = cacheId; |
90 | 91 | } |
91 | 92 |
|
| 93 | + private static _tryGetTarUtility(terminal: ITerminal): Promise<TarExecutable | undefined> { |
| 94 | + if (!OperationBuildCache._tarUtilityPromise) { |
| 95 | + OperationBuildCache._tarUtilityPromise = TarExecutable.tryInitializeAsync(terminal); |
| 96 | + } |
| 97 | + |
| 98 | + return OperationBuildCache._tarUtilityPromise; |
| 99 | + } |
| 100 | + |
92 | 101 | public get cacheId(): string | undefined { |
93 | 102 | return this._cacheId; |
94 | 103 | } |
@@ -167,52 +176,32 @@ export class OperationBuildCache { |
167 | 176 |
|
168 | 177 | const projectFolderPath: string = this._project.projectFolder; |
169 | 178 |
|
| 179 | + // Purge output folders |
| 180 | + terminal.writeVerboseLine(`Clearing cached folders: ${this._projectOutputFolderNames.join(', ')}`); |
| 181 | + await Promise.all( |
| 182 | + this._projectOutputFolderNames.map((outputFolderName: string) => |
| 183 | + FileSystem.deleteFolderAsync(`${projectFolderPath}/${outputFolderName}`) |
| 184 | + ) |
| 185 | + ); |
| 186 | + |
| 187 | + const tarUtility: TarExecutable | undefined = await OperationBuildCache._tryGetTarUtility(terminal); |
170 | 188 | let restoreSuccess: boolean = false; |
171 | | - try { |
172 | | - const logFilePath: string = this._getLogFilePath(cacheId, 'unpack'); |
173 | | - let unpackWorkerResult: IZipSyncUnpackWorkerResult; |
174 | | - try { |
175 | | - unpackWorkerResult = await unpackWorkerAsync({ |
176 | | - archivePath: localCacheEntryPath!, |
177 | | - targetDirectories: this._projectOutputFolderNames, |
178 | | - baseDir: projectFolderPath |
179 | | - }); |
180 | | - } catch (e) { |
181 | | - const { zipSyncLogs } = e as { zipSyncLogs: string | undefined }; |
182 | | - if (zipSyncLogs) { |
183 | | - fs.writeFileSync(logFilePath, zipSyncLogs); |
184 | | - terminal.writeVerboseLine(`The zipsync log has been written to: ${logFilePath}`); |
185 | | - } |
186 | | - throw e; |
187 | | - } |
188 | | - const { |
189 | | - zipSyncReturn: { filesDeleted, filesExtracted, filesSkipped, foldersDeleted, otherEntriesDeleted }, |
190 | | - zipSyncLogs |
191 | | - } = unpackWorkerResult; |
192 | | - fs.writeFileSync(logFilePath, zipSyncLogs); |
193 | | - terminal.writeVerboseLine(`The zipsync log has been written to: ${logFilePath}`); |
194 | | - terminal.writeVerboseLine(`Restored ${filesExtracted + filesSkipped} files from cache.`); |
195 | | - if (filesExtracted > 0) { |
196 | | - terminal.writeVerboseLine(`Extracted ${filesExtracted} files to target folders.`); |
197 | | - } |
198 | | - if (filesSkipped > 0) { |
199 | | - terminal.writeVerboseLine(`Skipped ${filesSkipped} files that were already up to date.`); |
200 | | - } |
201 | | - if (filesDeleted > 0) { |
202 | | - terminal.writeVerboseLine(`Deleted ${filesDeleted} files from target folders.`); |
203 | | - } |
204 | | - if (foldersDeleted > 0) { |
205 | | - terminal.writeVerboseLine(`Deleted ${foldersDeleted} empty folders from target folders.`); |
206 | | - } |
207 | | - if (otherEntriesDeleted > 0) { |
208 | | - terminal.writeVerboseLine( |
209 | | - `Deleted ${otherEntriesDeleted} items (e.g. symbolic links) from target folders.` |
| 189 | + if (tarUtility && localCacheEntryPath) { |
| 190 | + const logFilePath: string = this._getTarLogFilePath(cacheId, 'untar'); |
| 191 | + const tarExitCode: number = await tarUtility.tryUntarAsync({ |
| 192 | + archivePath: localCacheEntryPath, |
| 193 | + outputFolderPath: projectFolderPath, |
| 194 | + logFilePath |
| 195 | + }); |
| 196 | + if (tarExitCode === 0) { |
| 197 | + restoreSuccess = true; |
| 198 | + terminal.writeLine('Successfully restored output from the build cache.'); |
| 199 | + } else { |
| 200 | + terminal.writeWarningLine( |
| 201 | + 'Unable to restore output from the build cache. ' + |
| 202 | + `See "${logFilePath}" for logs from the tar process.` |
210 | 203 | ); |
211 | 204 | } |
212 | | - restoreSuccess = true; |
213 | | - terminal.writeLine('Successfully restored output from the build cache.'); |
214 | | - } catch (e) { |
215 | | - terminal.writeWarningLine(`Unable to restore output from the build cache: ${e}`); |
216 | 205 | } |
217 | 206 |
|
218 | 207 | if (updateLocalCacheSuccess === false) { |
@@ -245,71 +234,59 @@ export class OperationBuildCache { |
245 | 234 |
|
246 | 235 | let localCacheEntryPath: string | undefined; |
247 | 236 |
|
248 | | - const finalLocalCacheEntryPath: string = this._localBuildCacheProvider.getCacheEntryPath(cacheId); |
249 | | - |
250 | | - // Derive the temp file from the destination path to ensure they are on the same volume |
251 | | - // In the case of a shared network drive containing the build cache, we also need to make |
252 | | - // sure the the temp path won't be shared by two parallel rush builds. |
253 | | - const randomSuffix: string = crypto.randomBytes(8).toString('hex'); |
254 | | - const tempLocalCacheEntryPath: string = `${finalLocalCacheEntryPath}-${randomSuffix}.temp`; |
| 237 | + const tarUtility: TarExecutable | undefined = await OperationBuildCache._tryGetTarUtility(terminal); |
| 238 | + if (tarUtility) { |
| 239 | + const finalLocalCacheEntryPath: string = this._localBuildCacheProvider.getCacheEntryPath(cacheId); |
| 240 | + |
| 241 | + // Derive the temp file from the destination path to ensure they are on the same volume |
| 242 | + // In the case of a shared network drive containing the build cache, we also need to make |
| 243 | + // sure the the temp path won't be shared by two parallel rush builds. |
| 244 | + const randomSuffix: string = crypto.randomBytes(8).toString('hex'); |
| 245 | + const tempLocalCacheEntryPath: string = `${finalLocalCacheEntryPath}-${randomSuffix}.temp`; |
| 246 | + |
| 247 | + const logFilePath: string = this._getTarLogFilePath(cacheId, 'tar'); |
| 248 | + const tarExitCode: number = await tarUtility.tryCreateArchiveFromProjectPathsAsync({ |
| 249 | + archivePath: tempLocalCacheEntryPath, |
| 250 | + paths: filesToCache.outputFilePaths, |
| 251 | + project: this._project, |
| 252 | + logFilePath |
| 253 | + }); |
255 | 254 |
|
256 | | - terminal.writeVerboseLine(`Using zipsync to create cache archive.`); |
257 | | - try { |
258 | | - const logFilePath: string = this._getLogFilePath(cacheId, 'pack'); |
259 | | - let packWorkerResult: IZipSyncPackWorkerResult; |
260 | | - try { |
261 | | - packWorkerResult = await packWorkerAsync({ |
262 | | - compression: 'auto', |
263 | | - archivePath: tempLocalCacheEntryPath, |
264 | | - targetDirectories: this._projectOutputFolderNames, |
265 | | - baseDir: this._project.projectFolder |
266 | | - }); |
267 | | - } catch (e) { |
268 | | - const { zipSyncLogs } = e as { zipSyncLogs: string | undefined }; |
269 | | - if (zipSyncLogs) { |
270 | | - fs.writeFileSync(logFilePath, zipSyncLogs); |
271 | | - terminal.writeVerboseLine(`The zipsync log has been written to: ${logFilePath}`); |
272 | | - } |
273 | | - throw e; |
274 | | - } |
275 | | - const { |
276 | | - zipSyncReturn: { filesPacked }, |
277 | | - zipSyncLogs |
278 | | - } = packWorkerResult; |
279 | | - fs.writeFileSync(logFilePath, zipSyncLogs); |
280 | | - terminal.writeVerboseLine(`The zipsync log has been written to: ${logFilePath}`); |
281 | | - terminal.writeVerboseLine(`Packed ${filesPacked} files for caching.`); |
282 | | - |
283 | | - // Move after the archive is finished so that if the process is interrupted we aren't left with an invalid file |
284 | | - try { |
285 | | - await Async.runWithRetriesAsync({ |
286 | | - action: () => |
287 | | - new Promise<void>((resolve, reject) => { |
288 | | - fs.rename(tempLocalCacheEntryPath, finalLocalCacheEntryPath, (err) => { |
289 | | - if (err) { |
290 | | - reject(err); |
291 | | - } else { |
292 | | - resolve(); |
293 | | - } |
294 | | - }); |
295 | | - }), |
296 | | - maxRetries: 2, |
297 | | - retryDelayMs: 500 |
298 | | - }); |
299 | | - } catch (moveError) { |
| 255 | + if (tarExitCode === 0) { |
| 256 | + // Move after the archive is finished so that if the process is interrupted we aren't left with an invalid file |
300 | 257 | try { |
301 | | - await FileSystem.deleteFileAsync(tempLocalCacheEntryPath); |
302 | | - } catch (deleteError) { |
303 | | - // Ignored |
| 258 | + await Async.runWithRetriesAsync({ |
| 259 | + action: () => |
| 260 | + FileSystem.moveAsync({ |
| 261 | + sourcePath: tempLocalCacheEntryPath, |
| 262 | + destinationPath: finalLocalCacheEntryPath, |
| 263 | + overwrite: true |
| 264 | + }), |
| 265 | + maxRetries: 2, |
| 266 | + retryDelayMs: 500 |
| 267 | + }); |
| 268 | + } catch (moveError) { |
| 269 | + try { |
| 270 | + await FileSystem.deleteFileAsync(tempLocalCacheEntryPath); |
| 271 | + } catch (deleteError) { |
| 272 | + // Ignored |
| 273 | + } |
| 274 | + throw moveError; |
304 | 275 | } |
305 | | - throw moveError; |
| 276 | + localCacheEntryPath = finalLocalCacheEntryPath; |
| 277 | + } else { |
| 278 | + terminal.writeWarningLine( |
| 279 | + `"tar" exited with code ${tarExitCode} while attempting to create the cache entry. ` + |
| 280 | + `See "${logFilePath}" for logs from the tar process.` |
| 281 | + ); |
| 282 | + return false; |
306 | 283 | } |
307 | | - localCacheEntryPath = finalLocalCacheEntryPath; |
308 | | - } catch (e) { |
309 | | - await FileSystem.deleteFileAsync(tempLocalCacheEntryPath).catch(() => { |
310 | | - /* ignore delete error */ |
311 | | - }); |
312 | | - throw e; |
| 284 | + } else { |
| 285 | + terminal.writeWarningLine( |
| 286 | + `Unable to locate "tar". Please ensure that "tar" is on your PATH environment variable, or set the ` + |
| 287 | + `${EnvironmentVariableNames.RUSH_TAR_BINARY_PATH} environment variable to the full path to the "tar" binary.` |
| 288 | + ); |
| 289 | + return false; |
313 | 290 | } |
314 | 291 |
|
315 | 292 | let cacheEntryBuffer: Buffer | undefined; |
@@ -418,7 +395,7 @@ export class OperationBuildCache { |
418 | 395 | }; |
419 | 396 | } |
420 | 397 |
|
421 | | - private _getLogFilePath(cacheId: string, mode: 'pack' | 'unpack'): string { |
| 398 | + private _getTarLogFilePath(cacheId: string, mode: 'tar' | 'untar'): string { |
422 | 399 | return path.join(this._project.projectRushTempFolder, `${cacheId}.${mode}.log`); |
423 | 400 | } |
424 | 401 |
|
|
0 commit comments