OverviewFilesCommitsIssuesDocsReleasesPeers

favela-android / scripts / gen-tree-manifest.mjs

54 lines · javascript ref:
// Publish the worklet bundle as a pgh TREE, so a phone can verify what it downloaded.
//
// pgh-runtime does not fetch a file, it installs a TREE: it asks for the tree, hashes the entries it is
// handed, and refuses them unless they reproduce the root hash. That guard exists so a peer cannot answer
// "I don't have this" with an empty directory. It means a release with a bundle and no tree.json is
// unfetchable — which is the state favela-android shipped in for two weeks, alongside an OTA client that
// had never run.
//
// Emits tree.json next to the bundle at the repo root, both listed in .pear-git/ci.yaml artifacts:
//
//   { "treeHash": "<hex>", "size": <bytes>, "entries": [ { kind, exec, name, childHash } ] }
//
// Built with lib/ota.js's own treeFor(), which is built with hyper-overlay's encoder — the same function
// pgh-runtime verifies with. Computing the hash any other way would be a second definition of the format
// that agrees until it doesn't.
//
// Run under BARE, not node: hyper-overlay/tree resolves #fs through bare's import map.
//   bare scripts/gen-tree-manifest.mjs
// ⛔ ONE FILE PER MANIFEST, AND THE APK GETS ITS OWN. It is tempting to put both artifacts in tree.json
// — a pgh tree is a set of entries, so it would encode fine. It would also brick every phone already in
// the field: the deployed bundleStore.materialize loops the entries and keeps the LAST file it writes as
// the bundle to boot, so a two-entry tree would stage a 154 MB APK as the worklet bundle. The OTA client
// on a handset is the thing we cannot fix remotely, so its manifest keeps exactly the shape it has
// always had, and the APK is described in a SEPARATE file that only new code looks for.
//
//   --in <file>    what to hash          (default: the worklet bundle)
//   --out <file>   where to write        (default: tree.json)
//   --name <name>  the entry's name      (default: treeFor's own, worklet.bundle)
import fs from 'bare-fs'
import path from 'bare-path'
import { treeFor } from '../lib/ota.js'

const ROOT = path.join(path.dirname(import.meta.url.replace('file://', '')), '..')

function arg (flag, fallback) {
  const i = Bare.argv.indexOf(flag)
  return i === -1 ? fallback : Bare.argv[i + 1]
}

const IN = path.resolve(ROOT, arg('--in', path.join(ROOT, 'android', 'app', 'src', 'main', 'assets', 'worklet.bundle')))
const OUT = path.resolve(ROOT, arg('--out', path.join(ROOT, 'tree.json')))
const NAME = arg('--name', null)

if (!fs.existsSync(IN)) {
  console.error('nothing to hash at ' + IN + ' — run `npm run pack:worklet` first (or point --in at a built artifact)')
  Bare.exit(1)
}

const bytes = fs.readFileSync(IN)
const { entries, treeHash, contentHash, size } = NAME ? treeFor(bytes, NAME) : treeFor(bytes)

fs.writeFileSync(OUT, JSON.stringify({ treeHash, size, entries }, null, 2) + '\n')
console.log(`${path.basename(OUT)} — treeHash ${treeHash.slice(0, 16)}… content ${contentHash.slice(0, 16)}… ${size} bytes`)