BuildModeProcessor.java
package com.cloudforge.core.buildmode;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedOptions;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.Writer;
import java.util.Map;
import java.util.Set;
/**
* Bakes a compile-time constant into a generated class from a single {@code -A} compiler option,
* so a build-time fact (which Maven profile compiled these sources) survives into the compiled
* artifact as a real {@code static final} field — not something a caller can influence by how the
* resulting binary is later invoked.
*
* <p>This exists because a runtime heuristic doing the equivalent job (distinguishing "packaged
* production build" from "running from source" by inspecting the current {@link ClassLoader}'s
* class name) stops meaning what it's supposed to mean the moment an artifact ships as an
* ahead-of-time-compiled native image: there is no packaging-specific classloader in a native
* image to detect at all, so that kind of check silently collapses to always-true inside the real
* shipped binary. A value fixed at compile time has no equivalent failure mode — it's a constant
* in the generated bytecode, evaluated once, at the moment the option is passed to {@code javac},
* not re-derived from anything observable or influenceable at runtime.
*
* <p>Registered as a real {@link javax.annotation.processing.Processor} in this module's own
* {@code META-INF/services} — any consumer already depending on this artifact can put it on its
* annotation processor path with no separate module or extra dependency. Generic on purpose: this
* class has no knowledge of what the baked value is used for, only how to bake one in.
*
* <p>Emits its generated file on the very first round, not on {@link
* RoundEnvironment#processingOver()}: generating a new type during the last round is invalid per
* {@link Filer#createSourceFile}'s own contract (javac warns "will not be subject to annotation
* processing" and doesn't reliably compile it into the same pass). The first round is guaranteed
* to happen exactly once before any dependent source is compiled, and this processor doesn't need
* to observe anything from other rounds, so there's no reason to wait for the last one.
*/
@SupportedAnnotationTypes("*")
@SupportedOptions({BuildModeProcessor.OPTION, BuildModeProcessor.TARGET_CLASS_OPTION})
@SupportedSourceVersion(SourceVersion.RELEASE_25)
public final class BuildModeProcessor extends AbstractProcessor {
/** {@code -A} option carrying the baked value, {@code "true"}/{@code "false"} (case-sensitive,
* matching {@link Boolean#parseBoolean}'s own contract, deliberately not more lenient than
* that — an unrecognized value should fail the build, not silently parse as {@code false}). */
public static final String OPTION = "cfc.buildmode.value";
/** {@code -A} option naming the fully-qualified generated class, e.g.
* {@code com.cloudforgeci.manager.generated.BuildProvenance}. Required, not defaulted: a
* processor silently picking its own package/class name for every consumer would be a worse
* surprise than requiring one explicit line per consumer's build. */
public static final String TARGET_CLASS_OPTION = "cfc.buildmode.targetClass";
/** Name of the generated field, fixed rather than configurable — one processor responsibility
* at a time; consumers needing a different field name can trivially reference this one by its
* fully-qualified name from wherever they need it. */
private static final String FIELD_NAME = "VALUE";
private boolean emitted;
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (emitted) {
return false;
}
emitted = true;
Messager messager = processingEnv.getMessager();
Map<String, String> options = processingEnv.getOptions();
// This processor is registered in cloudforge-core's own META-INF/services, so it's on the
// classpath -- and, by javac's default auto-discovery, auto-RUN -- for every module that
// merely depends on cloudforge-core, which is nearly every module in this platform, not
// just the one that actually wants a baked constant. TARGET_CLASS_OPTION absent is the
// "this module never asked for this" case and must stay a silent no-op, not an error, or
// adding this class would break every other module's build the moment cloudforge-core
// republishes. Only once a caller has clearly opted in (targetClass given) does a further
// problem (missing/malformed value) become a real, intended-to-be-loud misconfiguration.
String targetClass = options.get(TARGET_CLASS_OPTION);
if (targetClass == null) {
return false;
}
String rawValue = options.get(OPTION);
if (rawValue == null) {
messager.printMessage(Diagnostic.Kind.ERROR, "BuildModeProcessor: -A" + TARGET_CLASS_OPTION
+ " was given but -A" + OPTION + "=<true|false> was not -- both are required together.");
return false;
}
if (!"true".equals(rawValue) && !"false".equals(rawValue)) {
messager.printMessage(Diagnostic.Kind.ERROR, "BuildModeProcessor's -A" + OPTION
+ " must be exactly \"true\" or \"false\", got \"" + rawValue + "\".");
return false;
}
writeGeneratedClass(targetClass, rawValue, messager);
return false;
}
private void writeGeneratedClass(String fullyQualifiedName, String literal, Messager messager) {
int lastDot = fullyQualifiedName.lastIndexOf('.');
String packageName = lastDot < 0 ? "" : fullyQualifiedName.substring(0, lastDot);
String simpleName = lastDot < 0 ? fullyQualifiedName : fullyQualifiedName.substring(lastDot + 1);
Filer filer = processingEnv.getFiler();
try {
JavaFileObject sourceFile = filer.createSourceFile(fullyQualifiedName);
try (Writer writer = sourceFile.openWriter()) {
if (!packageName.isEmpty()) {
writer.write("package " + packageName + ";\n\n");
}
writer.write("/** Generated at compile time by " + BuildModeProcessor.class.getName()
+ " -- see that class's javadoc. Do not edit; do not commit. */\n");
writer.write("public final class " + simpleName + " {\n");
writer.write(" public static final boolean " + FIELD_NAME + " = " + literal + ";\n");
writer.write(" private " + simpleName + "() {}\n");
writer.write("}\n");
}
} catch (IOException e) {
messager.printMessage(Diagnostic.Kind.ERROR,
"BuildModeProcessor could not write " + fullyQualifiedName + ": " + e.getMessage());
}
}
}