001/*
002 * Copyright 2017-2022 original authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package io.micronaut.maven.core;
017
018import org.apache.commons.io.FilenameUtils;
019import org.apache.maven.execution.MavenSession;
020import org.apache.maven.project.MavenProject;
021import org.apache.maven.toolchain.Toolchain;
022import org.apache.maven.toolchain.ToolchainManager;
023import org.codehaus.plexus.util.Os;
024
025import java.io.File;
026import java.io.IOException;
027import java.nio.file.Files;
028import java.nio.file.Path;
029import java.nio.file.Paths;
030import java.util.ArrayList;
031import java.util.List;
032import java.util.stream.Collectors;
033import java.util.stream.Stream;
034
035/**
036 * Shared utility methods for Micronaut Maven plugin modules.
037 */
038public final class MojoUtils {
039
040    public static final String THIS_PLUGIN = "io.micronaut.maven:micronaut-maven-plugin";
041    public static final String SHARED_ARENA_SUPPORT = "-H:+SharedArenaSupport";
042    private static final String JAVA = "java";
043    private static final String MOSTLY_STATIC_NATIVE_IMAGE_GRAALVM_FLAG = "-H:+StaticExecutableWithDynamicLibC";
044
045    private MojoUtils() {
046    }
047
048    public static String findJavaExecutable(ToolchainManager toolchainManager, MavenSession mavenSession) {
049        String executable;
050        Toolchain toolchain = toolchainManager.getToolchainFromBuildContext("jdk", mavenSession);
051        if (toolchain != null) {
052            executable = toolchain.findTool(JAVA);
053        } else {
054            executable = null;
055        }
056
057        // Fallback to default Java executable if toolchain is not configured or doesn't provide a valid tool.
058        if (executable == null) {
059            var javaBinariesDir = new File(new File(System.getProperty("java.home")), "bin");
060            if (Os.isFamily(Os.FAMILY_UNIX)) {
061                executable = new File(javaBinariesDir, JAVA).getAbsolutePath();
062            } else if (Os.isFamily(Os.FAMILY_WINDOWS)) {
063                executable = new File(javaBinariesDir, "java.exe").getAbsolutePath();
064            } else {
065                executable = JAVA;
066            }
067        }
068        return executable;
069    }
070
071    public static boolean hasMicronautMavenPlugin(MavenProject project) {
072        String[] parts = THIS_PLUGIN.split(":");
073        String groupId = parts[0];
074        String artifactId = parts[1];
075        return project.getBuildPlugins().stream()
076            .anyMatch(p -> p.getGroupId().equals(groupId) && p.getArtifactId().equals(artifactId));
077    }
078
079    public static List<String> computeNativeImageArgs(List<String> nativeImageBuildArgs, String baseImageRun, String argsFile) {
080        return computeNativeImageArgs(nativeImageBuildArgs, baseImageRun, argsFile, false);
081    }
082
083    public static List<String> computeNativeImageArgs(List<String> nativeImageBuildArgs, String baseImageRun, String argsFile, boolean sharedArenaSupport) {
084        var allNativeImageBuildArgs = new ArrayList<String>();
085        if (nativeImageBuildArgs != null && !nativeImageBuildArgs.isEmpty()) {
086            allNativeImageBuildArgs.addAll(nativeImageBuildArgs);
087        }
088        if (baseImageRun.contains("distroless") && !allNativeImageBuildArgs.contains(MOSTLY_STATIC_NATIVE_IMAGE_GRAALVM_FLAG)) {
089            allNativeImageBuildArgs.add(MOSTLY_STATIC_NATIVE_IMAGE_GRAALVM_FLAG);
090        }
091
092        List<String> argsFileContent = parseNativeImageArgsFile(argsFile).toList();
093        allNativeImageBuildArgs.addAll(argsFileContent);
094        if (sharedArenaSupport) {
095            if (allNativeImageBuildArgs.contains(SHARED_ARENA_SUPPORT)) {
096                removeDuplicateSharedArenaSupport(allNativeImageBuildArgs);
097            } else {
098                allNativeImageBuildArgs.add(SHARED_ARENA_SUPPORT);
099            }
100        }
101        return allNativeImageBuildArgs;
102    }
103
104    public static boolean supportsSharedArena(int graalVmMajorVersion) {
105        return graalVmMajorVersion >= 25;
106    }
107
108    private static void removeDuplicateSharedArenaSupport(List<String> nativeImageBuildArgs) {
109        boolean found = false;
110        for (int i = 0; i < nativeImageBuildArgs.size(); i++) {
111            if (!SHARED_ARENA_SUPPORT.equals(nativeImageBuildArgs.get(i))) {
112                continue;
113            }
114            if (found) {
115                nativeImageBuildArgs.remove(i);
116                i--;
117            } else {
118                found = true;
119            }
120        }
121    }
122
123    public static String parseConfigurationFilesDirectoriesArg(String arg) {
124        String[] split = arg.split("=");
125        String[] directories = split[1].split(",");
126        String separator = "/";
127        if (arg.contains("generateResourceConfig") || arg.contains("generateTestResourceConfig")) {
128            return Stream.of(directories)
129                .map(FilenameUtils::separatorsToUnix)
130                .map(directory -> {
131                    String[] splitDirectory = directory.split(separator);
132                    return "/home/app/" + splitDirectory[splitDirectory.length - 1];
133                })
134                .collect(Collectors.joining(","))
135                .transform(s -> "-H:ConfigurationFileDirectories=" + s);
136        } else {
137            return Stream.of(directories)
138                .map(FilenameUtils::separatorsToUnix)
139                .map(directory -> {
140                    String[] splitDirectory = directory.split(separator);
141                    String last4Directories = splitDirectory[splitDirectory.length - 4] + separator +
142                        splitDirectory[splitDirectory.length - 3] + separator +
143                        splitDirectory[splitDirectory.length - 2] + separator +
144                        splitDirectory[splitDirectory.length - 1];
145                    return "/home/app/graalvm-reachability-metadata/" + last4Directories;
146                })
147                .collect(Collectors.joining(","))
148                .transform(s -> "-H:ConfigurationFileDirectories=" + s);
149        }
150    }
151
152    private static Stream<String> parseNativeImageArgsFile(String argsFile) {
153        return parseNativeImageArgsFile(Paths.get(FilenameUtils.separatorsToSystem(argsFile)));
154    }
155
156    private static Stream<String> parseNativeImageArgsFile(Path argsFilePath) {
157        if (Files.exists(argsFilePath)) {
158            List<String> args;
159            try {
160                args = Files.readAllLines(argsFilePath);
161            } catch (IOException e) {
162                throw new RuntimeException("Could not read the args file: " + argsFilePath, e);
163            }
164            if (args.contains("-cp")) {
165                int cpPosition = args.indexOf("-cp");
166                args.remove(cpPosition);
167                args.remove(cpPosition);
168            }
169
170            return args.stream()
171                .filter(arg -> !arg.startsWith("-H:Name"))
172                .filter(arg -> !arg.startsWith("-H:Class"))
173                .filter(arg -> !arg.startsWith("-H:Path"))
174                .flatMap(arg -> {
175                    if (arg.startsWith("@")) {
176                        String fileName = arg.substring(1);
177                        return parseNativeImageArgsFile(resolveNestedArgsFilePath(argsFilePath, fileName));
178                    } else if (arg.startsWith("\\Q") && arg.endsWith("\\E")) {
179                        return Stream.of(parseQuotedClasspathArg(arg));
180                    } else if (arg.startsWith("-H:ConfigurationFileDirectories")) {
181                        return Stream.of(parseConfigurationFilesDirectoriesArg(arg));
182                    } else {
183                        return Stream.of(arg);
184                    }
185                });
186        } else {
187            throw new RuntimeException("Unable to find args file: " + argsFilePath);
188        }
189    }
190
191    private static Path resolveNestedArgsFilePath(Path argsFilePath, String fileName) {
192        Path nestedArgsFilePath = Paths.get(FilenameUtils.separatorsToSystem(fileName)).normalize();
193        if (nestedArgsFilePath.isAbsolute()) {
194            return nestedArgsFilePath;
195        }
196        Path parent = argsFilePath.getParent();
197        if (parent != null) {
198            Path resolved = parent.resolve(nestedArgsFilePath).normalize();
199            if (Files.exists(resolved)) {
200                return resolved;
201            }
202        }
203        return nestedArgsFilePath;
204    }
205
206    private static String parseQuotedClasspathArg(String arg) {
207        String quotedPath = arg.substring(2, arg.length() - 2);
208        String normalizedPath = FilenameUtils.separatorsToUnix(quotedPath);
209        String fileName = FilenameUtils.getName(quotedPath);
210        String layerDirectory = normalizedPath.contains("-SNAPSHOT/") || fileName.contains("SNAPSHOT")
211            ? "snapshot"
212            : "release";
213        return "\\Q/home/app/libs/" + layerDirectory + "/" + fileName + "\\E";
214    }
215}