Move trampoline files (boot-hook, watcher, overlay) into trampoline/ subdirectory. Add upstream Luma v2.3.1 source at repo root with our additions: - Page::Ha (4th nav tab) with placeholder empty state - HA tab config schema and default entity mapping (ha-home/) - Nav keys renamed: "home" → "start", new "ha" tab between Apps and Settings - All state.rs/view.rs/main.rs/tests.rs updated for 4-tab navigation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
157 lines
6.4 KiB
JavaScript
157 lines
6.4 KiB
JavaScript
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { createHash } from 'node:crypto';
|
|
import { prepareWebosRuntime, verifyWebosSdl } from './webos-runtime.mjs';
|
|
import { normalizeAndVerifyPackage } from './ipk.mjs';
|
|
|
|
const root = path.resolve(import.meta.dirname, '..');
|
|
const highResolution = process.argv.includes('--4k');
|
|
const manifest = JSON.parse(await fs.readFile(path.join(root, 'appinfo.json'), 'utf8'));
|
|
const project = JSON.parse(await fs.readFile(path.join(root, 'package.json'), 'utf8'));
|
|
if (
|
|
project.version !== manifest.version ||
|
|
manifest.id !== 'com.ecklf.luma' ||
|
|
manifest.type !== 'native' ||
|
|
manifest.main !== 'luma'
|
|
) {
|
|
throw new Error(
|
|
'Native package metadata must use com.ecklf.luma, main luma, and synchronized versions.',
|
|
);
|
|
}
|
|
const binary = path.join(root, 'target/armv7-unknown-linux-gnueabi/release/luma');
|
|
const bytes = await fs.readFile(binary);
|
|
if (
|
|
bytes.length < 52 ||
|
|
bytes.subarray(0, 4).toString('hex') !== '7f454c46' ||
|
|
bytes[4] !== 1 ||
|
|
bytes[5] !== 1 ||
|
|
bytes.readUInt16LE(18) !== 40 ||
|
|
(bytes.readUInt32LE(36) & 0x400) !== 0
|
|
) {
|
|
throw new Error(
|
|
'Packaging requires a 32-bit little-endian ARM soft-float ELF from build-webos.sh.',
|
|
);
|
|
}
|
|
const out = path.join(root, '.build');
|
|
const dist = path.join(root, 'dist');
|
|
let vectorManifest;
|
|
try {
|
|
vectorManifest = JSON.parse(
|
|
await fs.readFile(path.join(out, 'assets/vector-artwork.json'), 'utf8'),
|
|
);
|
|
} catch (error) {
|
|
throw new Error('Prepare vector artwork with pnpm assets:vector before packaging.', {
|
|
cause: error,
|
|
});
|
|
}
|
|
if (
|
|
vectorManifest.version !== 1 ||
|
|
!Array.isArray(vectorManifest.assets) ||
|
|
vectorManifest.assets.length === 0
|
|
) {
|
|
throw new Error('Invalid native vector artwork manifest. Regenerate native assets.');
|
|
}
|
|
for (const file of vectorManifest.assets) {
|
|
if (typeof file !== 'string' || !/^(logos|ui-icons)\/[a-z0-9-]+\.lvec$/.test(file)) {
|
|
throw new Error('Invalid native vector artwork path.');
|
|
}
|
|
const mesh = await fs.readFile(path.join(out, 'assets', file));
|
|
if (
|
|
mesh.length < 20 ||
|
|
mesh.subarray(0, 8).toString() !== 'LUMAVEC1' ||
|
|
mesh.readUInt32LE(16) === 0 ||
|
|
mesh.readUInt32LE(16) % 3 !== 0 ||
|
|
mesh.length !== 20 + mesh.readUInt32LE(16) * 36
|
|
) {
|
|
throw new Error(`Invalid native vector geometry: ${file}`);
|
|
}
|
|
}
|
|
const stage = path.join(out, 'package');
|
|
await fs.rm(stage, { recursive: true, force: true });
|
|
await fs.mkdir(stage, { recursive: true });
|
|
await fs.mkdir(dist, { recursive: true });
|
|
await fs.copyFile(binary, path.join(stage, manifest.main));
|
|
await fs.chmod(path.join(stage, manifest.main), 0o755);
|
|
await fs.cp(path.join(out, 'assets'), path.join(stage, 'assets'), {
|
|
recursive: true,
|
|
filter: (source) =>
|
|
path.basename(source) !== '.DS_Store' &&
|
|
// Protect direct packaging calls that reuse an older generated asset cache.
|
|
!['app-icons', 'icons.json', 'colors.json', 'splash'].includes(path.basename(source)) &&
|
|
(highResolution || !source.endsWith('@2x.jpg')) &&
|
|
!(
|
|
source.endsWith('.png') && ['logos', 'ui-icons'].includes(path.basename(path.dirname(source)))
|
|
),
|
|
});
|
|
await fs.copyFile(path.join(out, 'assets/app-icon.png'), path.join(stage, 'app-icon.png'));
|
|
await fs.copyFile(
|
|
path.join(out, 'assets/app-icon-large.png'),
|
|
path.join(stage, 'app-icon-large.png'),
|
|
);
|
|
await fs.copyFile(path.join(root, 'tv/autostart.sh'), path.join(stage, 'autostart.sh'));
|
|
// Splash images: one per background, plus splash.png (the default background) read by webOS.
|
|
await fs.mkdir(path.join(stage, 'splash'), { recursive: true });
|
|
for (const file of await fs.readdir(path.join(out, 'assets/splash')))
|
|
await fs.copyFile(path.join(out, 'assets/splash', file), path.join(stage, 'splash', file));
|
|
await fs.copyFile(path.join(out, 'assets/splash/dunes.png'), path.join(stage, 'splash.png'));
|
|
await fs.chmod(path.join(stage, 'autostart.sh'), 0o755);
|
|
for (const file of ['home.sh', 'home.cjs']) {
|
|
await fs.copyFile(path.join(root, 'tv', file), path.join(stage, file));
|
|
await fs.chmod(path.join(stage, file), file.endsWith('.sh') ? 0o755 : 0o644);
|
|
}
|
|
await fs.copyFile(path.join(root, 'license'), path.join(stage, 'LICENSE'));
|
|
await fs.copyFile(path.join(root, 'assets.md'), path.join(stage, 'ASSETS.md'));
|
|
await fs.mkdir(path.join(stage, 'lib'));
|
|
async function copyLibrary(directory, name) {
|
|
const library = await fs.readFile(path.join(directory, name));
|
|
if (
|
|
library.length < 52 ||
|
|
library.subarray(0, 4).toString('hex') !== '7f454c46' ||
|
|
library[4] !== 1 ||
|
|
library[5] !== 1 ||
|
|
library.readUInt16LE(18) !== 40 ||
|
|
(library.readUInt32LE(36) & 0x400) !== 0
|
|
) {
|
|
throw new Error(`Not an ARM soft-float webOS library: ${name}`);
|
|
}
|
|
if (/^libSDL2(?:-2\.0)?\.so(?:\.[0-9]+)*$/.test(name)) verifyWebosSdl(library);
|
|
await fs.writeFile(path.join(stage, 'lib', name), library);
|
|
}
|
|
// Explicitly selected ARM libraries can override the pinned SDL-webOS runtime.
|
|
if (process.env.WEBOS_NATIVE_LIBS) {
|
|
const directory = path.resolve(process.env.WEBOS_NATIVE_LIBS);
|
|
for (const name of await fs.readdir(directory)) {
|
|
if (!/\.so(?:\.[0-9]+)*$/.test(name)) continue;
|
|
await copyLibrary(directory, name);
|
|
}
|
|
}
|
|
const sdlName = 'libSDL2-2.0.so.0';
|
|
if (!(await fs.readdir(path.join(stage, 'lib'))).includes(sdlName)) {
|
|
const runtime = await prepareWebosRuntime();
|
|
await copyLibrary(path.join(runtime, 'lib'), sdlName);
|
|
}
|
|
await fs.cp(path.join(root, 'assets/licenses'), path.join(stage, 'licenses'), { recursive: true });
|
|
await fs.writeFile(path.join(stage, 'appinfo.json'), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
const state = path.join(out, 'cli-state');
|
|
await fs.mkdir(state, { recursive: true });
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[
|
|
path.join(root, 'node_modules/@webos-tools/cli/bin/ares-package.js'),
|
|
'--no-minify',
|
|
'--outdir',
|
|
dist,
|
|
stage,
|
|
],
|
|
{ cwd: root, env: { ...process.env, APPDATA: state }, stdio: 'inherit', timeout: 120000 },
|
|
);
|
|
if (result.error) throw result.error;
|
|
if (result.status !== 0) process.exit(result.status ?? 1);
|
|
const filename = `${manifest.id}_${manifest.version}_arm.ipk`;
|
|
const packaged = normalizeAndVerifyPackage(await fs.readFile(path.join(dist, filename)), manifest);
|
|
await fs.writeFile(path.join(dist, filename), packaged);
|
|
const hash = createHash('sha256').update(packaged).digest('hex');
|
|
await fs.writeFile(path.join(dist, 'sha256sums'), `${hash} ${filename}\n`);
|
|
console.log(`Ready to install: dist/${filename}\nSHA-256: ${hash}`);
|