Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ jobs:
working-directory: Frontend
run: npm run build

- name: Build built-in plugins
run: node Backend/scripts/ensure-built-in-plugins.js

# ---------- Rust & platform deps ----------
- name: Install Rust (stable)
uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # stable
Expand All @@ -179,8 +182,6 @@ jobs:
- name: Setup sccache
uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9

- name: Install OpenVCS SDK CLI
run: cargo install --locked openvcs-sdk --bin cargo-openvcs

- name: Install Linux deps
if: matrix.platform == 'ubuntu-24.04'
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/publish-stable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ jobs:
working-directory: Frontend
run: npm run build

- name: Build built-in plugins
run: node Backend/scripts/ensure-built-in-plugins.js

# ---------- Rust toolchain & deps ----------
- name: Install Rust (stable)
uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # stable
Expand All @@ -69,8 +72,6 @@ jobs:
- name: Setup sccache
uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9

- name: Install OpenVCS SDK CLI
run: cargo install --locked openvcs-sdk --bin cargo-openvcs

- name: Install Linux build deps (Ubuntu)
if: matrix.platform == 'ubuntu-24.04'
Expand Down
107 changes: 93 additions & 14 deletions Backend/scripts/ensure-built-in-plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,21 @@ const { spawnSync } = require('child_process');
const scriptDir = __dirname;
const backendDir = path.resolve(scriptDir, '..');
const repoRoot = path.resolve(backendDir, '..');
const workspaceRoot = path.resolve(repoRoot, '..');
const sdkDir = path.join(workspaceRoot, 'SDK');
const pluginSources = path.join(backendDir, 'built-in-plugins');
const pluginBundles = path.join(repoRoot, 'target', 'openvcs', 'built-in-plugins');
const nodeRuntimeDir = path.join(repoRoot, 'target', 'openvcs', 'node-runtime');
const npmExecutable = process.platform === 'win32' ? 'npm.cmd' : 'npm';

const forceRebuild = process.argv.includes('--force');

const skipDirs = new Set(['target', '.git', 'node_modules', 'dist']);

/**
* Returns the newest file mtime (ms) under a directory, ignoring known build dirs.
*
* @param {string} dir - Directory to scan.
* @returns {number|null} Latest mtime or null when no files are present.
*/
function latestSourceTime(dir) {
let latest = 0;
let hasFile = false;
Expand Down Expand Up @@ -50,6 +56,12 @@ function latestSourceTime(dir) {
return hasFile ? latest : null;
}

/**
* Resolves the canonical built-in plugin bundle filename from plugin metadata.
*
* @param {string} name - Plugin source directory name.
* @returns {string} Expected `.ovcsp` filename.
*/
function bundleFileNameForPlugin(name) {
const manifestPath = path.join(pluginSources, name, 'openvcs.plugin.json');
try {
Expand All @@ -58,7 +70,8 @@ function bundleFileNameForPlugin(name) {
if (pluginId) {
return `${pluginId}.ovcsp`;
}
} catch {
} catch (e) {
console.debug(`Manifest unavailable for ${name}; using directory-name fallback.`, e);
// Fall back to the directory name so the missing/invalid manifest still
// forces a rebuild attempt and surfaces the real packaging error later.
}
Expand Down Expand Up @@ -87,6 +100,19 @@ function findOutdatedPlugins() {
return outdated;
}

/**
* Lists all plugin directories regardless of build state.
*
* @returns {string[]} Plugin directory names.
*/
function findAllPlugins() {
if (!fs.existsSync(pluginSources)) return [];
return fs
.readdirSync(pluginSources, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name);
}

function ensureBundlesDir() {
fs.mkdirSync(pluginBundles, { recursive: true });
}
Expand Down Expand Up @@ -169,34 +195,87 @@ function ensurePluginDependencies(pluginDir) {
}
}

/**
* Copies plugin archive(s) created by `npm run dist` into the app bundle output.
*
* @param {string} pluginName - Plugin directory name.
* @param {string} pluginDir - Plugin directory path.
*/
function copyPackagedBundles(pluginName, pluginDir) {
ensureBundlesDir();
const distDir = path.join(pluginDir, 'dist');
if (!fs.existsSync(distDir)) {
console.error(`Missing dist directory for ${pluginName}: ${distDir}`);
process.exit(1);
}

const archiveEntries = fs
.readdirSync(distDir, { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.endsWith('.ovcsp'));

if (archiveEntries.length === 0) {
console.error(`No .ovcsp bundle produced for ${pluginName} in ${distDir}`);
process.exit(1);
}

const preferredName = bundleFileNameForPlugin(pluginName);
const preferred = archiveEntries.find((entry) => entry.name === preferredName);
const sourceArchive = preferred
? path.join(distDir, preferred.name)
: path.join(distDir, archiveEntries[0].name);
const destArchive = path.join(pluginBundles, preferredName);

if (archiveEntries.length > 1) {
console.warn(
`Multiple .ovcsp archives found for ${pluginName}; using ${path.basename(sourceArchive)}.`
);
}

fs.copyFileSync(sourceArchive, destArchive);
console.log(`Built-in plugin bundle copied -> ${destArchive}`);
}

function runDistCommand(pluginNames) {
console.log(`Built-in plugin bundles need rebuilding: ${pluginNames.join(', ')}`);
const header = forceRebuild
? `Forcing rebuild of built-in plugins: ${pluginNames.join(', ')}`
: `Built-in plugin bundles need rebuilding: ${pluginNames.join(', ')}`;
console.log(header);
for (const pluginName of pluginNames) {
const pluginDir = path.join(pluginSources, pluginName);
const packageJsonPath = path.join(pluginDir, 'package.json');

if (!fs.existsSync(packageJsonPath)) {
console.warn(`Skipping non-code plugin ${pluginName} (no package.json).`);
continue;
}

ensurePluginDependencies(pluginDir);
console.log(`Packaging built-in plugin ${pluginName} via SDK CLI...`);
const res = spawnSync(
npmExecutable,
['--prefix', sdkDir, 'run', 'openvcs', '--', 'dist', '--plugin-dir', pluginDir, '--out', pluginBundles],
{ cwd: backendDir, stdio: 'inherit' }
);
console.log(`Packaging built-in plugin ${pluginName} via npm run dist...`);
const res = spawnSync(npmExecutable, ['run', 'dist'], {
cwd: pluginDir,
stdio: 'inherit',
});
if (res.error) {
console.error(`Failed to run SDK packager for ${pluginName}:`, res.error);
console.error(`Failed to run npm dist for ${pluginName}:`, res.error);
process.exit(res.status || 1);
}
if (res.status !== 0) {
process.exit(res.status);
}

copyPackagedBundles(pluginName, pluginDir);
}
}

ensureBundlesDir();
ensureNodeRuntimeDir();
ensureBundledNodeRuntime();

const outdated = findOutdatedPlugins();
if (outdated.length > 0) {
runDistCommand(outdated);
const targets = forceRebuild ? findAllPlugins() : findOutdatedPlugins();
if (targets.length > 0) {
runDistCommand(targets);
} else if (forceRebuild) {
console.log('Force rebuild requested, but no built-in plugins were found.');
} else {
console.log('Built-in plugin bundles are up to date.');
}
2 changes: 1 addition & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ build target="all":
_build_all: _build_client

_build_plugins:
cargo openvcs dist --all --plugin-dir Backend/built-in-plugins --out target/openvcs/built-in-plugins
node Backend/scripts/ensure-built-in-plugins.js

_build_client: _build_plugins
npm --prefix Frontend run build
Expand Down
16 changes: 16 additions & 0 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,22 @@ npx openvcs dist --plugin-dir /path/to/plugin --out /path/to/dist

`openvcs dist` runs the build step automatically unless `--no-build` is passed.

## Bundling built-in plugins

The app ships a handful of built-in plugins. Their `.ovcsp` bundles are rebuilt by
running the helper script from the repository root:

```bash
node Backend/scripts/ensure-built-in-plugins.js
```

The script compares source timestamps against the previously packaged bundles,
installs npm deps if needed, runs `npm run dist` inside each plugin, and copies
the resulting archives into `target/openvcs/built-in-plugins`. Pass `--force` to
rebuild all built-in plugins regardless of timestamps (useful for CI or manual
repackaging). Non-code plugins missing `package.json` are skipped because they
ship prebuilt archives.

Typical Node plugin author modules now look like:

```ts
Expand Down
39 changes: 38 additions & 1 deletion scripts/tauri-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');

/**
* Loads key/value pairs from a dotenv-style file into process.env.
* Existing environment values are preserved.
*
* @param {string} filePath - Path to the dotenv file.
*/
function loadLocalEnv(filePath) {
if (!fs.existsSync(filePath)) return;
const content = fs.readFileSync(filePath, 'utf8');
Expand All @@ -23,6 +29,12 @@ function loadLocalEnv(filePath) {
}
}

/**
* Normalizes the Tauri signing key into the format expected by TAURI_SIGNING_PRIVATE_KEY.
*
* @param {string} raw - Raw key content.
* @returns {string} Normalized key value.
*/
function normalizeSigningKey(raw) {
let key = raw;
// Tauri expects TAURI_SIGNING_PRIVATE_KEY to be base64 of the minisign key box text.
Expand All @@ -38,6 +50,12 @@ function normalizeSigningKey(raw) {
return key;
}

/**
* Prompts for sensitive input without echoing typed characters.
*
* @param {string} question - Prompt text.
* @returns {Promise<string>} User-provided value.
*/
function promptHidden(question) {
return new Promise((resolve) => {
const stdin = process.stdin;
Expand Down Expand Up @@ -66,8 +84,26 @@ function promptHidden(question) {
});
}

/**
* Returns repository and Backend directories based on this script location.
*
* @returns {{repoRoot: string, backendDir: string}} Build directory paths.
*/
function resolvePaths() {
const cwdRepoRoot = process.cwd();
const cwdBackendDir = path.join(cwdRepoRoot, 'Backend');
const cwdTauriConfig = path.join(cwdBackendDir, 'tauri.conf.json');
if (fs.existsSync(cwdTauriConfig)) {
return { repoRoot: cwdRepoRoot, backendDir: cwdBackendDir };
}

const scriptRepoRoot = path.resolve(__dirname, '..');
const scriptBackendDir = path.join(scriptRepoRoot, 'Backend');
return { repoRoot: scriptRepoRoot, backendDir: scriptBackendDir };
}

async function main() {
const repoRoot = process.cwd();
const { repoRoot, backendDir } = resolvePaths();
loadLocalEnv(path.join(repoRoot, '.env.tauri.local'));

if (process.env.TAURI_SIGNING_PRIVATE_KEY_FILE && !process.env.TAURI_SIGNING_PRIVATE_KEY) {
Expand All @@ -90,6 +126,7 @@ async function main() {
process.env.NO_STRIP = process.env.NO_STRIP || 'true';

const child = spawn('cargo', ['tauri', 'build'], {
cwd: backendDir,
stdio: 'inherit',
env: process.env,
});
Expand Down
Loading