From 9136bdaf18bdc2abc5418afe3e5caa93ed610c6b Mon Sep 17 00:00:00 2001 From: Ajit Mehrotra Date: Wed, 11 Mar 2026 13:45:27 -0400 Subject: [PATCH] fix(plugin): isolate docker builder assets - Purpose: make the plugin docker builder work from a forked Codex worktree without mutating protected mount targets. - Before: docker run mounted UI build outputs directly into nested paths under plugin/source and relied on the local checkout .git file. - Problem: the forked worktree was detached, plugin/.git was invalid for git lookups, and Docker failed to create nested mountpoints under the mounted source tree. - Change: move the UI asset mounts to a neutral container path, read git metadata from the repo worktree, and stage a temporary writable copy of plugin/source before packaging. - How: dc.sh now resolves git state through the parent repo and exports compose env vars, docker-compose mounts assets under /app/.mounted-assets, and build-txz syncs those assets into a temp build tree before makepkg runs. --- plugin/builder/build-txz.ts | 105 ++++++++++++++++++++++++++---------- plugin/docker-compose.yml | 6 +-- plugin/scripts/dc.sh | 12 ++++- 3 files changed, 89 insertions(+), 34 deletions(-) diff --git a/plugin/builder/build-txz.ts b/plugin/builder/build-txz.ts index ce1bafaa7f..7927e792dd 100644 --- a/plugin/builder/build-txz.ts +++ b/plugin/builder/build-txz.ts @@ -1,17 +1,73 @@ -import { join } from "path"; +import { dirname, join } from "path"; +import { tmpdir } from "os"; import { $, cd } from "zx"; import { existsSync } from "node:fs"; -import { readdir, writeFile } from "node:fs/promises"; +import { cp, mkdir, readdir, rm, 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"; import { validateStandaloneManifest, getStandaloneManifestPath } from "./utils/manifest-validator"; +const mountedAssetsDir = join(startingDir, ".mounted-assets"); +const mountedUuiDir = join(mountedAssetsDir, "uui"); +const mountedStandaloneDir = join(mountedAssetsDir, "standalone"); +const hostSourceDir = join(startingDir, "source"); +const buildRootDir = join(tmpdir(), "connect-plugin-build"); +const buildSourceDir = join(buildRootDir, "source"); +const buildPluginDir = join(buildSourceDir, pluginName); +const buildWebcomponentsDir = join( + buildSourceDir, + "dynamix.unraid.net", + "usr", + "local", + "emhttp", + "plugins", + "dynamix.my.servers", + "unraid-components" +); +const buildApiDir = join(buildPluginDir, "usr", "local", "unraid-api"); + +const prepareBuildSourceDir = async () => { + await rm(buildRootDir, { force: true, recursive: true }); + await mkdir(buildRootDir, { recursive: true }); + await cp(hostSourceDir, buildSourceDir, { force: true, recursive: true }); +}; + +const syncMountedAssetDir = async ({ + sourceDir, + targetDir, + label, +}: { + sourceDir: string; + targetDir: string; + label: string; +}) => { + if (!existsSync(sourceDir)) { + return; + } + + await rm(targetDir, { force: true, recursive: true }); + await mkdir(dirname(targetDir), { recursive: true }); + await cp(sourceDir, targetDir, { force: true, recursive: true }); + console.log(`Synced ${label}: ${sourceDir} -> ${targetDir}`); +}; + +const syncMountedAssets = async () => { + await syncMountedAssetDir({ + sourceDir: mountedUuiDir, + targetDir: join(buildWebcomponentsDir, "uui"), + label: "web components", + }); + await syncMountedAssetDir({ + sourceDir: mountedStandaloneDir, + targetDir: join(buildWebcomponentsDir, "standalone"), + label: "standalone assets", + }); +}; // Check for manifest files in expected locations const findManifestFiles = async (dir: string): Promise => { @@ -58,8 +114,7 @@ const storeVendorArchiveInfo = async (version: string, vendorUrl: string, vendor // Create a config directory in the source tree const configDir = join( - startingDir, - "source", + buildSourceDir, "dynamix.unraid.net", "usr", "local", @@ -106,27 +161,19 @@ const validateSourceDir = async (validatedEnv: TxzEnv) => { if (!validatedEnv.ci) { console.log("Validating TXZ source directory"); } - const sourceDir = join(startingDir, "source"); - if (!existsSync(sourceDir)) { - throw new Error(`Source directory ${sourceDir} does not exist`); + if (!existsSync(hostSourceDir)) { + throw new Error(`Source directory ${hostSourceDir} does not exist`); } - // Validate existence of webcomponent files: - // source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components - const webcomponentDir = join( - sourceDir, - "dynamix.unraid.net", - "usr", - "local", - "emhttp", - "plugins", - "dynamix.my.servers", - "unraid-components" - ); - if (!existsSync(webcomponentDir)) { - throw new Error(`Webcomponent directory ${webcomponentDir} does not exist`); + await prepareBuildSourceDir(); + await syncMountedAssets(); + + if (!existsSync(buildWebcomponentsDir)) { + throw new Error( + `Webcomponent directory ${buildWebcomponentsDir} does not exist` + ); } - const manifestFiles = await findManifestFiles(webcomponentDir); + const manifestFiles = await findManifestFiles(buildWebcomponentsDir); const hasStandaloneManifest = manifestFiles.some(file => file === "standalone.manifest.json" || file === "standalone/standalone.manifest.json" ); @@ -141,7 +188,7 @@ const validateSourceDir = async (validatedEnv: TxzEnv) => { } // Validate the manifest contents - const manifestPath = getStandaloneManifestPath(webcomponentDir); + const manifestPath = getStandaloneManifestPath(buildWebcomponentsDir); if (manifestPath) { const validation = await validateStandaloneManifest(manifestPath); @@ -163,15 +210,15 @@ const validateSourceDir = async (validatedEnv: TxzEnv) => { console.log("✅ Standalone manifest validation passed"); } - if (!existsSync(apiDir)) { - throw new Error(`API directory ${apiDir} does not exist`); + if (!existsSync(buildApiDir)) { + throw new Error(`API directory ${buildApiDir} does not exist`); } - const packageJson = join(apiDir, "package.json"); + const packageJson = join(buildApiDir, "package.json"); if (!existsSync(packageJson)) { throw new Error(`API package.json file ${packageJson} does not exist`); } // Now CHMOD the api/dist directory files to allow execution - await $`chmod +x ${apiDir}/dist/*.js`; + await $`chmod +x ${buildApiDir}/dist/*.js`; }; const buildTxz = async (validatedEnv: TxzEnv) => { @@ -202,7 +249,7 @@ const buildTxz = async (validatedEnv: TxzEnv) => { // 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)); + await cd(buildPluginDir); $.verbose = true; // Create the package using the default package name diff --git a/plugin/docker-compose.yml b/plugin/docker-compose.yml index 3ee13f7594..71e9db861f 100644 --- a/plugin/docker-compose.yml +++ b/plugin/docker-compose.yml @@ -11,8 +11,8 @@ services: - ../.rclone-version:/app/.rclone-version - ./source:/app/source - ./scripts:/app/scripts - - ../unraid-ui/dist-wc:/app/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/uui - - ../web/dist:/app/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/standalone + - ../unraid-ui/dist-wc:/app/.mounted-assets/uui + - ../web/dist:/app/.mounted-assets/standalone - ../api/deploy/release/:/app/source/dynamix.unraid.net/usr/local/unraid-api # Use the release dir instead of pack to allow watcher to not try to build with node_modules stdin_open: true # equivalent to -i tty: true # equivalent to -t @@ -20,4 +20,4 @@ services: - HOST_LAN_IP=${HOST_LAN_IP} - CI=${CI:-false} - TAG=${TAG} - - API_VERSION=${API_VERSION} \ No newline at end of file + - API_VERSION=${API_VERSION} diff --git a/plugin/scripts/dc.sh b/plugin/scripts/dc.sh index e5887d1a2e..ea64877a8d 100755 --- a/plugin/scripts/dc.sh +++ b/plugin/scripts/dc.sh @@ -1,5 +1,12 @@ #!/bin/bash +REPO_ROOT=$(cd .. && pwd) +REPO_GIT_DIR=${GIT_DIR:-../.git} + +git_repo() { + git --git-dir="$REPO_GIT_DIR" --work-tree="$REPO_ROOT" "$@" +} + # Get host IP based on platform if [[ "$OSTYPE" == "darwin"* ]]; then # macOS @@ -17,10 +24,11 @@ fi CI=${CI:-false} TAG="LOCAL_PLUGIN_BUILD" -IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') +IS_TAGGED=$(git_repo describe --tags --abbrev=0 --exact-match 2>/dev/null || echo '') PACKAGE_LOCK_VERSION=$(jq -r '.version' package.json) -GIT_SHA=$(git rev-parse --short HEAD) +GIT_SHA=$(git_repo rev-parse --short HEAD) API_VERSION=$([[ -n "$IS_TAGGED" ]] && echo "$PACKAGE_LOCK_VERSION" || echo "${PACKAGE_LOCK_VERSION}+${GIT_SHA}") +export HOST_LAN_IP CI TAG API_VERSION # Define container name for easier management CONTAINER_NAME="plugin-builder"