mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-10-13 00:44:55 +08:00
381 lines
13 KiB
JavaScript
381 lines
13 KiB
JavaScript
const path = require("path");
|
|
const fs = require("fs-extra");
|
|
const del = require("del");
|
|
const { src, dest, series, parallel, watch } = require("gulp");
|
|
const child_process = require("child_process");
|
|
const { globSync } = require("glob");
|
|
const buildIcons = require("./icons/build_icons");
|
|
const xml2js = require("xml2js");
|
|
|
|
/*
|
|
* We'll want a flexible glob down the road, but for now there are no
|
|
* un-built visualizations in the repository; for performance and
|
|
* simplicity just add them one at a time until we upgrade older viz's.
|
|
*/
|
|
const PLUGIN_BUILD_IDS = [
|
|
"annotate_image",
|
|
"example",
|
|
"fits_graph_viewer",
|
|
"hyphyvision",
|
|
"jqplot/jqplot_bar",
|
|
"mvpapp",
|
|
"nora",
|
|
"nvd3/nvd3_bar",
|
|
"pv",
|
|
"scatterplot",
|
|
];
|
|
const INSTALL_PLUGIN_BUILD_IDS = [
|
|
"aceeditor",
|
|
"aladin",
|
|
"alignment",
|
|
"chiraviz",
|
|
"cytoscape",
|
|
"drawrna",
|
|
"h5web",
|
|
"heatmap",
|
|
"jupyterlite",
|
|
"kepler",
|
|
"ngl",
|
|
"molstar",
|
|
"msa",
|
|
"niivue",
|
|
"openlayers",
|
|
"openseadragon",
|
|
"pca",
|
|
"phylocanvas",
|
|
"plotly",
|
|
"plyr",
|
|
"tabulator",
|
|
"tiffviewer",
|
|
"ts_visjs",
|
|
"unipept",
|
|
"venn",
|
|
"vitessce",
|
|
"vizarr",
|
|
"vtk",
|
|
]; // todo: derive from XML
|
|
|
|
const args = process.argv.slice(2);
|
|
const limitIndex = args.indexOf("--limit");
|
|
const pluginFilter = limitIndex !== -1 && args[limitIndex + 1] ? args[limitIndex + 1] : "";
|
|
|
|
const applyPluginFilter = (plugin) => {
|
|
return !pluginFilter || plugin.includes(pluginFilter);
|
|
};
|
|
|
|
const failOnError =
|
|
process.env.GALAXY_PLUGIN_BUILD_FAIL_ON_ERROR && process.env.GALAXY_PLUGIN_BUILD_FAIL_ON_ERROR !== "0"
|
|
? true
|
|
: false;
|
|
|
|
const PATHS = {
|
|
nodeModules: "./node_modules",
|
|
stagedLibraries: {
|
|
backbone: ["backbone.js", "backbone.js"],
|
|
jquery: ["dist/jquery.js", "jquery/jquery.js"],
|
|
"jquery-migrate": ["dist/jquery-migrate.js", "jquery/jquery.migrate.js"],
|
|
"jquery-mousewheel": ["jquery.mousewheel.js", "jquery/jquery.mousewheel.js"],
|
|
requirejs: ["require.js", "require.js"],
|
|
underscore: ["underscore.js", "underscore.js"],
|
|
},
|
|
};
|
|
|
|
PATHS.pluginBaseDir =
|
|
(process.env.GALAXY_PLUGIN_PATH && process.env.GALAXY_PLUGIN_PATH !== "None"
|
|
? process.env.GALAXY_PLUGIN_PATH
|
|
: undefined) || "../config/plugins/";
|
|
|
|
PATHS.pluginDirs = [
|
|
path.join(PATHS.pluginBaseDir, "visualizations/*/static/**/*"),
|
|
path.join(PATHS.pluginBaseDir, "visualizations/*/*/static/**/*"),
|
|
];
|
|
|
|
PATHS.pluginBuildModules = [
|
|
...PLUGIN_BUILD_IDS.filter(applyPluginFilter).map((plugin) =>
|
|
path.join(PATHS.pluginBaseDir, `visualizations/${plugin}/package.json`)
|
|
),
|
|
];
|
|
|
|
function stageLibs(callback) {
|
|
Object.keys(PATHS.stagedLibraries).forEach((lib) => {
|
|
const p1 = path.resolve(path.join(PATHS.nodeModules, lib, PATHS.stagedLibraries[lib][0]));
|
|
const p2 = path.resolve(path.join("src", "libs", PATHS.stagedLibraries[lib][1]));
|
|
if (fs.existsSync(p1)) {
|
|
del.sync(p2);
|
|
fs.createReadStream(p1).pipe(fs.createWriteStream(p2));
|
|
} else {
|
|
callback(
|
|
p1 +
|
|
" does not exist, yet it is a required library. This is an error. Check that the package in question exists in node_modules."
|
|
);
|
|
}
|
|
});
|
|
return callback();
|
|
}
|
|
|
|
async function icons() {
|
|
await buildIcons("./src/assets/icons.json");
|
|
}
|
|
|
|
function stagePlugins(callback) {
|
|
// Get visualization directories
|
|
const visualizationDirs = [
|
|
path.join(PATHS.pluginBaseDir, "visualizations/*/static"),
|
|
path.join(PATHS.pluginBaseDir, "visualizations/*/*/static"),
|
|
];
|
|
|
|
// Flatten the glob patterns to actual directory paths
|
|
const dirs = [...globSync(visualizationDirs)];
|
|
|
|
// Process each directory
|
|
const copyPromises = dirs.map((sourceDir) => {
|
|
// Get the relative path from the plugin base dir
|
|
const relativeDir = path.relative(PATHS.pluginBaseDir, sourceDir);
|
|
// The target should preserve the full path including 'static'
|
|
const targetDir = path.join("../static/plugins", relativeDir);
|
|
|
|
// Skip node_modules/.bin directories
|
|
if (sourceDir.includes("node_modules/.bin")) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
return fs
|
|
.copy(sourceDir, targetDir, {
|
|
filter: (src) => !src.includes("node_modules/.bin"),
|
|
})
|
|
.catch((err) => {
|
|
console.error(`Error copying ${sourceDir}:`, err);
|
|
// Don't fail the entire build for a single plugin
|
|
return Promise.resolve();
|
|
});
|
|
});
|
|
|
|
Promise.all(copyPromises)
|
|
.then(() => {
|
|
console.log("Plugin staging completed successfully");
|
|
callback();
|
|
})
|
|
.catch((err) => {
|
|
console.error("Error during plugin staging:", err);
|
|
callback(err);
|
|
});
|
|
}
|
|
|
|
function buildPlugins(callback, forceRebuild) {
|
|
/*
|
|
* Walk pluginBuildModules glob and attempt to build modules.
|
|
*/
|
|
const packageJsons = globSync(PATHS.pluginBuildModules, {});
|
|
packageJsons.forEach((file) => {
|
|
let skipBuild = false;
|
|
const pluginDir = path.dirname(file);
|
|
const pluginName = pluginDir.split(path.sep).pop();
|
|
|
|
const hashFilePath = path.join(pluginDir, "static", "plugin_build_hash.txt");
|
|
|
|
if (forceRebuild) {
|
|
skipBuild = false;
|
|
} else {
|
|
// Try reading existing plugin_build_hash.txt
|
|
if (fs.existsSync(hashFilePath)) {
|
|
const oldHash = fs.readFileSync(hashFilePath, "utf8").trim();
|
|
const isHash = /^[0-9a-f]{7,40}$/.test(oldHash);
|
|
|
|
if (!isHash) {
|
|
console.log(`Hash file for ${pluginName} exists but does not have a valid git hash.`);
|
|
skipBuild = false;
|
|
} else {
|
|
// Check if there are changes since the stored hash
|
|
const diffResult = child_process.spawnSync("git", ["diff", "--quiet", oldHash, "--", pluginDir], {
|
|
stdio: "inherit",
|
|
shell: true,
|
|
});
|
|
skipBuild = diffResult.status === 0;
|
|
if (!skipBuild) {
|
|
// Hash exists and is outdated, rename to .orig
|
|
fs.renameSync(hashFilePath, `${hashFilePath}.orig`);
|
|
}
|
|
}
|
|
} else {
|
|
console.log(`No build hashfile detected for ${pluginName}, generating now.`);
|
|
}
|
|
}
|
|
|
|
if (skipBuild) {
|
|
console.log(`No changes detected for ${pluginName}, skipping build.`);
|
|
} else {
|
|
console.log(`Installing Dependencies for ${pluginName}`);
|
|
// Yarn install
|
|
child_process.spawnSync(
|
|
"yarn",
|
|
["install", "--production=false", "--network-timeout=300000", "--check-files"],
|
|
{
|
|
cwd: pluginDir,
|
|
stdio: "inherit",
|
|
shell: true,
|
|
}
|
|
);
|
|
|
|
console.log(`Building ${pluginName}`);
|
|
const opts = {
|
|
cwd: pluginDir,
|
|
stdio: "inherit",
|
|
shell: true,
|
|
};
|
|
// Node >16 fix
|
|
if (Number(process.versions.node.split(".")[0]) > 16) {
|
|
opts.env = {
|
|
...process.env,
|
|
PARCEL_WORKER_BACKEND: "process",
|
|
NODE_OPTIONS: "--openssl-legacy-provider",
|
|
};
|
|
}
|
|
|
|
const buildResult = child_process.spawnSync("yarn", ["build"], opts);
|
|
if (buildResult.status === 0) {
|
|
console.log(`Successfully built, saving build state to ${hashFilePath}`);
|
|
// Save new hash
|
|
child_process.execSync(`git rev-parse HEAD > ${hashFilePath}`, {
|
|
stdio: "inherit",
|
|
shell: true,
|
|
});
|
|
} else {
|
|
console.error(`Error building ${pluginName}, not saving build state.`);
|
|
if (failOnError) {
|
|
console.error("Failing build due to GALAXY_PLUGIN_BUILD_FAIL_ON_ERROR being set.");
|
|
process.exit(1);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
return callback();
|
|
}
|
|
|
|
/**
|
|
* Same logic as buildPlugins, but for 'installed' plugins that rely on an XML
|
|
* specifying npm dependencies (the INSTALL_PLUGIN_BUILD_IDS).
|
|
*/
|
|
async function installPlugins(callback, forceReinstall) {
|
|
for (const pluginName of INSTALL_PLUGIN_BUILD_IDS.filter(applyPluginFilter)) {
|
|
const pluginDir = path.join(PATHS.pluginBaseDir, `visualizations/${pluginName}`);
|
|
const xmlPath = path.join(pluginDir, `config/${pluginName}.xml`);
|
|
|
|
if (!fs.existsSync(xmlPath)) {
|
|
console.error(`XML file not found: ${xmlPath}`);
|
|
continue;
|
|
}
|
|
|
|
const hashFilePath = path.join(pluginDir, "static", "plugin_build_hash.txt");
|
|
|
|
let skipInstall = false;
|
|
if (!forceReinstall && fs.existsSync(hashFilePath)) {
|
|
const oldHash = fs.readFileSync(hashFilePath, "utf8").trim();
|
|
const isHash = /^[0-9a-f]{7,40}$/.test(oldHash);
|
|
|
|
if (!isHash) {
|
|
console.log(`Hash file for ${pluginName} exists but is not a valid git hash.`);
|
|
} else {
|
|
const diffResult = child_process.spawnSync("git", ["diff", "--quiet", oldHash, "--", pluginDir], {
|
|
stdio: "inherit",
|
|
shell: true,
|
|
});
|
|
skipInstall = diffResult.status === 0;
|
|
if (!skipInstall) {
|
|
fs.renameSync(hashFilePath, `${hashFilePath}.orig`);
|
|
}
|
|
}
|
|
} else if (!fs.existsSync(hashFilePath)) {
|
|
console.log(`No install hashfile detected for ${pluginName}, generating now.`);
|
|
}
|
|
|
|
if (skipInstall) {
|
|
console.log(`No changes detected for ${pluginName}, skipping re-installation.`);
|
|
} else {
|
|
console.log(`Installing npm dependencies for installed plugin ${pluginName}...`);
|
|
try {
|
|
await installDependenciesFromXML(xmlPath, pluginDir);
|
|
console.log(`Finished installing for ${pluginName}, saving new hash.`);
|
|
|
|
// Save the current commit hash
|
|
child_process.execSync(`git rev-parse HEAD > ${hashFilePath}`, {
|
|
stdio: "inherit",
|
|
shell: true,
|
|
});
|
|
} catch (err) {
|
|
console.error(`Error installing for ${pluginName}:`, err);
|
|
if (failOnError) {
|
|
console.error(
|
|
"Failing build due to GALAXY_PLUGIN_BUILD_FAIL_ON_ERROR being set, see error(s) above."
|
|
);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return callback();
|
|
}
|
|
|
|
// Function to parse XML and install specified npm packages
|
|
async function installDependenciesFromXML(xmlPath, pluginDir) {
|
|
const pluginXML = fs.readFileSync(xmlPath);
|
|
const parsedXML = await xml2js.parseStringPromise(pluginXML);
|
|
const requirements = parsedXML?.visualization?.requirements?.[0]?.requirement || [];
|
|
|
|
// For each <requirement type="npm" package="xxx" version="yyy" />
|
|
for (const dep of requirements) {
|
|
const { type: reqType, package: pkgName, version } = dep.$;
|
|
if (reqType === "npm" && pkgName && version) {
|
|
console.log(` → Installing ${pkgName}@${version} in ${pluginDir}`);
|
|
const installResult = child_process.spawnSync(
|
|
"npm",
|
|
["install", "--silent", "--no-save", `--prefix= .`, `${pkgName}@${version}`],
|
|
{
|
|
cwd: pluginDir,
|
|
stdio: "inherit",
|
|
shell: true,
|
|
}
|
|
);
|
|
|
|
if (installResult.status === 0) {
|
|
// Installed successfully, copy over '/static' assets
|
|
const potentialStaticPath = path.join(pluginDir, "node_modules", pkgName, "static");
|
|
if (fs.existsSync(potentialStaticPath)) {
|
|
await fs.copy(potentialStaticPath, path.join(pluginDir, "static"));
|
|
}
|
|
} else {
|
|
throw new Error(`npm install of ${pkgName}@${version} failed in ${pluginDir}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function forceBuildPlugins(callback) {
|
|
return buildPlugins(callback, true);
|
|
}
|
|
|
|
function forceInstallPlugins(callback) {
|
|
return installPlugins(callback, true);
|
|
}
|
|
|
|
function cleanPlugins() {
|
|
return del(["../static/plugins/visualizations/*"], { force: true });
|
|
}
|
|
|
|
const client = parallel(stageLibs, icons);
|
|
const plugins = series(buildPlugins, installPlugins, cleanPlugins, stagePlugins);
|
|
const pluginsRebuild = series(forceBuildPlugins, forceInstallPlugins, cleanPlugins, stagePlugins);
|
|
|
|
function watchPlugins() {
|
|
const BUILD_PLUGIN_WATCH_GLOB = [
|
|
path.join(PATHS.pluginBaseDir, `visualizations/{${PLUGIN_BUILD_IDS.join(",")}}/**/*`),
|
|
];
|
|
watch(BUILD_PLUGIN_WATCH_GLOB, { queue: false }, plugins);
|
|
}
|
|
|
|
module.exports.client = client;
|
|
module.exports.plugins = plugins;
|
|
module.exports.pluginsRebuild = pluginsRebuild;
|
|
module.exports.watchPlugins = watchPlugins;
|
|
module.exports.default = parallel(client, plugins);
|
|
module.exports.installPlugins = installPlugins;
|