• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

jreleaser / jreleaser / #558

08 Dec 2025 02:56PM UTC coverage: 48.239% (+0.02%) from 48.215%
#558

push

github

aalmiray
feat(core): warn when a name template cannot be resolved

Closes #1960

Closes #1961

299 of 573 new or added lines in 133 files covered. (52.18%)

4 existing lines in 4 files now uncovered.

26047 of 53996 relevant lines covered (48.24%)

0.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

69.05
/core/jreleaser-engine/src/main/java/org/jreleaser/assemblers/NativeImageAssemblerProcessor.java
1
/*
2
 * SPDX-License-Identifier: Apache-2.0
3
 *
4
 * Copyright 2020-2025 The JReleaser authors.
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 *     https://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
package org.jreleaser.assemblers;
19

20
import org.jreleaser.bundle.RB;
21
import org.jreleaser.model.Archive;
22
import org.jreleaser.model.Constants;
23
import org.jreleaser.model.internal.JReleaserContext;
24
import org.jreleaser.model.internal.assemble.NativeImageAssembler;
25
import org.jreleaser.model.spi.assemble.AssemblerProcessingException;
26
import org.jreleaser.mustache.TemplateContext;
27
import org.jreleaser.sdk.command.Command;
28
import org.jreleaser.sdk.command.CommandException;
29
import org.jreleaser.sdk.tool.ToolException;
30
import org.jreleaser.sdk.tool.Upx;
31
import org.jreleaser.util.FileUtils;
32
import org.jreleaser.util.PlatformUtils;
33
import org.jreleaser.version.SemanticVersion;
34

35
import java.io.File;
36
import java.io.IOException;
37
import java.io.InputStream;
38
import java.nio.file.Files;
39
import java.nio.file.Path;
40
import java.util.ArrayList;
41
import java.util.List;
42
import java.util.Properties;
43
import java.util.Set;
44

45
import static java.util.stream.Collectors.joining;
46
import static java.util.stream.Collectors.toList;
47
import static org.jreleaser.assemblers.AssemblerUtils.copyJars;
48
import static org.jreleaser.assemblers.AssemblerUtils.readJavaVersion;
49
import static org.jreleaser.model.Constants.KEY_ARCHIVE_FORMAT;
50
import static org.jreleaser.mustache.Templates.resolveTemplate;
51
import static org.jreleaser.util.FileType.EXE;
52
import static org.jreleaser.util.StringUtils.isNotBlank;
53

54
/**
55
 * @author Andres Almiray
56
 * @since 0.2.0
57
 */
58
public class NativeImageAssemblerProcessor extends AbstractAssemblerProcessor<org.jreleaser.model.api.assemble.NativeImageAssembler, NativeImageAssembler> {
59
    private static final String KEY_GRAALVM_VERSION = "GRAALVM_VERSION";
60

61
    public NativeImageAssemblerProcessor(JReleaserContext context) {
62
        super(context);
1✔
63
    }
1✔
64

65
    @Override
66
    protected void doAssemble(TemplateContext props) throws AssemblerProcessingException {
67
        if (!assembler.getGraal().isActiveAndSelected()) return;
1✔
68

69
        // verify graal
70
        Path graalPath = assembler.getGraal().getEffectivePath(context, assembler);
1✔
71
        SemanticVersion javaVersion = SemanticVersion.of(readJavaVersion(graalPath));
1✔
72
        SemanticVersion graalVersion = SemanticVersion.of(readGraalVersion(graalPath));
1✔
73
        context.getLogger().debug(RB.$("assembler.graal.java"), javaVersion, graalPath.toAbsolutePath().toString());
1✔
74
        context.getLogger().debug(RB.$("assembler.graal.graal"), graalVersion, graalPath.toAbsolutePath().toString());
1✔
75

76
        String platform = assembler.getGraal().getPlatform();
1✔
77
        // copy jars to assembly
78
        Path assembleDirectory = props.get(Constants.KEY_DISTRIBUTION_ASSEMBLE_DIRECTORY);
1✔
79
        Path jarsDirectory = assembleDirectory.resolve(JARS_DIRECTORY);
1✔
80
        Path universalJarsDirectory = jarsDirectory.resolve(UNIVERSAL_DIRECTORY);
1✔
81
        context.getLogger().debug(RB.$("assembler.copy.jars"), context.relativizeToBasedir(universalJarsDirectory));
1✔
82
        Set<Path> jars = copyJars(context, assembler, universalJarsDirectory, "");
1✔
83
        Path platformJarsDirectory = jarsDirectory.resolve(platform);
1✔
84
        context.getLogger().debug(RB.$("assembler.copy.jars"), context.relativizeToBasedir(platformJarsDirectory));
1✔
85
        jars.addAll(copyJars(context, assembler, platformJarsDirectory, platform));
1✔
86

87
        // install native-image
88
        installNativeImage(graalPath);
1✔
89
        installComponents(graalPath);
1✔
90

91
        // run native-image
92
        String imageName = assembler.getResolvedImageName(context);
1✔
93
        if (isNotBlank(assembler.getImageNameTransform())) {
1✔
94
            imageName = assembler.getResolvedImageNameTransform(context);
×
95
        }
96

97
        nativeImage(props, assembleDirectory, graalPath, jars, imageName);
1✔
98
    }
1✔
99

100
    private void installNativeImage(Path graalPath) throws AssemblerProcessingException {
101
        Path nativeImageExecutable = graalPath
1✔
102
            .resolve("bin")
1✔
103
            .resolve(PlatformUtils.isWindows() ? "native-image.cmd" : "native-image")
1✔
104
            .toAbsolutePath();
1✔
105

106
        if (!Files.exists(nativeImageExecutable)) {
1✔
107
            Path guExecutable = graalPath
×
108
                .resolve(BIN_DIRECTORY)
×
109
                .resolve(PlatformUtils.isWindows() ? "gu.cmd" : "gu")
×
110
                .toAbsolutePath();
×
111

112
            context.getLogger().debug(RB.$("assembler.graal.install.native.exec"));
×
113
            Command cmd = new Command(guExecutable.toString())
×
114
                .arg("install")
×
115
                .arg("-n")
×
116
                .arg("native-image");
×
117
            context.getLogger().debug(String.join(" ", cmd.getArgs()));
×
118
            executeCommand(cmd);
×
119
        }
120
    }
1✔
121

122
    private void installComponents(Path graalPath) throws AssemblerProcessingException {
123
        Path guExecutable = graalPath
1✔
124
            .resolve(BIN_DIRECTORY)
1✔
125
            .resolve(PlatformUtils.isWindows() ? "gu.cmd" : "gu")
1✔
126
            .toAbsolutePath();
1✔
127

128
        for (String component : assembler.getComponents()) {
1✔
129
            context.getLogger().debug(RB.$("assembler.graal.install.component", component));
×
130
            Command cmd = new Command(guExecutable.toString())
×
131
                .arg("install")
×
132
                .arg("-n")
×
133
                .arg(component);
×
134
            context.getLogger().debug(String.join(" ", cmd.getArgs()));
×
135
            executeCommand(cmd);
×
136
        }
×
137
    }
1✔
138

139
    private void nativeImage(TemplateContext props, Path assembleDirectory, Path graalPath, Set<Path> jars, String imageName) throws AssemblerProcessingException {
140
        String platform = assembler.getGraal().getPlatform();
1✔
141
        String platformReplaced = assembler.getPlatform().applyReplacements(platform);
1✔
142
        String finalImageName = imageName + "-" + platformReplaced;
1✔
143

144
        String executable = assembler.getExecutable();
1✔
145
        if (!assembler.getArchiving().isEnabled()) {
1✔
146
            executable = finalImageName;
×
147
        }
148
        String executableFileName = executable;
1✔
149
        if (PlatformUtils.isWindows()) {
1✔
150
            executableFileName += EXE.extension();
×
151
        }
152
        context.getLogger().info("- {}", finalImageName);
1✔
153

154
        Path image = assembleDirectory.resolve(executableFileName).toAbsolutePath();
1✔
155
        try {
156
            if (Files.exists(image)) {
1✔
157
                Files.deleteIfExists(image);
×
158
            }
159
        } catch (IOException e) {
×
160
            throw new AssemblerProcessingException(RB.$("ERROR_assembler_delete_image", executableFileName), e);
×
161
        }
1✔
162

163
        assembler.getArgs().stream()
1✔
164
            .filter(arg -> arg.startsWith("-H:Name"))
1✔
165
            .findFirst()
1✔
166
            .ifPresent(assembler.getArgs()::remove);
1✔
167

168
        Path nativeImageExecutable = graalPath
1✔
169
            .resolve("bin")
1✔
170
            .resolve(PlatformUtils.isWindows() ? "native-image.cmd" : "native-image")
1✔
171
            .toAbsolutePath();
1✔
172

173
        Command cmd = new Command(nativeImageExecutable.toString(), true)
1✔
174
            .args(assembler.getArgs().stream()
1✔
175
                .map(arg -> resolveTemplate(context.getLogger(), arg, props))
1✔
176
                .collect(toList()));
1✔
177

178
        NativeImageAssembler.PlatformCustomizer customizer = assembler.getResolvedPlatformCustomizer();
1✔
179
        cmd.args(customizer.getArgs().stream()
1✔
180
            .map(arg -> resolveTemplate(context.getLogger(), arg, props))
1✔
181
            .collect(toList()));
1✔
182

183
        if (isNotBlank(assembler.getJava().getMainModule())) {
1✔
184
            cmd.arg("--module")
×
NEW
185
                .arg(resolveTemplate(context.getLogger(), assembler.getJava().getMainModule(), props) + "/" + resolveTemplate(context.getLogger(), assembler.getJava().getMainClass(), props));
×
186

187
            cmd.arg("--module-path")
×
188
                .arg(jars.stream()
×
189
                    .map(Path::toAbsolutePath)
×
190
                    .map(Path::getParent)
×
191
                    .distinct()
×
192
                    .map(Path::toString)
×
193
                    .map(this::maybeQuote)
×
194
                    .collect(joining(File.pathSeparator)));
×
195

196
        } else {
197
            cmd.arg("-jar")
1✔
198
                .arg(maybeQuote(assembler.getMainJar().getEffectivePath(context, assembler).toAbsolutePath().toString()));
1✔
199

200
            if (!jars.isEmpty()) {
1✔
201
                cmd.arg("-cp")
1✔
202
                    .arg(jars.stream()
1✔
203
                        .map(Path::toAbsolutePath)
1✔
204
                        .map(image.getParent()::relativize)
1✔
205
                        .map(Path::toString)
1✔
206
                        .map(this::maybeQuote)
1✔
207
                        .collect(joining(File.pathSeparator)));
1✔
208
            }
209
        }
210

211
        cmd.arg("-H:Name=" + executable);
1✔
212
        context.getLogger().debug(String.join(" ", cmd.getArgs()));
1✔
213
        executeCommand(image.getParent(), cmd);
1✔
214

215
        if (assembler.getUpx().isEnabled()) {
1✔
216
            upx(image);
1✔
217
        }
218

219
        if (!assembler.getArchiving().isEnabled()) return;
1✔
220

221
        try {
222
            Path tempDirectory = Files.createTempDirectory("jreleaser");
1✔
223
            Path distDirectory = tempDirectory.resolve(finalImageName);
1✔
224
            Files.createDirectories(distDirectory);
1✔
225
            Path binDirectory = distDirectory.resolve(BIN_DIRECTORY);
1✔
226
            Files.createDirectories(binDirectory);
1✔
227
            Files.copy(image, binDirectory.resolve(image.getFileName()));
1✔
228
            FileUtils.copyFiles(context.getLogger(),
1✔
229
                context.getBasedir(),
1✔
230
                distDirectory, path -> path.getFileName().startsWith(LICENSE));
1✔
231
            copyTemplates(context, props, distDirectory);
1✔
232
            copyArtifacts(context, distDirectory, platform, true);
1✔
233
            copyFiles(context, distDirectory);
1✔
234
            copyFileSets(context, distDirectory);
1✔
235
            generateSwidTag(context, distDirectory);
1✔
236

237
            String str = assembler.getGraal().getExtraProperties()
1✔
238
                .getOrDefault(KEY_ARCHIVE_FORMAT, assembler.getArchiveFormat())
1✔
239
                .toString();
1✔
240
            Archive.Format archiveFormat = Archive.Format.of(str);
1✔
241

242
            Path imageArchive = assembleDirectory.resolve(finalImageName + "." + archiveFormat.extension());
1✔
243
            FileUtils.packArchive(tempDirectory, imageArchive, assembler.getOptions().toOptions());
1✔
244

245
            context.getLogger().debug("- {}", imageArchive.getFileName());
1✔
246
        } catch (IOException e) {
×
247
            throw new AssemblerProcessingException(RB.$("ERROR_unexpected_error"), e);
×
248
        }
1✔
249
    }
1✔
250

251
    private void upx(Path image) throws AssemblerProcessingException {
252
        Upx upx = new Upx(context.asImmutable(), assembler.getUpx().getVersion());
1✔
253
        try {
254
            if (!upx.setup()) {
1✔
255
                context.getLogger().warn(RB.$("tool_unavailable", "upx"));
1✔
256
                return;
1✔
257
            }
258
        } catch (ToolException e) {
×
259
            throw new AssemblerProcessingException(e.getMessage(), e);
×
260
        }
×
261

262
        List<String> args = new ArrayList<>(assembler.getUpx().getArgs());
×
263
        args.add(image.getFileName().toString());
×
264
        context.getLogger().info("  upx {}", image.getFileName().toString());
×
265

266
        try {
267
            upx.invoke(image.getParent(), args);
×
268
        } catch (CommandException e) {
×
269
            throw new AssemblerProcessingException(RB.$("ERROR_unexpected_error"), e);
×
270
        }
×
271
    }
×
272

273
    private String readGraalVersion(Path path) throws AssemblerProcessingException {
274
        Path release = path.resolve("release");
1✔
275
        if (!Files.exists(release)) {
1✔
276
            throw new AssemblerProcessingException(RB.$("ERROR_assembler_invalid_graal_release", path.toAbsolutePath()));
×
277
        }
278

279
        try (InputStream in = Files.newInputStream(release)) {
1✔
280
            Properties props = new Properties();
1✔
281
            props.load(in);
1✔
282
            if (props.containsKey(KEY_GRAALVM_VERSION)) {
1✔
283
                String version = props.getProperty(KEY_GRAALVM_VERSION);
1✔
284
                if (version.startsWith("\"") && version.endsWith("\"")) {
1✔
285
                    return version.substring(1, version.length() - 1);
1✔
286
                }
287
                return version;
×
288
            } else {
289
                throw new AssemblerProcessingException(RB.$("ERROR_assembler_invalid_graal_release_file", release.toAbsolutePath()));
×
290
            }
291
        } catch (IOException e) {
1✔
292
            throw new AssemblerProcessingException(RB.$("ERROR_assembler_invalid_graal_release_file", release.toAbsolutePath()), e);
×
293
        }
294
    }
295
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc