• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jreleaser / jreleaser / #470

20 Mar 2025 08:32PM UTC coverage: 49.197% (-0.9%) from 50.067%
#470

push

github

web-flow
docs: Add anneloreegger as a contributor for code

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>

24776 of 50361 relevant lines covered (49.2%)

0.49 hits per line

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

68.13
/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 org.jreleaser.assemblers.AssemblerUtils.copyJars;
47
import static org.jreleaser.assemblers.AssemblerUtils.readJavaVersion;
48
import static org.jreleaser.model.Constants.KEY_ARCHIVE_FORMAT;
49
import static org.jreleaser.util.FileType.EXE;
50
import static org.jreleaser.util.StringUtils.isNotBlank;
51

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

59
    public NativeImageAssemblerProcessor(JReleaserContext context) {
60
        super(context);
1✔
61
    }
1✔
62

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

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

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

85
        // install native-image
86
        installNativeImage(graalPath);
1✔
87
        installComponents(graalPath);
1✔
88

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

95
        nativeImage(props, assembleDirectory, graalPath, jars, imageName);
1✔
96
    }
1✔
97

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

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

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

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

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

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

142
        String executable = assembler.getExecutable();
1✔
143
        if (PlatformUtils.isWindows()) {
1✔
144
            executable += EXE.extension();
×
145
        }
146
        context.getLogger().info("- {}", finalImageName);
1✔
147

148
        Path image = assembleDirectory.resolve(executable).toAbsolutePath();
1✔
149
        try {
150
            if (Files.exists(image)) {
1✔
151
                Files.deleteIfExists(image);
×
152
            }
153
        } catch (IOException e) {
×
154
            throw new AssemblerProcessingException(RB.$("ERROR_assembler_delete_image", executable), e);
×
155
        }
1✔
156

157
        assembler.getArgs().stream()
1✔
158
            .filter(arg -> arg.startsWith("-H:Name"))
1✔
159
            .findFirst()
1✔
160
            .ifPresent(assembler.getArgs()::remove);
1✔
161

162
        Path nativeImageExecutable = graalPath
1✔
163
            .resolve("bin")
1✔
164
            .resolve(PlatformUtils.isWindows() ? "native-image.cmd" : "native-image")
1✔
165
            .toAbsolutePath();
1✔
166

167
        Command cmd = new Command(nativeImageExecutable.toString(), true)
1✔
168
            .args(assembler.getArgs());
1✔
169

170
        NativeImageAssembler.PlatformCustomizer customizer = assembler.getResolvedPlatformCustomizer();
1✔
171
        cmd.args(customizer.getArgs());
1✔
172

173
        if (isNotBlank(assembler.getJava().getMainModule())) {
1✔
174
            cmd.arg("--module")
×
175
                .arg(assembler.getJava().getMainModule() + "/" + assembler.getJava().getMainClass());
×
176

177
            cmd.arg("--module-path")
×
178
                .arg(jars.stream()
×
179
                    .map(Path::toAbsolutePath)
×
180
                    .map(Path::getParent)
×
181
                    .distinct()
×
182
                    .map(Path::toString)
×
183
                    .map(this::maybeQuote)
×
184
                    .collect(joining(File.pathSeparator)));
×
185

186
        } else {
187
            cmd.arg("-jar")
1✔
188
                .arg(maybeQuote(assembler.getMainJar().getEffectivePath(context, assembler).toAbsolutePath().toString()));
1✔
189

190
            if (!jars.isEmpty()) {
1✔
191
                cmd.arg("-cp")
1✔
192
                    .arg(jars.stream()
1✔
193
                        .map(Path::toAbsolutePath)
1✔
194
                        .map(image.getParent()::relativize)
1✔
195
                        .map(Path::toString)
1✔
196
                        .map(this::maybeQuote)
1✔
197
                        .collect(joining(File.pathSeparator)));
1✔
198
            }
199
        }
200

201
        cmd.arg("-H:Name=" + assembler.getExecutable());
1✔
202
        context.getLogger().debug(String.join(" ", cmd.getArgs()));
1✔
203
        executeCommand(image.getParent(), cmd);
1✔
204

205
        if (assembler.getUpx().isEnabled()) {
1✔
206
            upx(image);
1✔
207
        }
208

209
        try {
210
            Path tempDirectory = Files.createTempDirectory("jreleaser");
1✔
211
            Path distDirectory = tempDirectory.resolve(finalImageName);
1✔
212
            Files.createDirectories(distDirectory);
1✔
213
            Path binDirectory = distDirectory.resolve(BIN_DIRECTORY);
1✔
214
            Files.createDirectories(binDirectory);
1✔
215
            Files.copy(image, binDirectory.resolve(image.getFileName()));
1✔
216
            FileUtils.copyFiles(context.getLogger(),
1✔
217
                context.getBasedir(),
1✔
218
                distDirectory, path -> path.getFileName().startsWith(LICENSE));
1✔
219
            copyTemplates(context, props, distDirectory);
1✔
220
            copyArtifacts(context, distDirectory, platform, true);
1✔
221
            copyFiles(context, distDirectory);
1✔
222
            copyFileSets(context, distDirectory);
1✔
223
            generateSwidTag(context, distDirectory);
1✔
224

225
            String str = assembler.getGraal().getExtraProperties()
1✔
226
                .getOrDefault(KEY_ARCHIVE_FORMAT, assembler.getArchiveFormat())
1✔
227
                .toString();
1✔
228
            Archive.Format archiveFormat = Archive.Format.of(str);
1✔
229

230
            Path imageArchive = assembleDirectory.resolve(finalImageName + "." + archiveFormat.extension());
1✔
231
            FileUtils.packArchive(tempDirectory, imageArchive, assembler.getOptions().toOptions());
1✔
232

233
            context.getLogger().debug("- {}", imageArchive.getFileName());
1✔
234
        } catch (IOException e) {
×
235
            throw new AssemblerProcessingException(RB.$("ERROR_unexpected_error"), e);
×
236
        }
1✔
237
    }
1✔
238

239
    private void upx(Path image) throws AssemblerProcessingException {
240
        Upx upx = new Upx(context.asImmutable(), assembler.getUpx().getVersion());
1✔
241
        try {
242
            if (!upx.setup()) {
1✔
243
                context.getLogger().warn(RB.$("tool_unavailable", "upx"));
1✔
244
                return;
1✔
245
            }
246
        } catch (ToolException e) {
×
247
            throw new AssemblerProcessingException(e.getMessage(), e);
×
248
        }
×
249

250
        List<String> args = new ArrayList<>(assembler.getUpx().getArgs());
×
251
        args.add(image.getFileName().toString());
×
252
        context.getLogger().info("  upx {}", image.getFileName().toString());
×
253

254
        try {
255
            upx.invoke(image.getParent(), args);
×
256
        } catch (CommandException e) {
×
257
            throw new AssemblerProcessingException(RB.$("ERROR_unexpected_error"), e);
×
258
        }
×
259
    }
×
260

261
    private String readGraalVersion(Path path) throws AssemblerProcessingException {
262
        Path release = path.resolve("release");
1✔
263
        if (!Files.exists(release)) {
1✔
264
            throw new AssemblerProcessingException(RB.$("ERROR_assembler_invalid_graal_release", path.toAbsolutePath()));
×
265
        }
266

267
        try (InputStream in = Files.newInputStream(release)) {
1✔
268
            Properties props = new Properties();
1✔
269
            props.load(in);
1✔
270
            if (props.containsKey(KEY_GRAALVM_VERSION)) {
1✔
271
                String version = props.getProperty(KEY_GRAALVM_VERSION);
1✔
272
                if (version.startsWith("\"") && version.endsWith("\"")) {
1✔
273
                    return version.substring(1, version.length() - 1);
1✔
274
                }
275
                return version;
×
276
            } else {
277
                throw new AssemblerProcessingException(RB.$("ERROR_assembler_invalid_graal_release_file", release.toAbsolutePath()));
×
278
            }
279
        } catch (IOException e) {
1✔
280
            throw new AssemblerProcessingException(RB.$("ERROR_assembler_invalid_graal_release_file", release.toAbsolutePath()), e);
×
281
        }
282
    }
283
}
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