diff --git a/packages/core/gea_app_entry.cpp b/packages/core/gea_app_entry.cpp index 74084d1..7d39db7 100644 --- a/packages/core/gea_app_entry.cpp +++ b/packages/core/gea_app_entry.cpp @@ -41,7 +41,7 @@ void drainMicrotasks(); #endif extern "C" void gea_cycle_collection_defer_begin() GEA_PROGRAM_HOOK; extern "C" void gea_cycle_collection_defer_end() GEA_PROGRAM_HOOK; -extern "C" bool gea_embedded_assets_prepare() __attribute__((weak)); +extern "C" bool gea_embedded_assets_prepare() GEA_PROGRAM_HOOK; namespace gea::framework::app { diff --git a/packages/core/include/platform/asset_bundle.h b/packages/core/include/platform/asset_bundle.h index 8347048..f6a19e6 100644 --- a/packages/core/include/platform/asset_bundle.h +++ b/packages/core/include/platform/asset_bundle.h @@ -27,8 +27,10 @@ struct AssetSegment { inline bool loadAssetBundle(const char *path, const AssetSegment *segments, std::size_t count, std::size_t bytes, std::uint32_t expectedCrc) { - if (!ensureMounted()) + if (!ensureMounted()) { + std::fprintf(stderr, "Gea asset bundle storage not mounted: %s\n", path); return false; + } auto *file = std::fopen(path, "rb"); if (!file) { std::fprintf(stderr, "Gea asset bundle missing: %s\n", path); diff --git a/packages/core/scripts/gea-file-asset-bundle.mjs b/packages/core/scripts/gea-file-asset-bundle.mjs index b92ce15..65cfc74 100644 --- a/packages/core/scripts/gea-file-asset-bundle.mjs +++ b/packages/core/scripts/gea-file-asset-bundle.mjs @@ -26,7 +26,13 @@ export function fileAssetBundle(appDir, outCpp, kind, entries) { ? JSON.parse(fs.readFileSync(manifestFile, "utf8")) : {}; const directory = fileAssetDirectory(manifest); - if (!directory || !entries.length) return null; + const metadata = `${outCpp}.assets.json`; + if (!directory || !entries.length) { + // A manifest left from an earlier storage-backed build would still have + // deploy tooling push that bundle. + fs.rmSync(metadata, { force: true }); + return null; + } const bytes = Buffer.concat(entries.map((entry) => Buffer.from(entry.data))); const hash = createHash("sha256").update(bytes).digest("hex"); const id = String(manifest.gea?.id || path.basename(appDir)).replace( @@ -43,7 +49,6 @@ export function fileAssetBundle(appDir, outCpp, kind, entries) { version: 1, files: [{ local, remote, bytes: bytes.length, sha256: hash }], }; - const metadata = `${outCpp}.assets.json`; const contents = JSON.stringify(deployment, null, 2) + "\n"; if ( !fs.existsSync(metadata) || diff --git a/packages/core/test/test_embedded_assets_manifest.mjs b/packages/core/test/test_embedded_assets_manifest.mjs new file mode 100644 index 0000000..9e5d928 --- /dev/null +++ b/packages/core/test/test_embedded_assets_manifest.mjs @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict' +import { execFileSync } from 'node:child_process' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const core = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'gea-assets-manifest-')) + +try { + // Dropping file storage must not leave the previous deployment manifest for + // deploy tooling to push. + const app = path.join(fixture, 'app') + const outCpp = path.join(fixture, 'images.cpp') + fs.mkdirSync(path.join(app, 'public'), { recursive: true }) + fs.writeFileSync(path.join(app, 'public', 'pixel.png'), 'x') + const generate = (gea) => { + fs.writeFileSync(path.join(app, 'package.json'), JSON.stringify({ gea })) + execFileSync(process.execPath, [path.join(core, 'scripts', 'generate-gea-embedded-assets.mjs'), '--app-dir', app, '--out-cpp', outCpp], { + stdio: 'pipe' + }) + } + const windows = process.platform === 'win32' + const store = path.join(fixture, 'store') + const directory = windows ? '/' + store.slice(path.parse(store).root.length).replaceAll('\\', '/') : store + generate({ id: 'manifest-test', embeddedAssets: { storage: { type: 'file', directory } } }) + assert.ok(fs.existsSync(outCpp + '.assets.json'), 'a storage-backed build writes its manifest') + generate({ id: 'manifest-test' }) + assert.ok(!fs.existsSync(outCpp + '.assets.json'), 'a build without storage removes the stale manifest') + + console.log('Embedded assets manifest: a build without file storage removes the stale deployment manifest') +} finally { + fs.rmSync(fixture, { recursive: true, force: true }) +} diff --git a/packages/core/test/test_embedded_assets_prepare.mjs b/packages/core/test/test_embedded_assets_prepare.mjs new file mode 100644 index 0000000..16f22a9 --- /dev/null +++ b/packages/core/test/test_embedded_assets_prepare.mjs @@ -0,0 +1,60 @@ +import assert from 'node:assert/strict' +import { execFileSync } from 'node:child_process' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +// gea_app_entry.cpp references gea_embedded_assets_prepare, and Apple's ld rejects a +// weak reference nothing defines, so the generated assets unit has to define it for +// every app. Link the real entry object with each kind of generated unit and check +// that the hook resolves. lld-link has no relocatable link, so COFF is covered by the +// windows target's hook-defaults test instead. +if (process.platform === 'win32') { + console.log('Embedded assets prepare: skipped on Windows (no relocatable link)') + process.exit(0) +} + +const core = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const packages = path.dirname(core) +const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'gea-assets-prepare-')) +const cxx = process.env.CXX || 'clang++' +const includes = ['core/include', 'core', 'host/include', 'host', 'engine', 'engine/ui', 'elements', 'elements/ui', 'geaos'].map( + (dir) => '-I' + path.join(packages, dir) +) + +try { + const entry = path.join(fixture, 'entry.o') + execFileSync(cxx, ['-std=c++20', '-O2', '-c', path.join(core, 'gea_app_entry.cpp'), ...includes, '-o', entry], { stdio: 'inherit' }) + + const linked = (name, gea, asset) => { + const app = path.join(fixture, name) + fs.mkdirSync(path.join(app, 'public'), { recursive: true }) + if (asset) fs.writeFileSync(path.join(app, 'public', 'pixel.png'), 'x') + fs.writeFileSync(path.join(app, 'package.json'), JSON.stringify({ gea })) + const cpp = path.join(fixture, name + '.cpp') + execFileSync(process.execPath, [path.join(core, 'scripts', 'generate-gea-embedded-assets.mjs'), '--app-dir', app, '--out-cpp', cpp], { + stdio: 'pipe' + }) + const unit = path.join(fixture, name + '.o') + execFileSync(cxx, ['-std=c++20', '-O2', '-c', cpp, ...includes, '-o', unit], { stdio: 'inherit' }) + const combined = path.join(fixture, name + '-linked.o') + execFileSync(cxx, ['-r', entry, unit, '-o', combined], { stdio: 'inherit' }) + const symbols = execFileSync(process.env.NM || 'nm', [combined], { encoding: 'utf8' }) + return symbols.split('\n').find((line) => /\b_?gea_embedded_assets_prepare$/.test(line)) + } + + for (const [name, gea, asset] of [ + ['no-storage', { id: 'no-storage' }, true], + ['no-assets', { id: 'no-assets' }, false], + ['file-backed', { id: 'file-backed', embeddedAssets: { storage: { type: 'file', directory: '/nand/assets' } } }, true] + ]) { + const prepare = linked(name, gea, asset) + assert.ok(prepare, `${name}: the linked object names gea_embedded_assets_prepare`) + assert.doesNotMatch(prepare, /\s[Uwv]\s/, `${name}: gea_app_entry's reference resolves: ${prepare.trim()}`) + } + + console.log('Embedded assets prepare: entry links against no-storage, no-asset and file-backed generated units') +} finally { + fs.rmSync(fixture, { recursive: true, force: true }) +} diff --git a/packages/core/test/test_embedded_weak_symbols.mjs b/packages/core/test/test_embedded_weak_symbols.mjs index f4fe37f..48d1f4f 100644 --- a/packages/core/test/test_embedded_weak_symbols.mjs +++ b/packages/core/test/test_embedded_weak_symbols.mjs @@ -9,6 +9,7 @@ assert.doesNotMatch(entry, /weak_import/, 'embedded GCC does not support the Mac assert.match(entry, /#else\n#define GEA_PROGRAM_HOOK __attribute__\(\(weak\)\)\n#endif/) assert.match(entry, /gea_cycle_collection_defer_begin\(\) GEA_PROGRAM_HOOK;/) assert.match(entry, /gea_cycle_collection_defer_end\(\) GEA_PROGRAM_HOOK;/) +assert.match(entry, /gea_embedded_assets_prepare\(\) GEA_PROGRAM_HOOK;/) const initBody = entry.slice(entry.indexOf('void Application::init'), entry.indexOf('namespace {', entry.indexOf('void Application::init'))) const frameBody = entry.slice(entry.indexOf('void Application::frame'), entry.indexOf('void Application::toggleSettings'))