Compare commits

..
2 Commits
Author SHA1 Message Date
Laurent Cozic 2992b7a39f Don't cache default plugin builds from a modified source checkout
Untracked or modified files in plugin-sources are included in the build but not in the cache key.
2026-11-12 00:25:07 +00:00
Laurent Cozic c297ef2e1d Tools: Cache default plugin builds
Building the default plugins from source takes about 6 minutes per job on Windows CI. The built .jpl is now cached, keyed on the plugin commit and patch, and restored on CI with actions/cache.
2026-11-12 00:05:03 +00:00
5 changed files with 49 additions and 3 deletions

No files matched your search

@@ -76,3 +76,10 @@ runs:
- uses: actions/setup-python@v6
with:
python-version: '3.14'
# Building the default plugins from source takes several minutes on Windows. Must be restored
# before "yarn install", which builds them.
- uses: actions/cache@v6
with:
path: packages/default-plugins/build-cache
key: default-plugins-${{ runner.os }}-${{ hashFiles('packages/default-plugins/pluginRepositories.json', 'packages/default-plugins/plugin-patches/**', 'packages/default-plugins/buildDefaultPlugins.ts') }}
+2 -1
View File
@@ -1,2 +1,3 @@
built-plugins/
plugin-sources/*
plugin-sources/*
build-cache/
@@ -1,9 +1,10 @@
/* eslint-disable no-console */
import { copy, exists, remove, readdir, mkdtemp } from 'fs-extra';
import { copy, exists, remove, readdir, mkdtemp, readFile } from 'fs-extra';
import { join, resolve, basename } from 'path';
import { tmpdir } from 'os';
import { createHash } from 'crypto';
import { chdir, cwd } from 'process';
import { execCommand } from '@joplin/utils';
import { glob } from 'glob';
@@ -16,8 +17,22 @@ interface Options {
outputParentDir: string|null;
beforeInstall: (buildDir: string, pluginName: string)=> Promise<void>;
beforePatch: ()=> Promise<void>;
useBuildCache: boolean;
}
// Increment to invalidate cached builds, for example after changing how plugins are built
const buildCacheVersion = 1;
const buildCacheDir = process.env.JOPLIN_DEFAULT_PLUGINS_CACHE_DIR || join(__dirname, 'build-cache');
const getBuildCachePath = async (pluginId: string, repositoryData: RepositoryData) => {
const patchFile = getPathToPatchFileFor(pluginId);
const patch = await exists(patchFile) ? await readFile(patchFile, 'utf8') : '';
const hash = createHash('sha256')
.update(JSON.stringify([buildCacheVersion, pluginId, repositoryData.cloneUrl, repositoryData.commit, patch]))
.digest('hex');
return join(buildCacheDir, `${pluginId}-${hash.substring(0, 16)}.jpl`);
};
const buildDefaultPlugins = async (options: Options) => {
const pluginRepositoryData = await readRepositoryJson(join(__dirname, 'pluginRepositories.json'));
@@ -26,7 +41,23 @@ const buildDefaultPlugins = async (options: Options) => {
const outputPath = options.outputParentDir && join(options.outputParentDir, `${pluginId}.jpl`);
if (repositoryData.type === BuiltInPluginType.Built) {
await buildPlugin(pluginId, repositoryData, outputPath, options);
const cachePath = options.useBuildCache && outputPath ? await getBuildCachePath(pluginId, repositoryData) : null;
if (cachePath && await exists(cachePath)) {
logStatus('Using cached build of', pluginId, 'from', cachePath);
await copy(cachePath, outputPath);
continue;
}
const sourceHasLocalChanges = await buildPlugin(pluginId, repositoryData, outputPath, options);
if (cachePath) {
if (sourceHasLocalChanges) {
logStatus('Not caching build of', pluginId, ': its source checkout has local changes');
} else {
await copy(outputPath, cachePath);
logStatus('Saved build of', pluginId, 'to', cachePath);
}
}
} else {
if (!outputPath) {
console.warn('Skipping NPM plugin,', pluginId, ': missing output path.');
@@ -80,6 +111,9 @@ const buildPlugin = async (pluginId: string, repositoryData: RepositoryData, out
throw new Error(`Unable to checkout commit ${expectedCommitHash}`);
}
// Local changes are included in the build but not in the build cache key
const sourceHasLocalChanges = (await execCommand(['git', 'status', '--porcelain'], { showStdout: false })).trim() !== '';
logStatus('Copying repository files...');
await copy(pluginDir, buildDir, {
filter: fileName => {
@@ -122,6 +156,8 @@ const buildPlugin = async (pluginId: string, repositoryData: RepositoryData, out
} else {
console.warn('No output directory specified. Not copying built .jpl files.');
}
return sourceHasLocalChanges;
} catch (error) {
console.error(error);
console.log('Build directory', buildDir);
@@ -5,6 +5,7 @@ const buildAll = (outputDirectory: string) => {
outputParentDir: outputDirectory,
beforeInstall: async () => { },
beforePatch: async () => { },
useBuildCache: true,
});
};
@@ -10,6 +10,7 @@ const editPatch = async (targetPluginId: string, outputParentDir: string|null) =
await buildDefaultPlugins({
outputParentDir: outputParentDir,
useBuildCache: false,
beforePatch: async () => {
// To make updating just the patch possible, a commit is created just before applying
// the patch.