001/* 002 * Copyright 2017-2026 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; 017 018import com.google.cloud.tools.jib.api.CacheDirectoryCreationException; 019import com.google.cloud.tools.jib.api.Containerizer; 020import com.google.cloud.tools.jib.api.Credential; 021import com.google.cloud.tools.jib.api.ImageReference; 022import com.google.cloud.tools.jib.api.InvalidImageReferenceException; 023import com.google.cloud.tools.jib.api.Jib; 024import com.google.cloud.tools.jib.api.JibContainer; 025import com.google.cloud.tools.jib.api.JibContainerBuilder; 026import com.google.cloud.tools.jib.api.LogEvent; 027import com.google.cloud.tools.jib.api.RegistryException; 028import com.google.cloud.tools.jib.api.RegistryImage; 029import com.google.cloud.tools.jib.api.TarImage; 030import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; 031import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; 032import com.google.cloud.tools.jib.api.buildplan.FilePermissions; 033import com.google.cloud.tools.jib.api.buildplan.ImageFormat; 034import com.google.cloud.tools.jib.api.buildplan.Platform; 035import com.google.cloud.tools.jib.api.buildplan.Port; 036import io.micronaut.core.util.StringUtils; 037import io.micronaut.maven.core.DockerBuildStrategy; 038import io.micronaut.maven.core.MicronautRuntime; 039import io.micronaut.maven.jib.JibConfiguration; 040import io.micronaut.maven.jib.JibConfigurationService; 041import io.micronaut.maven.services.ApplicationConfigurationService; 042import io.micronaut.maven.services.DockerService; 043import org.apache.maven.execution.MavenSession; 044import org.apache.maven.model.Plugin; 045import org.apache.maven.plugin.MojoExecution; 046import org.apache.maven.plugin.MojoExecutionException; 047import org.apache.maven.plugins.annotations.Mojo; 048import org.apache.maven.plugins.annotations.Parameter; 049import org.apache.maven.plugins.annotations.ResolutionScope; 050import org.apache.maven.project.MavenProject; 051import org.codehaus.plexus.util.xml.Xpp3Dom; 052import org.slf4j.Logger; 053import org.slf4j.LoggerFactory; 054 055import javax.inject.Inject; 056import java.io.File; 057import java.io.IOException; 058import java.nio.file.Files; 059import java.nio.file.Path; 060import java.util.LinkedHashSet; 061import java.util.List; 062import java.util.Locale; 063import java.util.Optional; 064import java.util.Properties; 065import java.util.Set; 066import java.util.concurrent.ExecutionException; 067 068/** 069 * Builds a container image from a locally compiled native executable with Jib Core. 070 */ 071@Mojo(name = NativeImageJibMojo.NATIVE_IMAGE_JIB_GOAL, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME) 072public class NativeImageJibMojo extends AbstractDockerMojo { 073 074 public static final String NATIVE_IMAGE_JIB_GOAL = "native-image-jib"; 075 static final String ENABLED_PROPERTY = "micronaut.native-image.jib.enabled"; 076 static final String EXECUTABLE_PROPERTY = "micronaut.native-image.jib.executable"; 077 static final String BASE_IMAGE_PROPERTY = "micronaut.native-image.jib.base-image"; 078 static final String ALLOW_PLATFORM_MISMATCH_PROPERTY = "micronaut.native-image.jib.allow-platform-mismatch"; 079 static final String DEFAULT_APP_ROOT = "/app"; 080 static final String DEFAULT_TAR_NAME = "jib-image.tar"; 081 private static final String JIB_BUILD_GOAL_PROPERTY = "jib.buildGoal"; 082 private static final String JIB_BUILD_GOAL_PARAMETER = "jibBuildGoal"; 083 private static final String JIB_BUILD_GOAL_EXPRESSION = "${" + JIB_BUILD_GOAL_PROPERTY + "}"; 084 private static final String DEFAULT_JIB_BUILD_GOAL = "buildTar"; 085 private static final String INHERITED_DOCKER_BUILD_GOAL = "dockerBuild"; 086 private static final Logger LOG = LoggerFactory.getLogger(NativeImageJibMojo.class); 087 private static final List<String> SUPPORTED_JIB_BUILD_GOALS = List.of(DEFAULT_JIB_BUILD_GOAL, "build"); 088 private static final String GRAALVM_NATIVE_PLUGIN_KEY = "org.graalvm.buildtools:native-maven-plugin"; 089 private static final String DEFAULT_USER = "65532"; 090 private static final String LINUX = "linux"; 091 private static final String AMD64 = "amd64"; 092 private static final String ARM64 = "arm64"; 093 094 /** 095 * Packages the generated native executable into a container image with Jib after native-image packaging. 096 */ 097 @Parameter(property = ENABLED_PROPERTY, defaultValue = "false") 098 protected boolean enabled; 099 100 /** 101 * Native executable to copy into the image. Defaults to {@code target/<native imageName or artifactId>}. 102 */ 103 @Parameter(property = EXECUTABLE_PROPERTY) 104 protected File executable; 105 106 /** 107 * Runtime base image for the native executable image. Jib {@code from.image} has higher precedence. 108 */ 109 @Parameter(property = BASE_IMAGE_PROPERTY) 110 protected String nativeImageJibBaseImage; 111 112 /** 113 * Allows packaging when the host OS/architecture differs from the selected Linux container platform. 114 */ 115 @Parameter(property = ALLOW_PLATFORM_MISMATCH_PROPERTY, defaultValue = "false") 116 protected boolean allowPlatformMismatch; 117 118 private final String pluginVersion; 119 private final MojoExecution mojoExecution; 120 121 @SuppressWarnings("CdiInjectionPointsInspection") 122 @Inject 123 public NativeImageJibMojo(MavenProject mavenProject, JibConfigurationService jibConfigurationService, 124 ApplicationConfigurationService applicationConfigurationService, DockerService dockerService, 125 MavenSession mavenSession, MojoExecution mojoExecution) { 126 super(mavenProject, jibConfigurationService, applicationConfigurationService, dockerService, mavenSession, 127 mojoExecution); 128 this.mojoExecution = mojoExecution; 129 pluginVersion = Optional.ofNullable(mojoExecution) 130 .map(MojoExecution::getPlugin) 131 .map(Plugin::getVersion) 132 .filter(StringUtils::hasText) 133 .orElse("unknown"); 134 } 135 136 @Override 137 public void execute() throws MojoExecutionException { 138 if (!enabled) { 139 getLog().debug("Skipping native image Jib packaging because " + ENABLED_PROPERTY + " is not enabled."); 140 return; 141 } 142 applyDefaultJibBuildGoal(); 143 validateNativeImageJibBuildGoal(); 144 validateRuntime(); 145 Platform platform = resolvePlatform(); 146 validatePlatform(platform); 147 Path nativeExecutable = resolveExecutable(); 148 if (!Files.isRegularFile(nativeExecutable)) { 149 throw new MojoExecutionException("Native executable not found: " + nativeExecutable 150 + ". Build with native-image packaging and -D" + ENABLED_PROPERTY + "=true, or set -D" 151 + EXECUTABLE_PROPERTY + "=<path>."); 152 } 153 ImageReference imageReference = parseImageReference(primaryImage()); 154 JibContainerBuilder builder = createContainerBuilder(nativeExecutable, platform); 155 Containerizer containerizer = createContainerizer(imageReference); 156 try { 157 JibContainer jibContainer = containerize(builder, containerizer); 158 getLog().info("Built native image container " + jibContainer.getTargetImage()); 159 } catch (InterruptedException e) { 160 Thread.currentThread().interrupt(); 161 throw new MojoExecutionException("Interrupted while building native image container", e); 162 } catch (CacheDirectoryCreationException | ExecutionException | IOException | RegistryException e) { 163 throw new MojoExecutionException("Failed to build native image container with Jib: " + e.getMessage(), e); 164 } 165 } 166 167 final JibContainerBuilder createContainerBuilder(Path nativeExecutable, Platform platform) throws MojoExecutionException { 168 String baseImage = baseImage(); 169 String executableName = nativeExecutable.getFileName().toString(); 170 AbsoluteUnixPath containerExecutable = AbsoluteUnixPath.get(DEFAULT_APP_ROOT + "/" + executableName); 171 var executableLayer = FileEntriesLayer.builder() 172 .setName("native executable") 173 .addEntry(nativeExecutable, containerExecutable, FilePermissions.fromOctalString("755")) 174 .build(); 175 return fromBaseImage(baseImage) 176 .addFileEntriesLayer(executableLayer) 177 .setEntrypoint(entrypoint(containerExecutable)) 178 .setProgramArguments(programArguments()) 179 .setExposedPorts(exposedPorts()) 180 .setFormat(ImageFormat.Docker) 181 .setPlatforms(Set.of(platform)) 182 .setWorkingDirectory(AbsoluteUnixPath.get(DEFAULT_APP_ROOT)) 183 .setUser(jibConfigurationService.getUser().orElse(DEFAULT_USER)); 184 } 185 186 private JibContainerBuilder fromBaseImage(String baseImage) throws MojoExecutionException { 187 if ("scratch".equals(baseImage)) { 188 return Jib.fromScratch(); 189 } 190 RegistryImage fromImage = registryImage(baseImage, jibConfigurationService.getFromCredentials()); 191 if (jibConfigurationService.getFromCredentials().isEmpty()) { 192 jibConfigurationService.resolveCredentialForImage(baseImage, LOG) 193 .ifPresent(credential -> addCredential(fromImage, credential)); 194 } 195 return Jib.from(fromImage); 196 } 197 198 final JibContainer containerize(JibContainerBuilder builder, Containerizer containerizer) 199 throws InterruptedException, RegistryException, IOException, CacheDirectoryCreationException, ExecutionException { 200 return builder.containerize(containerizer); 201 } 202 203 private Containerizer createContainerizer(ImageReference imageReference) throws MojoExecutionException { 204 Containerizer containerizer; 205 if (DEFAULT_JIB_BUILD_GOAL.equals(jibBuildGoal)) { 206 Path output = jibConfigurationService.getOutputPathsTar() 207 .filter(StringUtils::hasText) 208 .map(Path::of) 209 .map(this::resolveTarOutputPath) 210 .orElseGet(() -> Path.of(mavenProject.getBuild().getDirectory(), DEFAULT_TAR_NAME)); 211 containerizer = Containerizer.to(TarImage.at(output).named(imageReference)); 212 } else { 213 RegistryImage targetImage = registryImage(imageReference.toString(), jibConfigurationService.getToCredentials()); 214 if (jibConfigurationService.getToCredentials().isEmpty()) { 215 jibConfigurationService.resolveCredentialForImage(imageReference.toString(), LOG) 216 .ifPresent(credential -> addCredential(targetImage, credential)); 217 } 218 containerizer = Containerizer.to(targetImage); 219 } 220 for (String tag : additionalTags(imageReference)) { 221 containerizer.withAdditionalTag(tag); 222 } 223 return containerizer 224 .addEventHandler(LogEvent.class, this::logJibEvent) 225 .setToolName("micronaut-maven-plugin") 226 .setToolVersion(pluginVersion); 227 } 228 229 private Path resolveTarOutputPath(Path output) { 230 if (output.isAbsolute()) { 231 return output; 232 } 233 return mavenProject.getBasedir().toPath().resolve(output); 234 } 235 236 private void validateNativeImageJibBuildGoal() throws MojoExecutionException { 237 if (!SUPPORTED_JIB_BUILD_GOALS.contains(jibBuildGoal)) { 238 throw new MojoExecutionException("Unsupported jib.buildGoal '" + jibBuildGoal 239 + "' for native image Jib packaging. Supported values are: " + String.join(", ", SUPPORTED_JIB_BUILD_GOALS) 240 + ". Use docker-native packaging for Docker-backed native image builds."); 241 } 242 } 243 244 private void applyDefaultJibBuildGoal() { 245 if (INHERITED_DOCKER_BUILD_GOAL.equals(jibBuildGoal) && !hasConfiguredJibBuildGoal()) { 246 jibBuildGoal = DEFAULT_JIB_BUILD_GOAL; 247 } 248 } 249 250 private boolean hasConfiguredJibBuildGoal() { 251 return hasProperty(mavenSession.getUserProperties(), JIB_BUILD_GOAL_PROPERTY) 252 || hasProperty(mavenSession.getSystemProperties(), JIB_BUILD_GOAL_PROPERTY) 253 || hasProperty(mavenProject.getProperties(), JIB_BUILD_GOAL_PROPERTY) 254 || hasConfiguredMojoParameter(JIB_BUILD_GOAL_PARAMETER); 255 } 256 257 private static boolean hasProperty(Properties properties, String key) { 258 return properties != null && properties.containsKey(key); 259 } 260 261 private boolean hasConfiguredMojoParameter(String parameterName) { 262 if (mojoExecution == null || !(mojoExecution.getConfiguration() instanceof Xpp3Dom configuration)) { 263 return false; 264 } 265 Xpp3Dom parameter = configuration.getChild(parameterName); 266 return parameter != null && !isDescriptorDefaultJibBuildGoal(parameter); 267 } 268 269 private static boolean isDescriptorDefaultJibBuildGoal(Xpp3Dom parameter) { 270 return JIB_BUILD_GOAL_EXPRESSION.equals(parameter.getValue()) 271 && INHERITED_DOCKER_BUILD_GOAL.equals(parameter.getAttribute("default-value")); 272 } 273 274 private void validateRuntime() throws MojoExecutionException { 275 MicronautRuntime runtime; 276 try { 277 runtime = MicronautRuntime.valueOf(micronautRuntime.toUpperCase(Locale.ENGLISH)); 278 } catch (IllegalArgumentException e) { 279 throw new MojoExecutionException("Unsupported micronaut.runtime '" + micronautRuntime 280 + "' for native image Jib packaging.", e); 281 } 282 DockerBuildStrategy buildStrategy = runtime.getBuildStrategy(); 283 if (buildStrategy == DockerBuildStrategy.LAMBDA || buildStrategy == DockerBuildStrategy.ORACLE_FUNCTION) { 284 throw new MojoExecutionException("native image Jib packaging does not support micronaut.runtime=" 285 + micronautRuntime + ". Use docker-native packaging for Lambda and Oracle Function native images."); 286 } 287 } 288 289 private Path resolveExecutable() { 290 if (executable != null) { 291 return executable.toPath(); 292 } 293 return Path.of(mavenProject.getBuild().getDirectory(), nativeImageName()); 294 } 295 296 private String nativeImageName() { 297 return configuredNativeImageName().orElse(mavenProject.getArtifactId()); 298 } 299 300 private Optional<String> configuredNativeImageName() { 301 Plugin plugin = mavenProject.getPlugin(GRAALVM_NATIVE_PLUGIN_KEY); 302 if (plugin != null && plugin.getConfiguration() instanceof Xpp3Dom configuration) { 303 Xpp3Dom imageName = configuration.getChild("imageName"); 304 if (imageName != null && StringUtils.hasText(imageName.getValue())) { 305 return Optional.of(evaluateMavenExpression(imageName.getValue())); 306 } 307 } 308 return Optional.empty(); 309 } 310 311 private String baseImage() throws MojoExecutionException { 312 String image = getJibFromImageSystemProperty() 313 .or(() -> getFromImage().filter(StringUtils::hasText)) 314 .or(() -> Optional.ofNullable(nativeImageJibBaseImage).filter(StringUtils::hasText)) 315 .or(() -> Optional.ofNullable(baseImageRun).filter(StringUtils::hasText)) 316 .orElse(DEFAULT_BASE_IMAGE_GRAALVM_RUN); 317 return validateImageReference("native image Jib base image", evaluateMavenExpression(image)); 318 } 319 320 private ImageReference parseImageReference(String image) throws MojoExecutionException { 321 try { 322 return ImageReference.parse(image); 323 } catch (InvalidImageReferenceException e) { 324 throw new MojoExecutionException("native image Jib target image is not a valid image reference: " + image, e); 325 } 326 } 327 328 private RegistryImage registryImage(String image, Optional<Credential> credential) throws MojoExecutionException { 329 try { 330 RegistryImage registryImage = RegistryImage.named(image); 331 credential.ifPresent(value -> addCredential(registryImage, value)); 332 return registryImage; 333 } catch (InvalidImageReferenceException e) { 334 throw new MojoExecutionException("Invalid image reference for native image Jib: " + image, e); 335 } 336 } 337 338 private static void addCredential(RegistryImage image, Credential credential) { 339 image.addCredential(credential.getUsername(), credential.getPassword()); 340 } 341 342 private String primaryImage() { 343 String image = jibConfigurationService.getToImage() 344 .map(this::evaluateMavenExpression) 345 .filter(StringUtils::hasText) 346 .orElse(mavenProject.getArtifactId()); 347 return ensureTag(image); 348 } 349 350 private Set<String> additionalTags(ImageReference primaryImage) throws MojoExecutionException { 351 Set<String> tags = new LinkedHashSet<>(); 352 String primaryTag = primaryImage.getTag().orElse(LATEST_TAG); 353 for (String tag : jibConfigurationService.getTags()) { 354 String evaluated = evaluateMavenExpression(tag); 355 if (!StringUtils.hasText(evaluated) || evaluated.equals(primaryTag)) { 356 continue; 357 } 358 if (!ImageReference.isValidTag(evaluated)) { 359 throw new MojoExecutionException("jib.to.tags contains an invalid image tag for native image Jib: " + evaluated); 360 } 361 tags.add(evaluated); 362 } 363 return tags; 364 } 365 366 private static String ensureTag(String image) { 367 int lastSlash = image.lastIndexOf('/'); 368 int tagSeparator = image.indexOf(':', lastSlash + 1); 369 if (tagSeparator < 0 && !image.contains("@")) { 370 return image + ":" + LATEST_TAG; 371 } 372 return image; 373 } 374 375 private List<String> entrypoint(AbsoluteUnixPath containerExecutable) { 376 List<String> configuredEntrypoint = jibConfigurationService.getEntrypoint(); 377 if (!configuredEntrypoint.isEmpty()) { 378 return configuredEntrypoint; 379 } 380 return List.of(containerExecutable.toString()); 381 } 382 383 private List<String> programArguments() { 384 List<String> args = jibConfigurationService.getArgs(); 385 if (!args.isEmpty()) { 386 return args; 387 } 388 if (appArguments != null) { 389 return appArguments; 390 } 391 return List.of(); 392 } 393 394 private Set<Port> exposedPorts() throws MojoExecutionException { 395 String ports = validateExposedPorts("jib.container.ports", getPorts()); 396 if (!StringUtils.hasText(ports)) { 397 return Set.of(); 398 } 399 try { 400 return com.google.cloud.tools.jib.api.Ports.parse(List.of(ports.trim().split("\\s+"))); 401 } catch (IllegalArgumentException e) { 402 throw new MojoExecutionException("native image Jib supports individual exposed ports such as 8080 or 8080/tcp: " + ports, e); 403 } 404 } 405 406 private Platform resolvePlatform() throws MojoExecutionException { 407 Set<JibConfiguration.PlatformConfiguration> configuredPlatforms = jibConfigurationService.getFromPlatforms(); 408 if (configuredPlatforms.isEmpty()) { 409 return detectedPlatform(); 410 } 411 if (configuredPlatforms.size() > 1) { 412 throw new MojoExecutionException("native image Jib supports exactly one target platform because it packages one local native executable."); 413 } 414 JibConfiguration.PlatformConfiguration configuredPlatform = configuredPlatforms.iterator().next(); 415 String os = configuredPlatform.os().orElse(LINUX); 416 String architecture = configuredPlatform.architecture() 417 .map(NativeImageJibMojo::normalizeArchitecture) 418 .orElseThrow(() -> new MojoExecutionException("jib.from.platforms must define an architecture for native image Jib packaging.")); 419 return new Platform(architecture, os); 420 } 421 422 private Platform detectedPlatform() { 423 return new Platform(normalizeArchitecture(System.getProperty("os.arch")), LINUX); 424 } 425 426 private void validatePlatform(Platform platform) throws MojoExecutionException { 427 if (!LINUX.equals(platform.getOs())) { 428 throw new MojoExecutionException("native image Jib packages Linux container images only. Configured platform is " 429 + platform.getOs() + "/" + platform.getArchitecture() + "."); 430 } 431 if (allowPlatformMismatch) { 432 return; 433 } 434 String hostOs = System.getProperty("os.name").toLowerCase(Locale.ENGLISH); 435 if (!hostOs.contains(LINUX)) { 436 throw new MojoExecutionException("native image Jib packaging requires a Linux host by default because the local native executable is copied into a Linux container image. " 437 + "Use docker-native packaging for Docker-backed cross-platform builds, or set -D" + ALLOW_PLATFORM_MISMATCH_PROPERTY + "=true for a known Linux cross-compiled executable."); 438 } 439 String hostArchitecture = normalizeArchitecture(System.getProperty("os.arch")); 440 if (!hostArchitecture.equals(platform.getArchitecture())) { 441 throw new MojoExecutionException("native image Jib host architecture " + hostArchitecture 442 + " does not match configured target architecture " + platform.getArchitecture() 443 + ". Set -D" + ALLOW_PLATFORM_MISMATCH_PROPERTY + "=true only for a known compatible cross-compiled executable."); 444 } 445 } 446 447 private static String normalizeArchitecture(String architecture) { 448 return switch (architecture) { 449 case "x86_64", "x64", AMD64 -> AMD64; 450 case "aarch64", ARM64 -> ARM64; 451 default -> architecture; 452 }; 453 } 454 455 private String evaluateMavenExpression(String expression) { 456 try { 457 return expressionEvaluator.evaluate(expression, String.class).toString(); 458 } catch (Exception e) { 459 LOG.debug("Could not evaluate Maven expression '{}'", expression, e); 460 return expression; 461 } 462 } 463 464 private void logJibEvent(LogEvent event) { 465 switch (event.getLevel()) { 466 case ERROR -> getLog().error(event.getMessage()); 467 case WARN -> getLog().warn(event.getMessage()); 468 case DEBUG -> getLog().debug(event.getMessage()); 469 default -> getLog().info(event.getMessage()); 470 } 471 } 472}