109 lines
3.3 KiB
TypeScript
109 lines
3.3 KiB
TypeScript
import * as crypto from 'crypto';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as plugins from './tsdocker.plugins.js';
|
|
import * as paths from './tsdocker.paths.js';
|
|
import { logger } from './tsdocker.logging.js';
|
|
import type { ICacheData, ICacheEntry } from './interfaces/index.js';
|
|
|
|
const smartshellInstance = new plugins.smartshell.Smartshell({
|
|
executor: 'bash',
|
|
});
|
|
|
|
/**
|
|
* Manages content-hash-based build caching for Dockerfiles.
|
|
* Cache is stored in .nogit/tsdocker_support.json.
|
|
*/
|
|
export class TsDockerCache {
|
|
private cacheFilePath: string;
|
|
private data: ICacheData;
|
|
|
|
constructor() {
|
|
this.cacheFilePath = path.join(paths.cwd, '.nogit', 'tsdocker_support.json');
|
|
this.data = { version: 1, entries: {} };
|
|
}
|
|
|
|
/**
|
|
* Loads cache data from disk. Falls back to empty cache on missing/corrupt file.
|
|
*/
|
|
public load(): void {
|
|
try {
|
|
const raw = fs.readFileSync(this.cacheFilePath, 'utf-8');
|
|
const parsed = JSON.parse(raw);
|
|
if (parsed && parsed.version === 1 && parsed.entries) {
|
|
this.data = parsed;
|
|
} else {
|
|
logger.log('warn', '[cache] Cache file has unexpected format, starting fresh');
|
|
this.data = { version: 1, entries: {} };
|
|
}
|
|
} catch {
|
|
// Missing or corrupt file — start fresh
|
|
this.data = { version: 1, entries: {} };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Saves cache data to disk. Creates .nogit directory if needed.
|
|
*/
|
|
public save(): void {
|
|
const dir = path.dirname(this.cacheFilePath);
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
fs.writeFileSync(this.cacheFilePath, JSON.stringify(this.data, null, 2), 'utf-8');
|
|
}
|
|
|
|
/**
|
|
* Computes SHA-256 hash of Dockerfile content.
|
|
*/
|
|
public computeContentHash(content: string): string {
|
|
return crypto.createHash('sha256').update(content).digest('hex');
|
|
}
|
|
|
|
/**
|
|
* Checks whether a build can be skipped for the given Dockerfile.
|
|
* Logs detailed diagnostics and returns true if the build should be skipped.
|
|
*/
|
|
public async shouldSkipBuild(cleanTag: string, content: string): Promise<boolean> {
|
|
const contentHash = this.computeContentHash(content);
|
|
const entry = this.data.entries[cleanTag];
|
|
|
|
if (!entry) {
|
|
logger.log('info', `[cache] ${cleanTag}: no cached entry, will build`);
|
|
return false;
|
|
}
|
|
|
|
const hashMatch = entry.contentHash === contentHash;
|
|
logger.log('info', `[cache] ${cleanTag}: hash ${hashMatch ? 'matches' : 'changed'}`);
|
|
|
|
if (!hashMatch) {
|
|
logger.log('info', `[cache] ${cleanTag}: content changed, will build`);
|
|
return false;
|
|
}
|
|
|
|
// Hash matches — verify the image still exists locally
|
|
const inspectResult = await smartshellInstance.exec(
|
|
`docker image inspect ${entry.imageId} > /dev/null 2>&1`
|
|
);
|
|
const available = inspectResult.exitCode === 0;
|
|
|
|
if (available) {
|
|
logger.log('info', `[cache] ${cleanTag}: cache hit, skipping build`);
|
|
return true;
|
|
}
|
|
|
|
logger.log('info', `[cache] ${cleanTag}: image no longer available, will build`);
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Records a successful build in the cache.
|
|
*/
|
|
public recordBuild(cleanTag: string, content: string, imageId: string, buildTag: string): void {
|
|
this.data.entries[cleanTag] = {
|
|
contentHash: this.computeContentHash(content),
|
|
imageId,
|
|
buildTag,
|
|
timestamp: Date.now(),
|
|
};
|
|
}
|
|
}
|