Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions .github/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,17 +91,25 @@ Update those four in one commit:
| `Writerside/v.list` | `<var name="version" value="x.y.z"/>` |
| `aether-weaver-engine/…/engine/Weaver.java` | `static final String VERSION = "x.y.z";` — it is stamped into every weave record |

Two more the release workflow does not read, but that must move with these or the gate fails: the
agent's own `aether-weaver-agent/…/agent/WeaverAgent.java` `VERSION` — its start-up banner — and the
built-in plugin id in `aether-weaver-engine/…/engine/inject/CorePlugin.java`. `ExplainReportTest`,
`WeaverAgentEndToEndTest` and `WeaveMojoTest` pin the printed version, so they move with it too.

The poms are **not** touched. They stay on `-SNAPSHOT`; the workflow stamps the release version in
from the tag. That way a build from `main` can never overwrite a released artefact.

**The IDE plugin has two more, and nothing checks either.** It is a separate Gradle build, so the
release workflow never sees it — but it ships to the Marketplace beside the release, and both of
these decide what it ships as.
**The IDE plugin has three more, and they are set in [step 6](#step-6--the-ide-plugin), not here.**
It is a separate Gradle build the release workflow never sees, but the `IntelliJ plugin` CI check
builds it, and it resolves `aether-weaver-api` and `-engine` as ordinary published dependencies.
Bumping these before the release is on Central makes that check fail to resolve the version — so set
them once Central serves it, in step 6, where the plugin is actually built.

| File | What to change | What it breaks if you forget |
|---|---|---|
| `aether-weaver-ide/aether-weaver-idea/build.gradle.kts` | `version = "x.y.z"` | The archive uploads as `x.y.z-SNAPSHOT` |
| `aether-weaver-ide/aether-weaver-idea/gradle.properties` | `aetherWeaverVersion=x.y.z` | The plugin bundles the API and engine it resolves, so it ships the `-SNAPSHOT` jars from whoever built it rather than the published ones |
| `aether-weaver-ide/aether-weaver-idea/sample/pom.xml` | `<aether.weaver.version>x.y.z</aether.weaver.version>` | `checkSampleVersion` fails the Gradle build: the sample would resolve a different API than the plugin was built against |

Also worth doing: `Writerside/versions.json`, if the site is to offer a version switcher.

Expand Down Expand Up @@ -158,7 +166,16 @@ shortly after.
ordinary dependency and bundles it, so a build started before Central serves it either fails to
resolve or quietly bundles something else.

With the two versions from step 1 set:
Set the three IDE-plugin versions now — they are left alone during step 1 because this build
resolves the published artefacts, so bumping them earlier fails the `IntelliJ plugin` CI check:

| File | What to change |
|---|---|
| `aether-weaver-ide/aether-weaver-idea/build.gradle.kts` | `version = "x.y.z"` |
| `aether-weaver-ide/aether-weaver-idea/gradle.properties` | `aetherWeaverVersion=x.y.z` |
| `aether-weaver-ide/aether-weaver-idea/sample/pom.xml` | `<aether.weaver.version>x.y.z</aether.weaver.version>` |

Then:

```bash
cd aether-weaver-ide/aether-weaver-idea
Expand Down Expand Up @@ -224,6 +241,8 @@ switcher offers.
| Central rejects the deployment | Read the job log; it carries Central's own validation messages. Nothing was published |
| Signing fails, or the job hangs | The passphrase secret or the key secret is wrong. The key must be armoured, whole, including its BEGIN and END lines |
| The deployment sits in `VALIDATED` | `autoPublish` did not take effect. Publish it by hand in the portal, and fix the configuration before the next release |
| The publish job fails but the portal says `PUBLISHING` | The plugin stopped waiting; Central did not stop publishing. Nothing was lost and nothing may be re-uploaded. Wait for `PUBLISHED`, then create the GitHub Release by hand — the `github-release` job cannot be re-run alone, because it needs `publish`, and re-running failed jobs would deploy a second time |
| The deployment sits in `PUBLISHING` for over an hour | Sonatype's side. Check <https://status.maven.org>, then open a support ticket with the deployment id. Do not delete the tag and do not deploy again |
| The release published, the GitHub Release did not | Re-run only the `github-release` job. Central is already done and cannot be redone |

A released version is never re-released. If a release is wrong, release the next patch version.
Expand Down
16 changes: 15 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ reads it out of this file, so there is one copy of it and it cannot drift.

Nothing yet.

## [0.1.1] - 2026-09-15

### Fixed

- **Load-time weaving of application types now verifies.** On the load-time path, stack-map
generation and verification resolved the class hierarchy through the system class loader, so
a weave into a class whose method frames merge the application's own types was rejected —
dropped silently under `verification=report`, or reported as `AW4001` (or a
`Could not resolve class` from the Class-File API) under `verification=strict`. The defining
class loader is now threaded from the agent through the injection, merge and verification
steps, all of which build through `FrameSupport.forLoadTime(loader)`. Weaves whose signatures
reference only JDK types were unaffected, which is why the failure looked selective.

## [0.1.0] - 2026-08-31

The first release. Everything below is new, so this entry describes the shape of the framework
Expand Down Expand Up @@ -147,5 +160,6 @@ Stated here because a first release that hides its edges is worse than one that
- **There is no Gradle plugin.** A Gradle build can declare and compile-check weaves; applying them
means the agent or the runtime driver.

[Unreleased]: https://github.com/aether-framework/aether-weaver/compare/v0.1.0...HEAD
[Unreleased]: https://github.com/aether-framework/aether-weaver/compare/v0.1.1...HEAD
[0.1.1]: https://github.com/aether-framework/aether-weaver/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/aether-framework/aether-weaver/releases/tag/v0.1.0
2 changes: 1 addition & 1 deletion Writerside/v.list
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<vars>
<var name="product" value="Aether Weaver"/>
<var name="group" value="de.splatgames.aether.weaver"/>
<var name="version" value="0.1.0"/>
<var name="version" value="0.1.1"/>
<var name="jdk" value="25"/>
<var name="maven" value="3.9"/>
<var name="repo" value="https://github.com/aether-framework/aether-weaver"/>
Expand Down
2 changes: 1 addition & 1 deletion Writerside/writerside.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
<vars src="v.list"/>
<snippets src="snippets"/>
<build-config dir="cfg"/>
<instance src="aw.tree" version="0.1.0" web-path="aether-weaver"/>
<instance src="aw.tree" version="0.1.1" web-path="aether-weaver"/>
</ihp>
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
public final class WeaverAgent {

/** The version this agent prints on its startup line. */
private static final String VERSION = "0.1.0";
private static final String VERSION = "0.1.1";

/**
* Refuses instantiation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,11 @@ final class WeavingTransformer implements ClassFileTransformer {
return null;
}
try {
final byte[] woven = this.weaver.weave(className, buffer);
// The loader that defines this class is what lets the engine resolve the application's
// own types through their class-file resources when it regenerates stack maps. Dropping
// it here forces the frame builder onto a resolver that reflects over the system loader
// and cannot see host classes, which fails to verify any target whose frames merge them.
final byte[] woven = this.weaver.weave(className, buffer, loader);
if (woven == null) {
// Contract point 1. Returning `buffer` here would be correct-looking and would make
// the JVM re-verify every class in the application.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ void theStartupLineIsPrinted() throws Exception {
assertThat(result.output())
.as("a run whose configuration is not visible in its own log is a run whose "
+ "behaviour has to be reconstructed from the deployment")
.contains("Aether Weaver 0.1.0")
.contains("Aether Weaver 0.1.1")
.contains("1 weave")
.contains("fingerprint ")
.contains("verification=strict");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ private DiagnosticListener onceFor(@NotNull final String key,
}

/** The weaver version, written into every weave record and printed by the explain report. */
static final String VERSION = "0.1.0";
static final String VERSION = "0.1.1";

/**
* Every kind this release declares, in one place.
Expand Down Expand Up @@ -300,16 +300,38 @@ public static WeaverBuilder builder() {
*/
public byte @Nullable [] weave(@NotNull final String internalName,
final byte @NotNull [] original) {
return weave(internalName, original, null);
}

/**
* Weaves a class whose bytes are already in hand, resolving its type hierarchy through the loader
* that defines it.
*
* <p>The loader is what lets load-time stack-map generation resolve the application's own types
* through their class-file resources without loading them. {@code null} stands for the bootstrap
* loader, for which the platform loader is used, exactly as an agent is handed {@code null} for a
* class the bootstrap loader defined.
*
* @param internalName the class's internal name, such as {@code com/acme/Ledger}
* @param original the class as it stands; not modified
* @param loader the loader that defines the class, whose resources describe the hierarchy,
* or {@code null} for the bootstrap loader
* @return the woven class, or {@code null} when it is to be used unchanged
* @throws NullPointerException if {@code internalName} or {@code original} is {@code null}
*/
public byte @Nullable [] weave(@NotNull final String internalName,
final byte @NotNull [] original,
@Nullable final ClassLoader loader) {
Objects.requireNonNull(internalName, "internalName");
Objects.requireNonNull(original, "original");
this.statistics.seen();

final List<PlanEntryView> entries = this.plan.entriesFor(internalName);
final List<WeaveClass> dissolving = this.plan.structuralFor(internalName);
if (entries.isEmpty() && dissolving.isEmpty()) {
return extensionsOnly(internalName, original);
return extensionsOnly(internalName, original, loader);
}
return timed(internalName, () -> original, entries, dissolving);
return timed(internalName, () -> original, entries, dissolving, loader);
}

/**
Expand All @@ -335,10 +357,10 @@ public static WeaverBuilder builder() {
final List<PlanEntryView> entries = this.plan.entriesFor(internalName);
final List<WeaveClass> dissolving = this.plan.structuralFor(internalName);
if (entries.isEmpty() && dissolving.isEmpty()) {
return this.extensions.isEmpty() ? null : extensionsOnly(internalName, original.get());
return this.extensions.isEmpty() ? null : extensionsOnly(internalName, original.get(), null);
}

return timed(internalName, original, entries, dissolving);
return timed(internalName, original, entries, dissolving, null);
}

/**
Expand All @@ -353,12 +375,14 @@ public static WeaverBuilder builder() {
*
* @param internalName the class's internal name
* @param original the class as it stands
* @param loader the loader that defines the class, or {@code null} for the bootstrap loader
* @return the rewritten class, or {@code null} when neither rewrite applied; also the original
* bytes, unrewritten, when a rewrite applied but the verifier refused the result under
* {@code REPORT}
*/
private byte @Nullable [] extensionsOnly(@NotNull final String internalName,
final byte @NotNull [] original) {
final byte @NotNull [] original,
@Nullable final ClassLoader loader) {
if (this.extensions.isEmpty()) {
return null;
}
Expand All @@ -373,7 +397,7 @@ public static WeaverBuilder builder() {
// the original, and a caller that wrote the returned bytes unconditionally still gets a
// class that loads.
return this.verifier.check(internalName, original,
guarded == null ? rewritten : guarded);
guarded == null ? rewritten : guarded, loader);
}

/**
Expand All @@ -386,15 +410,17 @@ public static WeaverBuilder builder() {
* @param original supplies the class as it stands
* @param entries the injections planned for it
* @param dissolving the weaves to merge into it
* @param loader the loader that defines the class, or {@code null} for the bootstrap loader
* @return whatever {@link #apply} returned
*/
private byte @Nullable [] timed(@NotNull final String internalName,
@NotNull final ByteSupplier original,
@NotNull final List<PlanEntryView> entries,
@NotNull final List<WeaveClass> dissolving) {
@NotNull final List<WeaveClass> dissolving,
@Nullable final ClassLoader loader) {
final long started = System.nanoTime();
try {
return apply(internalName, original, entries, dissolving, started);
return apply(internalName, original, entries, dissolving, started, loader);
} finally {
this.statistics.spent(System.nanoTime() - started);
}
Expand Down Expand Up @@ -428,14 +454,16 @@ public static WeaverBuilder builder() {
* @param entries the injections planned for it
* @param dissolving the weaves to merge into it
* @param started when timing began, for the JFR event
* @param loader the loader that defines the class, or {@code null} for the bootstrap loader
* @return the woven class, {@code null} when it is to be used unchanged, or the original bytes
* when a verifier refusal under {@code REPORT} left {@code checked != woven}
*/
private byte @Nullable [] apply(@NotNull final String internalName,
@NotNull final ByteSupplier original,
@NotNull final List<PlanEntryView> entries,
@NotNull final List<WeaveClass> dissolving,
final long started) {
final long started,
@Nullable final ClassLoader loader) {
final byte[] bytes = original.get();
final ClassModel model = ClassFile.of().parse(bytes);

Expand Down Expand Up @@ -478,7 +506,7 @@ public static WeaverBuilder builder() {
final Reporter reporter = this.listener::report;
byte[] current = bytes;
if (!entries.isEmpty()) {
final byte[] woven = this.pipeline.weave(model, entries, this.groups, reporter);
final byte[] woven = this.pipeline.weave(model, entries, this.groups, reporter, loader);
if (woven == null) {
// Nothing resolved, or accounting refused it. The pipeline has already said why,
// and the original class stands — never a half-woven one, because the pipeline
Expand All @@ -489,7 +517,7 @@ public static WeaverBuilder builder() {
}
if (!dissolving.isEmpty()) {
final byte[] merged = this.structural.apply(
ClassFile.of().parse(current), dissolving, reporter);
ClassFile.of().parse(current), dissolving, reporter, loader);
if (merged == null && current == bytes) {
return null;
}
Expand Down Expand Up @@ -520,7 +548,7 @@ public static WeaverBuilder builder() {

// Gate 5: verify. Under REPORT this hands back the ORIGINAL, never the broken class.
final byte[] woven = current;
final byte[] checked = this.verifier.check(internalName, bytes, woven);
final byte[] checked = this.verifier.check(internalName, bytes, woven, loader);
if (checked != woven) {
// Counted, not returned as a success. The class the caller gets back is the original
// one, so a run that reported "42 classes woven" while a verifier had quietly handed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public final class CorePlugin implements WeaverPlugin {

/** Identifies the plugin. The version is a literal here and is not read from the build. */
private static final PluginId ID = new PluginId(
PluginId.BUILT_IN_NAMESPACE, "Aether Weaver", "0.1.0");
PluginId.BUILT_IN_NAMESPACE, "Aether Weaver", "0.1.1");

/**
* Creates the plugin.
Expand Down
Loading
Loading