mirror of
https://github.com/unraid/api.git
synced 2026-05-04 14:12:08 -05:00
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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user