From 9012d7a7d32b0702b1c05c5837cf42a4a91ee03c Mon Sep 17 00:00:00 2001 From: Adam Stolcenburg Date: Wed, 17 Jun 2026 15:08:29 +0200 Subject: [PATCH] Override license paths instead of resetting them This avoids generating .wh.* files by umoci that are unreadable by rsync. Ref: #RDKEAPPRT-861 --- bolt/src/PackageBuilder.cjs | 36 ++++++++++++++++++++++++------- bolt/src/utils.cjs | 42 +++++++++++++++++++++++++++++-------- 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/bolt/src/PackageBuilder.cjs b/bolt/src/PackageBuilder.cjs index 112acb5..10055d4 100644 --- a/bolt/src/PackageBuilder.cjs +++ b/bolt/src/PackageBuilder.cjs @@ -17,8 +17,11 @@ * limitations under the License. */ -const { readFileSync } = require('node:fs'); -const { exec, removeUnder } = require('./utils.cjs'); +const { lstatSync, readFileSync } = require('node:fs'); +const { join } = require('node:path'); +const { exec, execv, removeUnder, resolveUnder, touchUnder } = require('./utils.cjs'); + +const SYNC_FLAGS = ['-rlpgoDX']; class PackageBuilder { constructor(workDir) { @@ -28,19 +31,23 @@ class PackageBuilder { this.initialized = false; } - merge(rootfs, resetPaths = [], excludePaths = []) { - for (const path of excludePaths) { + merge(rootfs, override = [], exclude = []) { + for (const path of exclude) { if (!removeUnder(rootfs, path)) { console.warn(`Excluded path not found, skipping: ${path}`); } } if (this.initialized) { - for (const path of resetPaths) { - if (!removeUnder(`${this.workDir}/bundle/rootfs`, path)) { - console.warn(`Reset path not found, skipping: ${path}`); + const touched = []; + for (const path of override) { + if (touchUnder(rootfs, path)) { + touched.push(path); + } else { + console.warn(`Override path not found, skipping: ${path}`); } } - exec(`rsync -crlpgoDX ${rootfs}/ ${this.workDir}/bundle/rootfs`); + execv('rsync', [...SYNC_FLAGS, '-c', '--', `${rootfs}/`, join(this.workDir, 'bundle', 'rootfs')]); + this.resync(rootfs, touched, [...SYNC_FLAGS, '-t']); exec(`umoci repack --refresh-bundle --image ${this.workDir}/oci ${this.workDir}/bundle`); } else { exec(`umoci insert --rootless --image ${this.workDir}/oci ${rootfs} /`); @@ -49,6 +56,19 @@ class PackageBuilder { } } + resync(rootfs, paths, flags) { + const bundleRoot = join(this.workDir, 'bundle', 'rootfs'); + for (const path of paths) { + const source = resolveUnder(rootfs, path); + const dest = resolveUnder(bundleRoot, path); + if (!source || !dest || !lstatSync(source, { throwIfNoEntry: false })?.isDirectory()) { + console.warn(`Resync path not found, skipping: ${path}`); + continue; + } + execv('rsync', [...flags, '--', `${source}/`, dest]); + } + } + finish(layer) { exec(`umoci gc --layout=${this.workDir}/oci`); const index = JSON.parse(readFileSync(`${this.workDir}/oci/index.json`)); diff --git a/bolt/src/utils.cjs b/bolt/src/utils.cjs index 0548fa7..ce2aaf9 100644 --- a/bolt/src/utils.cjs +++ b/bolt/src/utils.cjs @@ -17,7 +17,7 @@ * limitations under the License. */ -const { renameSync, copyFileSync, linkSync, unlinkSync, mkdtempSync, statSync, lstatSync, rmSync, realpathSync } = require('node:fs'); +const { renameSync, copyFileSync, linkSync, unlinkSync, mkdtempSync, statSync, lstatSync, rmSync, realpathSync, lutimesSync, readdirSync } = require('node:fs'); const { join, dirname, basename, isAbsolute, resolve, relative, sep } = require('node:path'); const { execSync, execFileSync } = require('node:child_process'); const config = require('./config.cjs'); @@ -117,29 +117,51 @@ function assertFile(path) { } } -function removeUnder(baseDir, relPath) { - const base = realpathSync(resolve(baseDir)); - const target = resolve(base, relPath); - - let realTarget; +function resolveUnder(baseDir, relPath) { + let base, realTarget; try { + base = realpathSync(resolve(baseDir)); + const target = resolve(base, relPath); realTarget = join(realpathSync(dirname(target)), basename(target)); } catch (err) { - if (err.code === 'ENOENT') return false; + if (err.code === 'ENOENT') return null; throw err; } const rel = relative(base, realTarget); if (rel === '' || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) { - throw new Error(`Refusing to remove ${JSON.stringify(relPath)}: resolves to or outside ${baseDir}`); + throw new Error(`Path ${JSON.stringify(relPath)} resolves to or outside ${baseDir}`); } - if (lstatSync(realTarget, { throwIfNoEntry: false })) { + return realTarget; +} + +function removeUnder(baseDir, relPath) { + const realTarget = resolveUnder(baseDir, relPath); + if (realTarget && lstatSync(realTarget, { throwIfNoEntry: false })) { rmSync(realTarget, { recursive: true, force: true }); return true; } return false; } +function touchTree(target, now = new Date()) { + lutimesSync(target, now, now); + if (lstatSync(target).isDirectory()) { + for (const entry of readdirSync(target)) { + touchTree(join(target, entry), now); + } + } +} + +function touchUnder(baseDir, relPath) { + const realTarget = resolveUnder(baseDir, relPath); + if (realTarget && lstatSync(realTarget, { throwIfNoEntry: false })) { + touchTree(realTarget); + return true; + } + return false; +} + exports.exec = exec; exports.execv = execv; exports.execNoOutput = execNoOutput; @@ -150,3 +172,5 @@ exports.makeWorkDir = makeWorkDir; exports.resolvePath = resolvePath; exports.assertFile = assertFile; exports.removeUnder = removeUnder; +exports.resolveUnder = resolveUnder; +exports.touchUnder = touchUnder;