feat: native slackware package (#1381)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added detailed versioning for plugin packages incorporating
architecture and build identifiers.
- Simplified and improved install/uninstall scripts with backup and
dynamic package detection.
- Introduced comprehensive setup, verification, patching, and cleanup
scripts for the Unraid API environment.
- Enhanced service control with explicit start, stop, restart, and
status commands.
- Added robust dependency management scripts for restoring and archiving
Node.js modules.
- Implemented vendor archive metadata storage and dynamic handling
during build and runtime.
- Added new CLI options and environment schemas for consistent build
configuration.
- Introduced new shutdown scripts to gracefully stop flash-backup and
unraid-api services.
- Added utility scripts for API version detection and vendor archive
configuration.
- Added a new package description file detailing Unraid API features and
homepage link.

- **Bug Fixes**
- Improved validation and error reporting for missing manifests,
dependencies, and configuration files.
  - Enhanced fallback logic for locating and creating vendor archives.
- Fixed iframe compatibility in UI by updating HTML and Firefox
preference files.

- **Chores**
- Updated .gitignore with generated file patterns for Node.js binaries
and archives.
  - Removed obsolete internal documentation and legacy cleanup scripts.
- Refined Docker Compose and CI workflows to pass precise API versioning
and manage build artifacts.
- Centralized common environment validation and CLI option definitions
across build tools.
- Cleaned up plugin manifest by removing Node.js and PNPM-related
entities and legacy logic.
- Improved logging and error handling in build and installation scripts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Eli Bosley
2025-05-08 22:54:10 -04:00
committed by GitHub
parent a5f48da322
commit 4f63b4cf3b
35 changed files with 1658 additions and 1018 deletions
+27 -12
View File
@@ -2,7 +2,7 @@ import { readFile, writeFile, mkdir, rename } from "fs/promises";
import { $ } from "zx";
import { escape as escapeHtml } from "html-sloppy-escaper";
import { dirname, join } from "node:path";
import { getTxzName, pluginName, startingDir } from "./utils/consts";
import { getTxzName, pluginName, startingDir, defaultArch, defaultBuild } from "./utils/consts";
import { getAssetUrl, getPluginUrl } from "./utils/bucket-urls";
import { getMainTxzUrl } from "./utils/bucket-urls";
import {
@@ -26,9 +26,17 @@ const checkGit = async () => {
}
};
const moveTxzFile = async (txzPath: string, pluginVersion: string) => {
const txzName = getTxzName(pluginVersion);
await rename(txzPath, join(deployDir, txzName));
const moveTxzFile = async ({txzPath, apiVersion}: Pick<PluginEnv, "txzPath" | "apiVersion">) => {
const txzName = getTxzName(apiVersion);
const targetPath = join(deployDir, txzName);
// Ensure the txz always has the full version name
if (txzPath !== targetPath) {
console.log(`Ensuring TXZ has correct name: ${txzPath} -> ${targetPath}`);
await rename(txzPath, targetPath);
} else {
console.log(`TXZ file already has correct name: ${txzPath}`);
}
};
function updateEntityValue(
@@ -50,7 +58,10 @@ const buildPlugin = async ({
tag,
txzSha256,
releaseNotes,
apiVersion,
}: PluginEnv) => {
console.log(`API version: ${apiVersion}`);
// Update plg file
let plgContent = await readFile(getRootPluginPath({ startingDir }), "utf8");
@@ -58,12 +69,16 @@ const buildPlugin = async ({
const entities: Record<string, string> = {
name: pluginName,
version: pluginVersion,
pluginURL: getPluginUrl({ baseUrl, tag }),
MAIN_TXZ: getMainTxzUrl({ baseUrl, pluginVersion, tag }),
TXZ_SHA256: txzSha256,
VENDOR_STORE_URL: getAssetUrl({ baseUrl, tag }, getVendorBundleName()),
VENDOR_STORE_FILENAME: getVendorBundleName(),
...(tag ? { TAG: tag } : {}),
api_version: apiVersion,
arch: defaultArch,
build: defaultBuild,
plugin_url: getPluginUrl({ baseUrl, tag }),
txz_url: getMainTxzUrl({ baseUrl, apiVersion, tag }),
txz_sha256: txzSha256,
txz_name: getTxzName(apiVersion),
vendor_store_url: getAssetUrl({ baseUrl, tag }, getVendorBundleName(apiVersion)),
vendor_store_filename: getVendorBundleName(apiVersion),
...(tag ? { tag } : {}),
};
console.log("Entities:", entities);
@@ -107,8 +122,8 @@ const main = async () => {
await cleanupPluginFiles();
await buildPlugin(validatedEnv);
await moveTxzFile(validatedEnv.txzPath, validatedEnv.pluginVersion);
await bundleVendorStore();
await moveTxzFile(validatedEnv);
await bundleVendorStore(validatedEnv.apiVersion);
} catch (error) {
console.error(error);
process.exit(1);
+90 -4
View File
@@ -1,11 +1,16 @@
import { join } from "path";
import { $, cd } from "zx";
import { existsSync } from "node:fs";
import { readdir } from "node:fs/promises";
import { readdir, writeFile } from "node:fs/promises";
import { getTxzName, pluginName, startingDir } from "./utils/consts";
import { ensureNodeJs } from "./utils/nodejs-helper";
import { setupTxzEnv, TxzEnv } from "./cli/setup-txz-environment";
import { cleanupTxzFiles } from "./utils/cleanup";
import { apiDir } from "./utils/paths";
import { getVendorBundleName, getVendorFullPath } from "./build-vendor-store";
import { getAssetUrl } from "./utils/bucket-urls";
// Recursively search for manifest files
const findManifestFiles = async (dir: string): Promise<string[]> => {
@@ -40,6 +45,59 @@ const findManifestFiles = async (dir: string): Promise<string[]> => {
}
};
// Function to store vendor archive information in a recoverable location
const storeVendorArchiveInfo = async (version: string, vendorUrl: string, vendorFilename: string) => {
try {
if (!version || !vendorUrl || !vendorFilename) {
throw new Error("Cannot store vendor archive info: Missing required parameters");
}
// Create a config directory in the source tree
const configDir = join(
startingDir,
"source",
"dynamix.unraid.net",
"usr",
"local",
"share",
"dynamix.unraid.net",
"config"
);
// Ensure directory exists
await $`mkdir -p ${configDir}`;
// Get the full path for vendor archive
const vendorFullPath = getVendorFullPath(version);
// Create a JSON config file with vendor information
const configData = {
vendor_store_url: vendorUrl,
vendor_store_path: vendorFullPath,
api_version: version
};
// Validate all fields are present
Object.entries(configData).forEach(([key, value]) => {
if (!value) {
throw new Error(`Cannot store vendor archive info: Missing value for ${key}`);
}
});
const configPath = join(configDir, "vendor_archive.json");
await writeFile(configPath, JSON.stringify(configData, null, 2));
console.log(`Vendor archive information stored in ${configPath}`);
console.log(`API Version: ${version}`);
console.log(`Vendor URL: ${vendorUrl}`);
console.log(`Vendor Full Path: ${vendorFullPath}`);
return true;
} catch (error) {
console.error(`Failed to store vendor archive information: ${error.message}`);
throw error; // Re-throw to prevent build from succeeding with invalid vendor info
}
};
const validateSourceDir = async (validatedEnv: TxzEnv) => {
if (!validatedEnv.ci) {
console.log("Validating TXZ source directory");
@@ -70,8 +128,15 @@ const validateSourceDir = async (validatedEnv: TxzEnv) => {
if (!hasManifest || !hasUiManifest) {
console.log("Existing Manifest Files:", manifestFiles);
const missingFiles: string[] = [];
if (!hasManifest) missingFiles.push("manifest.json");
if (!hasUiManifest) missingFiles.push("ui.manifest.json");
throw new Error(
`Webcomponents must contain both "ui.manifest.json" and "manifest.json" - be sure to have run pnpm build:wc in unraid-ui`
`Webcomponents missing required file(s): ${missingFiles.join(", ")} - ` +
`${!hasUiManifest ? "run 'pnpm build:wc' in unraid-ui for ui.manifest.json" : ""}` +
`${!hasManifest && !hasUiManifest ? " and " : ""}` +
`${!hasManifest ? "run 'pnpm build' in web for manifest.json" : ""}`
);
}
@@ -88,16 +153,37 @@ const validateSourceDir = async (validatedEnv: TxzEnv) => {
const buildTxz = async (validatedEnv: TxzEnv) => {
await validateSourceDir(validatedEnv);
const txzPath = join(validatedEnv.txzOutputDir, getTxzName());
// Use version from validated environment
const version = validatedEnv.apiVersion;
// Always use version when getting txz name
const txzName = getTxzName(version);
console.log(`Package name: ${txzName}`);
const txzPath = join(validatedEnv.txzOutputDir, txzName);
// Use the getVendorBundleName function for consistent naming
const vendorFilename = getVendorBundleName(version);
// Use the baseUrl and tag from validatedEnv, consistent with build-plugin.ts
const vendorUrl = getAssetUrl({
baseUrl: validatedEnv.baseUrl,
tag: validatedEnv.tag
}, vendorFilename);
console.log(`Storing vendor archive information: ${vendorUrl} -> ${vendorFilename}`);
await storeVendorArchiveInfo(version, vendorUrl, vendorFilename);
await ensureNodeJs();
// Create package - must be run from within the pre-pack directory
// Use cd option to run command from prePackDir
await cd(join(startingDir, "source", pluginName));
$.verbose = true;
// Create the package using the default package name
await $`${join(startingDir, "scripts/makepkg")} --chown y --compress -${
validatedEnv.compress
} --linkadd y ${txzPath}`;
} --linkadd n ${txzPath}`;
$.verbose = false;
await cd(startingDir);
};
+86 -25
View File
@@ -1,42 +1,103 @@
import { apiDir, deployDir } from "./utils/paths";
import { deployDir, vendorStorePath } from "./utils/paths";
import { join } from "path";
import { readFileSync } from "node:fs";
import { existsSync, mkdirSync } from "node:fs";
import { startingDir } from "./utils/consts";
import { copyFile } from "node:fs/promises";
/**
* Get the version of the API from the package.json file
*
* Throws if package.json is not found or is invalid JSON.
* @returns The version of the API
*/
function getVersion(): string {
const packageJsonPath = join(apiDir, "package.json");
const packageJsonString = readFileSync(packageJsonPath, "utf8");
const packageJson = JSON.parse(packageJsonString);
return packageJson.version;
}
import { copyFile, stat } from "node:fs/promises";
import { execSync } from "child_process";
/**
* The name of the node_modules archive that will be vendored with the plugin.
* @param version API version to use in the filename
* @returns The name of the node_modules bundle file
*/
export function getVendorBundleName(): string {
const version = getVersion();
export function getVendorBundleName(version: string): string {
return `node_modules-for-v${version}.tar.xz`;
}
/**
* Get the full path where the vendor bundle should be stored
* @param version API version to use in the filename
* @returns The full path to the vendor bundle
*/
export function getVendorFullPath(version: string): string {
return join(vendorStorePath, getVendorBundleName(version));
}
/**
* Create a tarball of the node_modules for local development
* @param outputPath Path to write the tarball to
*/
async function createNodeModulesTarball(outputPath: string): Promise<void> {
console.log(`Creating node_modules tarball at ${outputPath}`);
try {
// Create a tarball of the node_modules directly from the API directory
const apiNodeModules = join(process.cwd(), "..", "api", "node_modules");
if (existsSync(apiNodeModules)) {
console.log(`Found API node_modules at ${apiNodeModules}, creating tarball...`);
execSync(`tar -cJf "${outputPath}" -C "${join(process.cwd(), "..", "api")}" node_modules`);
console.log(`Successfully created node_modules tarball at ${outputPath}`);
return;
}
throw new Error(`API node_modules not found at ${apiNodeModules}`);
} catch (error) {
console.error(`Failed to create node_modules tarball: ${error}`);
throw error;
}
}
/**
* Prepare a versioned bundle of the API's node_modules to vendor dependencies.
*
* It expects a generic `packed-node-modules.tar.xz` archive to be available in the `startingDir`.
* It copies this archive to the `deployDir` directory and adds a version to the filename.
* It does not actually create the packed node_modules archive; that is done inside the API's build script.
* It first tries to use the `packed-node-modules.tar.xz` from the mounted volume.
* If that fails, it checks the parent API directory and tries to create a tarball from node_modules.
*
* After this operation, the vendored node_modules will be available inside the `deployDir`.
*
* @param apiVersion Required API version to use for the vendor bundle
*/
export async function bundleVendorStore(): Promise<void> {
const storeArchive = join(startingDir, "packed-node-modules.tar.xz");
const vendorStoreTarPath = join(deployDir, getVendorBundleName());
await copyFile(storeArchive, vendorStoreTarPath);
export async function bundleVendorStore(apiVersion: string): Promise<void> {
// Ensure deploy directory exists
mkdirSync(deployDir, { recursive: true });
const vendorStoreTarPath = join(deployDir, getVendorBundleName(apiVersion));
// Possible locations for the node modules archive
const possibleLocations = [
join(startingDir, "node-modules-archive/packed-node-modules.tar.xz"), // Docker mount
join(process.cwd(), "..", "api", "deploy", "node-modules-archive", "packed-node-modules.tar.xz") // Direct path to API deploy
];
let foundArchive = false;
for (const archivePath of possibleLocations) {
try {
console.log(`Checking for vendor store at ${archivePath}`);
if (!existsSync(archivePath)) {
console.log(`Archive not found at ${archivePath}`);
continue;
}
const stats = await stat(archivePath);
if (!stats.isFile()) {
console.log(`${archivePath} exists but is not a file`);
continue;
}
console.log(`Copying vendor store from ${archivePath} to ${vendorStoreTarPath}`);
await copyFile(archivePath, vendorStoreTarPath);
console.log(`Successfully copied vendor store to ${vendorStoreTarPath}`);
foundArchive = true;
break;
} catch (error) {
console.log(`Error checking ${archivePath}: ${error}`);
}
}
if (!foundArchive) {
console.log("Could not find existing node_modules archive, attempting to create one");
// Create a temporary archive in the deploy directory
const tempArchivePath = join(deployDir, "temp-node-modules.tar.xz");
await createNodeModulesTarball(tempArchivePath);
await copyFile(tempArchivePath, vendorStoreTarPath);
}
}
+38
View File
@@ -0,0 +1,38 @@
import { Command } from "commander";
import { z } from "zod";
/**
* Common base environment fields shared between different build setups
*/
export const baseEnvSchema = z.object({
ci: z.boolean().optional().default(false),
apiVersion: z.string(),
baseUrl: z.string().url(),
tag: z.string().optional().default(''),
});
export type BaseEnv = z.infer<typeof baseEnvSchema>;
/**
* Generate a default base URL for local development
*/
export const getDefaultBaseUrl = (): string => {
return process.env.CI === "true"
? "This is a CI build, please set the base URL manually"
: `http://${process.env.HOST_LAN_IP || 'localhost'}:5858`;
};
/**
* Common CLI options shared across different command setups
*/
export const addCommonOptions = (program: Command) => {
return program
.option("--ci", "CI mode", process.env.CI === "true")
.requiredOption("--api-version <version>", "API version", process.env.API_VERSION)
.requiredOption(
"--base-url <url>",
"Base URL for assets",
getDefaultBaseUrl()
)
.option("--tag <tag>", "Tag (used for PR and staging builds)", process.env.TAG);
};
+92 -24
View File
@@ -4,12 +4,11 @@ import { Command } from "commander";
import { getStagingChangelogFromGit } from "../utils/changelog";
import { createHash } from "node:crypto";
import { getTxzPath } from "../utils/paths";
import { existsSync } from "node:fs";
import { join } from "node:path";
import { baseEnvSchema, addCommonOptions } from "./common-environment";
const safeParseEnvSchema = z.object({
ci: z.boolean().optional(),
baseUrl: z.string().url(),
tag: z.string().optional().default(''),
const safeParseEnvSchema = baseEnvSchema.extend({
txzPath: z.string().refine((val) => val.endsWith(".txz"), {
message: "TXZ Path must end with .txz",
}),
@@ -28,6 +27,85 @@ const pluginEnvSchema = safeParseEnvSchema.extend({
export type PluginEnv = z.infer<typeof pluginEnvSchema>;
/**
* Resolves the txz path, trying multiple possible locations based on apiVersion
* Also verifies the file exists and is accessible
* @param txzPath Initial txz path to check
* @param apiVersion API version to use for alternative path
* @param isCi Whether we're running in CI
* @returns Object containing the resolved txz path and SHA256 hash
* @throws Error if no valid txz file can be found
*/
export const resolveTxzPath = async (txzPath: string, apiVersion: string, isCi?: boolean): Promise<{path: string, sha256: string}> => {
if (existsSync(txzPath)) {
await access(txzPath, constants.F_OK);
console.log("Reading txz file from:", txzPath);
const txzFile = await readFile(txzPath);
if (!txzFile || txzFile.length === 0) {
throw new Error(`TXZ file is empty: ${txzPath}`);
}
return {
path: txzPath,
sha256: getSha256(txzFile)
};
}
console.log(`TXZ path not found at: ${txzPath}`);
console.log(`Attempting to find TXZ using apiVersion: ${apiVersion}`);
// Try different formats of generated TXZ name
const deployDir = join(process.cwd(), "deploy");
// Try with exact apiVersion format
const alternativePaths = [
join(deployDir, `dynamix.unraid.net-${apiVersion}-x86_64-1.txz`),
];
// In CI, we sometimes see unusual filenames, so try a glob-like approach
if (isCi) {
console.log("Checking for possible TXZ files in deploy directory");
try {
// Using node's filesystem APIs to scan the directory
const fs = require('fs');
const deployFiles = fs.readdirSync(deployDir);
// Find any txz file that contains the apiVersion
for (const file of deployFiles) {
if (file.endsWith('.txz') &&
file.includes('dynamix.unraid.net') &&
file.includes(apiVersion.split('+')[0])) {
alternativePaths.push(join(deployDir, file));
}
}
} catch (error) {
console.log(`Error scanning deploy directory: ${error}`);
}
}
// Check each path
for (const path of alternativePaths) {
if (existsSync(path)) {
console.log(`Found TXZ at: ${path}`);
await access(path, constants.F_OK);
console.log("Reading txz file from:", path);
const txzFile = await readFile(path);
if (!txzFile || txzFile.length === 0) {
console.log(`TXZ file is empty: ${path}, trying next alternative`);
continue;
}
return {
path,
sha256: getSha256(txzFile)
};
}
console.log(`Could not find TXZ at: ${path}`);
}
// If we get here, we couldn't find a valid txz file
throw new Error(`Could not find any valid TXZ file. Tried original path: ${txzPath} and alternatives.`);
};
export const validatePluginEnv = async (
envArgs: Record<string, any>
): Promise<PluginEnv> => {
@@ -48,15 +126,10 @@ export const validatePluginEnv = async (
: await getStagingChangelogFromGit(safeEnv);
}
if (safeEnv.txzPath) {
await access(safeEnv.txzPath, constants.F_OK);
console.log("Reading txz file from:", safeEnv.txzPath);
const txzFile = await readFile(safeEnv.txzPath);
if (!txzFile || txzFile.length === 0) {
throw new Error(`TXZ Path is empty: ${safeEnv.txzPath}`);
}
envArgs.txzSha256 = getSha256(txzFile);
}
// Resolve and validate the txz path
const { path, sha256 } = await resolveTxzPath(safeEnv.txzPath, safeEnv.apiVersion, safeEnv.ci);
envArgs.txzPath = path;
envArgs.txzSha256 = sha256;
const validatedEnv = pluginEnvSchema.parse(envArgs);
@@ -87,27 +160,22 @@ export const setupPluginEnv = async (argv: string[]): Promise<PluginEnv> => {
// CLI setup for plugin environment
const program = new Command();
// Add common options
addCommonOptions(program);
// Add plugin-specific options
program
.requiredOption(
"--base-url <url>",
"Base URL - will be used to determine the bucket, and combined with the tag (if set) to form the final URL",
process.env.CI === "true"
? "This is a CI build, please set the base URL manually"
: `http://${process.env.HOST_LAN_IP}:5858`
)
.option(
"--txz-path <path>",
"Path to built package, will be used to generate the SHA256 and renamed with the plugin version",
getTxzPath({ startingDir: process.cwd() })
getTxzPath({ startingDir: process.cwd(), pluginVersion: process.env.API_VERSION })
)
.option(
"--plugin-version <version>",
"Plugin Version in the format YYYY.MM.DD.HHMM",
getPluginVersion()
)
.option("--tag <tag>", "Tag (used for PR and staging builds)", process.env.TAG)
.option("--release-notes-path <path>", "Path to release notes file")
.option("--ci", "CI mode", process.env.CI === "true")
.parse(argv);
const options = program.opts();
+6 -4
View File
@@ -3,9 +3,9 @@ import { z } from "zod";
import { Command } from "commander";
import { startingDir } from "../utils/consts";
import { deployDir } from "../utils/paths";
import { baseEnvSchema, addCommonOptions } from "./common-environment";
const txzEnvSchema = z.object({
ci: z.boolean().optional().default(false),
const txzEnvSchema = baseEnvSchema.extend({
skipValidation: z
.string()
.optional()
@@ -33,11 +33,13 @@ export const setupTxzEnv = async (argv: string[]): Promise<TxzEnv> => {
// CLI setup for TXZ environment
const program = new Command();
// Add common options first
addCommonOptions(program);
// Add TXZ-specific options
program
.option("--skip-validation", "Skip validation", "false")
.option("--ci", "CI mode", process.env.CI === "true")
.option("--compress, -z", "Compress level", "1")
.parse(argv);
const options = program.opts();
+4 -4
View File
@@ -1,4 +1,4 @@
import { getTxzName, LOCAL_BUILD_TAG, pluginNameWithExt } from "./consts";
import { getTxzName, LOCAL_BUILD_TAG, pluginNameWithExt, defaultArch, defaultBuild } from "./consts";
// Define a common interface for URL parameters
interface UrlParams {
@@ -7,7 +7,7 @@ interface UrlParams {
}
interface TxzUrlParams extends UrlParams {
pluginVersion: string;
apiVersion: string;
}
/**
@@ -44,7 +44,7 @@ export const getPluginUrl = (params: UrlParams): string =>
/**
* Get the URL for the main TXZ file
* ex. returns = BASE_URL/TAG/dynamix.unraid.net-4.1.3.txz
* ex. returns = BASE_URL/TAG/dynamix.unraid.net-4.1.3-x86_64-1.txz
*/
export const getMainTxzUrl = (params: TxzUrlParams): string =>
getAssetUrl(params, getTxzName(params.pluginVersion));
getAssetUrl(params, getTxzName(params.apiVersion, defaultArch, defaultBuild));
+7 -2
View File
@@ -1,8 +1,13 @@
export const pluginName = "dynamix.unraid.net" as const;
export const pluginNameWithExt = `${pluginName}.plg` as const;
export const getTxzName = (version?: string) =>
version ? `${pluginName}-${version}.txz` : `${pluginName}.txz`;
// Default architecture and build number for Slackware package
export const defaultArch = "x86_64" as const;
export const defaultBuild = "1" as const;
// Get the txz name following Slackware naming convention: name-version-arch-build.txz
export const getTxzName = (version?: string, arch: string = defaultArch, build: string = defaultBuild) =>
version ? `${pluginName}-${version}-${arch}-${build}.txz` : `${pluginName}.txz`;
export const startingDir = process.cwd();
export const BASE_URLS = {
+59
View File
@@ -0,0 +1,59 @@
import { join } from "path";
import { existsSync, mkdirSync, createWriteStream, readFileSync } from "fs";
import { writeFile, readFile } from "fs/promises";
import { get } from "https";
import { $ } from "zx";
import { startingDir } from "./consts";
const findNvmrc = () => {
const nvmrcPaths = [
join(startingDir, "..", ".nvmrc"),
join(startingDir, ".nvmrc"),
];
for (const nvmrcPath of nvmrcPaths) {
if (existsSync(nvmrcPath)) {
return nvmrcPath;
}
}
throw new Error("NVMRC file not found");
}
// Read Node.js version from .nvmrc
const NVMRC_PATH = findNvmrc();
console.log(`NVMRC_PATH: ${NVMRC_PATH}`);
const NODE_VERSION = readFileSync(NVMRC_PATH, "utf8").trim();
const NODE_FILENAME = `node-v${NODE_VERSION}-linux-x64.tar.xz`;
const NODE_URL = `https://nodejs.org/download/release/v${NODE_VERSION}/${NODE_FILENAME}`;
const NODE_DEST = join(startingDir, "source", "dynamix.unraid.net", "usr", "local");
const NODE_VERSION_FILE = join(NODE_DEST, ".node-version");
async function fetchFile(url: string, dest: string) {
return new Promise((resolve, reject) => {
const file = createWriteStream(dest);
get(url, (response) => {
if (response.statusCode !== 200) {
reject(new Error(`Failed to get '${url}' (${response.statusCode})`));
return;
}
response.pipe(file);
file.on("finish", () => file.close(resolve));
file.on("error", reject);
}).on("error", reject);
});
}
export async function ensureNodeJs() {
let currentVersion: string | null = null;
if (existsSync(NODE_VERSION_FILE)) {
currentVersion = (await readFile(NODE_VERSION_FILE, "utf8")).trim();
}
if (currentVersion !== NODE_VERSION) {
mkdirSync(NODE_DEST, { recursive: true });
if (!existsSync(NODE_FILENAME)) {
await fetchFile(NODE_URL, NODE_FILENAME);
}
// Extract Node.js excluding include/node and share/doc/node directories
await $`tar --strip-components=1 -xf ${NODE_FILENAME} --exclude="*/include/node" --exclude="*/share/doc/node" --exclude="*/README.md" --exclude="*/LICENSE" --exclude="*/CHANGELOG.md" -C ${NODE_DEST}`;
await writeFile(NODE_VERSION_FILE, NODE_VERSION, "utf8");
}
}
+2
View File
@@ -25,6 +25,8 @@ export const apiDir = join(
"unraid-api"
);
export const vendorStorePath = "/boot/config/plugins/dynamix.my.servers";
/**
* Get the path to the root plugin directory
* @param startingDir - The starting directory