3939import io .quarkus .deployment .pkg .builditem .CurateOutcomeBuildItem ;
4040import io .quarkus .deployment .pkg .builditem .PurgeClassesBuildItem ;
4141import io .quarkus .maven .dependency .ArtifactKey ;
42+ import io .quarkus .maven .dependency .DependencyFlags ;
4243import io .quarkus .maven .dependency .ResolvedDependency ;
4344
4445public class PurgeProcessor {
4546
4647 private static final Logger log = Logger .getLogger (PurgeProcessor .class );
4748 private static final String SERVICE_LOADER_INTERNAL = "java/util/ServiceLoader" ;
4849 private static final String SISU_NAMED_RESOURCE = "META-INF/sisu/javax.inject.Named" ;
50+ private static final String META_INF_VERSIONS = "META-INF/versions" ;
51+ private static final String META_INF_SERVICES = "META-INF/services/" ;
4952
5053 @ BuildStep
5154 void analyzeReachableClasses (
@@ -98,46 +101,50 @@ void analyzeReachableClasses(
98101 // For multi-release entries, we pick the highest version <= appJavaVersion,
99102 // matching what JarFile.runtimeVersion() resolves at runtime.
100103 dep .getContentTree ().walkRaw (visit -> {
101- String relative = visit .getRelativePath ();
104+ String entry = visit .getResourceName ();
102105 // Handle multi-release version entries (META-INF/versions/N/...)
103- if (relative .startsWith ("META-INF/versions/" )) {
104- String afterVersions = relative .substring ("META-INF/versions/" .length ());
105- int slash = afterVersions .indexOf ('/' );
106- if (slash > 0 ) {
107- int version ;
108- try {
109- version = Integer .parseInt (afterVersions .substring (0 , slash ));
110- } catch (NumberFormatException e ) {
111- return ;
112- }
113- // Skip versions newer than the app's target — they won't be loaded at runtime
114- if (version > appJavaVersion ) {
115- return ;
116- }
117- String classPath = afterVersions .substring (slash + 1 );
118- if (isClassEntry (classPath )) {
119- String className = classNameOf (classPath );
120- // Replace existing bytecode only if this version is higher
121- // (higher version = closer to runtime resolution)
122- int currentVersion = depBytecodeVersion .getOrDefault (className , 0 );
123- if (version > currentVersion ) {
124- classToDep .put (className , key );
125- if (currentVersion == 0 && !depBytecode .containsKey (className )) {
126- classCount [0 ]++;
127- }
128- try (InputStream is = Files .newInputStream (visit .getPath ())) {
129- depBytecode .put (className , is .readAllBytes ());
130- depBytecodeVersion .put (className , version );
131- } catch (IOException e ) {
132- log .debugf (e , "Failed to read bytecode: %s" , visit .getPath ());
133- }
106+ if (entry .startsWith (META_INF_VERSIONS )) {
107+ if (entry .length () == META_INF_VERSIONS .length () || entry .charAt (META_INF_VERSIONS .length ()) != '/' ) {
108+ // if it does not start with META-INF/versions/
109+ return ;
110+ }
111+ // META-INF/versions/N/
112+ final int javaVersionSeparator = entry .indexOf ('/' , META_INF_VERSIONS .length () + 1 );
113+ if (javaVersionSeparator == -1 ) {
114+ return ;
115+ }
116+ int version ;
117+ try {
118+ version = Integer .parseInt (entry .substring (META_INF_VERSIONS .length () + 1 , javaVersionSeparator ));
119+ } catch (NumberFormatException e ) {
120+ return ;
121+ }
122+ // Skip versions newer than the app's target — they won't be loaded at runtime
123+ if (version > appJavaVersion ) {
124+ return ;
125+ }
126+ if (isClassEntry (entry )) {
127+ String className = classNameOf (entry , javaVersionSeparator + 1 );
128+ // Replace existing bytecode only if this version is higher
129+ // (higher version = closer to runtime resolution)
130+ int currentVersion = depBytecodeVersion .getOrDefault (className , 0 );
131+ if (version > currentVersion ) {
132+ classToDep .put (className , key );
133+ if (currentVersion == 0 && !depBytecode .containsKey (className )) {
134+ classCount [0 ]++;
135+ }
136+ try (InputStream is = Files .newInputStream (visit .getPath ())) {
137+ depBytecode .put (className , is .readAllBytes ());
138+ depBytecodeVersion .put (className , version );
139+ } catch (IOException e ) {
140+ log .debugf (e , "Failed to read bytecode: %s" , visit .getPath ());
134141 }
135142 }
136143 }
137144 return ;
138145 }
139- if (isClassEntry (relative )) {
140- String className = classNameOf (relative );
146+ if (isClassEntry (entry )) {
147+ String className = classNameOf (entry );
141148 classToDep .put (className , key );
142149 classCount [0 ]++;
143150 // Base class: only store if no versioned entry was seen yet
@@ -150,13 +157,15 @@ void analyzeReachableClasses(
150157 }
151158 }
152159 detectServiceLoaderCalls (visit .getPath (), className , serviceLoaderCalls );
160+ return ;
153161 }
154- if (relative .startsWith ("META-INF/services/" ) && !relative .endsWith ("/" )) {
155- parseServiceFile (visit .getPath (), relative , serviceProviders );
162+ if (entry .startsWith (META_INF_SERVICES ) && !entry .endsWith ("/" )) {
163+ parseServiceFile (visit .getPath (), entry , serviceProviders );
164+ return ;
156165 }
157166 // Collect sisu named components (META-INF/sisu/javax.inject.Named).
158167 // These are only included if ClassLoader.getResources() for this path is detected.
159- if (SISU_NAMED_RESOURCE .equals (relative )) {
168+ if (SISU_NAMED_RESOURCE .equals (entry )) {
160169 parseSisuNamedFile (visit .getPath (), sisuNamedClasses );
161170 }
162171 });
@@ -184,18 +193,19 @@ void analyzeReachableClasses(
184193 // to dependency classes (e.g. app code using commons-io IOUtils).
185194 final Map <String , byte []> appBytecode = new HashMap <>();
186195 appModel .getAppArtifact ().getContentTree ().walk (visit -> {
187- String relative = visit .getRelativePath ();
188- if (isClassEntry (relative )) {
189- String className = classNameOf (relative );
196+ String entry = visit .getResourceName ();
197+ if (isClassEntry (entry )) {
198+ String className = classNameOf (entry );
190199 detectServiceLoaderCalls (visit .getPath (), className , serviceLoaderCalls );
191200 try (InputStream is = Files .newInputStream (visit .getPath ())) {
192201 appBytecode .put (className , is .readAllBytes ());
193202 } catch (IOException e ) {
194203 log .debugf (e , "Failed to read app bytecode: %s" , visit .getPath ());
195204 }
205+ return ;
196206 }
197- if (relative .startsWith ("META-INF/services/" ) && !relative .endsWith ("/" )) {
198- parseServiceFile (visit .getPath (), relative , serviceProviders );
207+ if (entry .startsWith (META_INF_SERVICES ) && !entry .endsWith ("/" )) {
208+ parseServiceFile (visit .getPath (), entry , serviceProviders );
199209 }
200210 });
201211
@@ -209,12 +219,12 @@ void analyzeReachableClasses(
209219 final Set <String > roots = new HashSet <>();
210220 roots .add (mainClass .getClassName ());
211221 roots .addAll (generatedBytecode .keySet ());
212- for (ResolvedDependency dep : appModel .getRuntimeDependencies ( )) {
222+ for (ResolvedDependency dep : appModel .getDependencies ( DependencyFlags . RUNTIME_CP )) {
213223 if ("quarkus-bootstrap-runner" .equals (dep .getArtifactId ())) {
214224 dep .getContentTree ().walk (visit -> {
215- String relative = visit .getRelativePath ();
216- if (isClassEntry (relative )) {
217- roots .add (classNameOf (relative ));
225+ String entry = visit .getResourceName ();
226+ if (isClassEntry (entry )) {
227+ roots .add (classNameOf (entry ));
218228 }
219229 });
220230 break ;
@@ -836,19 +846,23 @@ private static String formatSize(long bytes) {
836846 }
837847 }
838848
839- private static boolean isClassEntry (String relativePath ) {
840- return relativePath .endsWith (".class" )
841- && !relativePath .equals ("module-info.class" )
842- && !relativePath .endsWith ("package-info.class" );
849+ private static boolean isClassEntry (String resourceName ) {
850+ return resourceName .endsWith (".class" )
851+ && !resourceName .equals ("module-info.class" )
852+ && !resourceName .endsWith ("package-info.class" );
853+ }
854+
855+ private static String classNameOf (String classResourceName ) {
856+ return classNameOf (classResourceName , 0 );
843857 }
844858
845- private static String classNameOf (String relativePath ) {
846- return relativePath .substring (0 , relativePath .length () - 6 ).replace ('/' , '.' );
859+ private static String classNameOf (String resourceName , int classNameStartIndex ) {
860+ return resourceName .substring (classNameStartIndex , resourceName .length () - 6 ).replace ('/' , '.' );
847861 }
848862
849863 private void parseServiceFile (Path file , String relativePath ,
850864 Map <String , Set <String >> serviceProviders ) {
851- String serviceInterface = relativePath .substring ("META-INF/services/" .length ());
865+ String serviceInterface = relativePath .substring (META_INF_SERVICES .length ());
852866 if (serviceInterface .isEmpty () || serviceInterface .contains ("/" )) {
853867 return ;
854868 }
@@ -967,16 +981,16 @@ public void visitMethodInsn(int opcode, String owner, String mname,
967981 private static int detectAppJavaVersion (ApplicationModel appModel ) {
968982 int [] majorVersion = new int [1 ];
969983 appModel .getAppArtifact ().getContentTree ().walk (visit -> {
970- if (majorVersion [0 ] > 0 ) {
971- return ;
972- }
973- String relative = visit .getRelativePath ();
974- if (isClassEntry (relative )) {
984+ String entry = visit .getResourceName ();
985+ if (isClassEntry (entry )) {
975986 try (InputStream is = Files .newInputStream (visit .getPath ())) {
976987 byte [] header = new byte [8 ];
977988 if (is .read (header ) == 8 ) {
978989 // Class file: magic (4 bytes) + minor (2 bytes) + major (2 bytes)
979990 majorVersion [0 ] = ((header [6 ] & 0xFF ) << 8 ) | (header [7 ] & 0xFF );
991+ if (majorVersion [0 ] > 0 ) {
992+ visit .stopWalking ();
993+ }
980994 }
981995 } catch (IOException e ) {
982996 // ignore, will try next class
0 commit comments