Recipe — multi-version bundles¶
A multi-version bundle is a single compiled .js script that
carries maps for many app versions in a registry block, and lets
the runtime pick the right one at attach time by the detected
version.
This page describes the workflow and the runtime semantics. Some of
the supporting CLI (rosetta merge-bundle) is deferred to V1.5;
you can still use registry bundles in V1 by building them
manually, which is the path documented here.
When to use a registry bundle¶
- Fleet deployment — you ship one bundle to many devices running different versions of the target app. The bundle Just Works on each.
- CI matrix — you test the same hook against several versions in one pipeline without recompiling.
- Long-tail support — the hook supports older versions you can't drop, plus the latest, in a single artifact.
When you control the deployment 1:1 with the version, the simpler per-environment patch workflow is usually cleaner. Registry bundles win when one binary needs to be right for many environments.
How the runtime picks¶
flowchart TB
A["rosetta.session({ map: registry })"]
B["detect app+version+version_code"]
VC{"registry entry whose
version_code matches?"}
C{exact label match in registry?}
D[use it]
E{versionMatch: 'fuzzy'?}
F[pick closest by semver distance]
G[throw — no map for version]
H[verify map.app matches detected app]
I[run health check]
A --> B --> VC
VC -- yes --> D --> H --> I
VC -- no --> C
C -- yes --> D
C -- no --> E
E -- yes --> F --> H --> I
E -- no --> G
The selection algorithm is in pickMapForVersion:
- By
version_codefirst (authoritative). When aversion_codewas detected (or supplied viaversionCode), the registry is scanned for the entry whoseversion_codeequals it and that map is selected directly — regardless of its label. This match is exact, never fuzzy. - Exact label mode (default fallback). When no
version_codeis available or no entry carries the detected code, the registry must contain an entry whose key equals the detected version label. Missing → throw. - Fuzzy mode. Parse each key into a
[major, minor, patch]tuple. Distance =Δmajor × 10_000 + Δminor × 100 + Δpatch. Tie → lower version wins.
Fuzzy is opt-in. Wrong-version maps silently corrupt hooks; the default fails loudly and tells you to ship a map for the detected version.
Building a registry bundle manually (V1)¶
Until rosetta merge-bundle lands in V1.5, you build registry
bundles by hand. There are two paths.
Path A — registry built into the hook source¶
// hook.ts
import map_2_16_31 from './maps/com.example.app/30405.json' with { type: 'json' };
import map_2_16_32 from './maps/com.example.app/3.4.6.json' with { type: 'json' };
import map_2_17_0 from './maps/com.example.app/3.5.0.json' with { type: 'json' };
import { rosetta, type RosettaMapRegistry } from 'rosetta-frida';
const registry = {
'3.4.5': map_2_16_31,
'3.4.6': map_2_16_32,
'3.5.0': map_2_17_0,
} as unknown as RosettaMapRegistry;
Java.perform(() => {
rosetta.session({ map: registry });
// ... your hooks here ...
});
frida-compile inlines all three JSON imports. The compiled bundle
carries the registry; the runtime picks the matching entry by the
detected version.
If you want the marker-block embedding (so rosetta inspect reports
the registry, and rosetta patch can swap the whole registry block),
follow the manual marker wrapping recipe
but emit emitMarkerRegistry(registry) instead of
emitMarkerBlock(map).
Path B — patch an existing single-map bundle with a registry¶
If you already have a working single-map bundle and want to convert
it to a registry without re-running frida-compile:
-
Build a registry JSON file on disk:
cat <<EOF > registry.json { "3.4.5": $(cat maps/com.example.app/30405.json), "3.4.6": $(cat maps/com.example.app/3.4.6.json), "3.5.0": $(cat maps/com.example.app/3.5.0.json) } EOFor use
jq -s/ a tiny Node script — anything that produces a record-of-RosettaMap.rosetta patchaccepts strict JSON (the constituent maps' authoring metadata both survive the concat). -
Patch the single-map bundle with the registry. The patch command's loader heuristic detects "no top-level
schema_version" and treats the input as a registry: -
Verify:
The runtime now picks per-version at attach time.
Enabling fuzzy version matching¶
Off by default. Enable per-session:
With 'fuzzy', the session falls back to the closest registry entry by
component-wise lexicographic distance [Δmajor, Δminor, Δpatch]
(compared major-first; ties → lower version). If the detected version
is 3.4.7 and the registry has [3.4.5, 3.4.6, 3.5.0], the session
picks 3.4.6 (Δ = [0,0,1]) over 3.4.5 (Δ = [0,0,2]) and 3.5.0
(Δ = [0,1,7] — the minor delta dominates).
The picked map's version field will not equal the detected
version. The session attaches and runs; the picked-version is
visible in the map-load event:
rosetta.events.onType('map-load', (e) => {
if (e.version !== rosetta.map.extract().version) {
// (in this code, e.version === extract().version; the
// session-level detected version differs.)
}
});
Inspect the session value directly for the detected version:
const session = rosetta.session({ map: registry, versionMatch: 'fuzzy' });
send({
stage: 'fuzzy-pick',
detected: session.version,
picked: session.map.version,
});
Expanded matching — ranges, ceilings, ranked hints¶
The string 'fuzzy' is shorthand for the richer object form, whose
knobs are all opt-in (and default to the legacy behaviour). Selection
order is exact version_code → exact label → code range → label range
→ nearest label, so an exact match always wins.
Each of strategy: 'fuzzy', versionCodeRange, and versionRange is an
independent opt-in: setting a range engages it even when strategy
is 'exact' (or omitted) — the examples below pick within a range without
turning on the nearest-label fallback.
Constrain by version_code range (the authoritative key):
rosetta.session({
map: registry,
versionMatch: { versionCodeRange: { min: 30400, max: 30599 } },
});
// The in-range map closest to the detected version_code wins.
Constrain by version-label range (semver-ish):
rosetta.session({
map: registry,
versionMatch: { versionRange: { min: '3.4.0', max: '3.6.0' } },
});
Cap how far a distance-ranked pick may stray — fail loudly past the
ceiling. maxDistance is a label-distance ceiling: it applies to the
nearest-label tier (strategy: 'fuzzy') and the versionRange tier, and
is compared by the same major-dominant lexicographic metric used to rank
candidates (distance <= [maxDistance, 0, 0]):
rosetta.session({
map: registry,
versionMatch: { strategy: 'fuzzy', maxDistance: 1 },
});
// A closest map at distance [2,0,0] is rejected (throws); [1,0,0] and any
// zero-major-delta pick (e.g. [0,2,0]) are accepted.
// The ceiling also gates a label range:
rosetta.session({
map: registry,
versionMatch: { versionRange: { min: '3.4.0', max: '3.6.0' }, maxDistance: 1 },
});
maxDistance does not apply to a versionCodeRange (that tier ranks
by numeric code, a different metric); pairing maxDistance with only a
versionCodeRange is rejected at config time. The parser likewise
rejects an inverted range (min > max) and an all-undefined range.
Set a project-wide default via the typed config (a per-session
versionMatch still overrides it):
import { resolveConfig } from 'rosetta-frida';
const config = resolveConfig({ versionMatching: { strategy: 'fuzzy', maxDistance: 1 } });
rosetta.session({ map: registry, config });
Inspecting a registry bundle¶
$ npx rosetta inspect hook.multi.bundle.js
registry: com.example.app, versions=[3.4.5, 3.4.6, 3.5.0], 45 classes total
$ npx rosetta extract hook.multi.bundle.js -o registry-extracted.json
extract: wrote registry-extracted.json (registry)
The extracted file is the full registry — Record<version, RosettaMap>.
V1.5 preview — rosetta merge-bundle¶
V1.5 will ship a one-shot CLI:
npx rosetta merge-bundle hook.bundle.js \
maps/com.example.app/30405.json \
maps/com.example.app/3.4.6.json \
maps/com.example.app/3.5.0.json \
-o hook.multi.bundle.js
This will subsume both paths A and B above. Until it lands, the manual workflows are the canonical V1 approach.
When versions don't match anything¶
In exact mode against a registry that doesn't contain the detected
version, the session throws on creation:
Error: rosetta-frida: no map for version '2.16.99' in registry (available: 3.4.5, 3.4.6, 3.5.0). Pass versionMatch: 'fuzzy' to fall back to the closest map.
The error message tells you what's available and how to opt into fuzzy.