analysis: lock mixin classes and plugin again (revert mixin rename)
Renaming mixin classes and patching mixins.json (90506a4) breaks Fabric
mods at runtime, proven on LoVisual (2026-10-13):
- When obfuscated packages are randomized per class the config patcher
computes an empty common prefix and writes `package: ""`. Fabric/Mixin
then drops every mixin as "orphaned", so `RenderPipelineMixin
implements IRenderPipeline` never applies, the interface is not
injected into `RenderPipeline`, and the entrypoint dies with
`ClassCastException: RenderPipeline cannot be cast to IRenderPipeline`.
- `IMixinConfigPlugin.shouldApplyMixin` compares the runtime mixin class
name against string literals (`Set.of("dev.loki...Mixin")`); renaming
desyncs those sets and optional sodium/iris/xaero mixins get applied
when the target mod is absent.
Re-enable absorb_mixins: mixin entry classes and the config plugin keep
their names, and locking a class freezes its members too (needed for
@Accessor/@Invoker/@Overwrite name lookups). Calls from locked mixin
bytecode are still rewritten consistently, and the family mechanism keeps
interface method names (IRenderPipeline) in sync with the frozen mixin
methods. patch_mixins_json stays but is identity for locked classes.
Verified: LoVisual obf jar loads all 221 mixins (0 orphaned), the main
menu renders, the entrypoint runs.
This commit is contained in:
parent
01afa6af37
commit
1ebbca10fa
3 changed files with 43 additions and 21 deletions
|
|
@ -168,6 +168,7 @@ impl Analysis {
|
||||||
keep.absorb(&entrypoints);
|
keep.absorb(&entrypoints);
|
||||||
keep.absorb_bootstrap(program);
|
keep.absorb_bootstrap(program);
|
||||||
keep.absorb_databind(program);
|
keep.absorb_databind(program);
|
||||||
|
keep.absorb_mixins(program);
|
||||||
|
|
||||||
let locked_classes = program
|
let locked_classes = program
|
||||||
.classes
|
.classes
|
||||||
|
|
|
||||||
|
|
@ -31,18 +31,19 @@ impl KeepRules {
|
||||||
/// Lock every Mixin class (and the plugin) listed in the configs.
|
/// Lock every Mixin class (and the plugin) listed in the configs.
|
||||||
///
|
///
|
||||||
/// The Mixin framework resolves mixin classes and their members by name
|
/// The Mixin framework resolves mixin classes and their members by name
|
||||||
/// at runtime, so renaming them breaks the mod at startup. Entries use
|
/// at runtime, so renaming them breaks the mod at startup. Locking the
|
||||||
/// simple names relative to the `package` field, which generic text
|
/// class also freezes its members (locked classes skip member renaming
|
||||||
/// patching can never see - locking is the only sound answer. Locking
|
/// entirely), which the `@Accessor`/`@Invoker`/`@Overwrite` name lookups
|
||||||
/// the class also freezes its members (locked classes skip member
|
/// and `IMixinConfigPlugin.shouldApplyMixin` string sets both need.
|
||||||
/// renaming entirely), which the `@Accessor`/`@Invoker`/`@Overwrite`
|
|
||||||
/// lookups need. Mixins configs are found two ways: any resource ending
|
|
||||||
/// in `mixins.json`, plus whatever `fabric.mod.json` lists.
|
|
||||||
///
|
///
|
||||||
/// DISABLED: the resources pass now patches mixins.json properly
|
/// Renaming mixins and patching the config was tried and reverted: when
|
||||||
/// (rewrites package + entries via common prefix), so mixin classes
|
/// obfuscated packages are randomized per class the config patcher writes
|
||||||
/// can be renamed like any other class. Reference: OBF fabric-obfuscator.
|
/// an empty `package`, and Fabric/Mixin drops every mixin as "orphaned"
|
||||||
#[allow(dead_code)]
|
/// (real case: injected-interface cast fails with `ClassCastException`).
|
||||||
|
/// Entries use simple names relative to `package`, which generic text
|
||||||
|
/// patching can never see - locking is the only sound answer. Mixin
|
||||||
|
/// configs are found two ways: any resource ending in `mixins.json`,
|
||||||
|
/// plus whatever `fabric.mod.json` lists.
|
||||||
pub fn absorb_mixins(&mut self, program: &Program) {
|
pub fn absorb_mixins(&mut self, program: &Program) {
|
||||||
let mut listed: Vec<String> = Vec::new();
|
let mut listed: Vec<String> = Vec::new();
|
||||||
for resource in &program.resources {
|
for resource in &program.resources {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,20 @@
|
||||||
//! Mixin keep: classes listed in `*.mixins.json` are now renameable.
|
//! Mixin keep: classes listed in `*.mixins.json` are locked, not renamed.
|
||||||
//!
|
//!
|
||||||
//! The resources pass patches mixins.json (rewrites package + entries),
|
//! Real-world proof (LoVisual, 2026-10-13): renaming mixin classes and
|
||||||
//! so mixin classes can be renamed. Reference: OBF fabric-obfuscator.
|
//! patching the config breaks the mod at runtime. Two independent failures:
|
||||||
|
//! 1. When obf packages are randomized per class, the config patcher writes
|
||||||
|
//! `package: ""`, and Fabric/Mixin then drops *every* mixin as "orphaned"
|
||||||
|
//! (`RenderPipelineMixin implements IRenderPipeline` never applies, so the
|
||||||
|
//! injected-interface cast fails with `ClassCastException`).
|
||||||
|
//! 2. `IMixinConfigPlugin.shouldApplyMixin` compares the runtime mixin class
|
||||||
|
//! name against string literals (`Set.of("dev.loki...Mixin")`); renaming
|
||||||
|
//! the mixins desyncs those sets and optional mixins get applied when the
|
||||||
|
//! target mod is absent.
|
||||||
|
//!
|
||||||
|
//! So mixin classes and their plugin stay under their original names, and
|
||||||
|
//! locking the class freezes its members too - which `@Accessor`/`@Invoker`/
|
||||||
|
//! `@Overwrite` name lookups require. Other program classes still rename, and
|
||||||
|
//! calls from locked mixin bytecode are rewritten consistently.
|
||||||
|
|
||||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||||
|
|
||||||
|
|
@ -54,20 +67,27 @@ fn sample() -> Program {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mixin_classes_are_renameable() {
|
fn mixin_classes_and_plugin_are_locked() {
|
||||||
let program = sample();
|
let program = sample();
|
||||||
let analysis = Analysis::run(&program, &Config::default()).expect("analysis");
|
let analysis = Analysis::run(&program, &Config::default()).expect("analysis");
|
||||||
|
|
||||||
// Mixin classes are no longer locked - resources pass patches the config.
|
// Mixin entry points and the config plugin are resolved by name at
|
||||||
for renameable in [
|
// runtime and must keep their names (and, being locked, their members).
|
||||||
|
for locked in [
|
||||||
"demo/mixin/TargetMixin",
|
"demo/mixin/TargetMixin",
|
||||||
"demo/mixin/other/Helper",
|
"demo/mixin/other/Helper",
|
||||||
"demo/mixin/ClientMixin",
|
"demo/mixin/ClientMixin",
|
||||||
"demo/mixin/Plugin",
|
"demo/mixin/Plugin",
|
||||||
] {
|
] {
|
||||||
assert!(
|
assert!(analysis.is_locked(locked), "{locked} should be locked");
|
||||||
!analysis.is_locked(renameable),
|
|
||||||
"{renameable} should be renameable now"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_mixin_classes_stay_renameable() {
|
||||||
|
let program = sample();
|
||||||
|
let analysis = Analysis::run(&program, &Config::default()).expect("analysis");
|
||||||
|
|
||||||
|
// Plain program classes are not named by any mixin config.
|
||||||
|
assert!(!analysis.is_locked("demo/App"));
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue