Skip to content

Commit 444eeee

Browse files
authored
Respect build-tool console colors for Native Image output (#961)
* Respect Gradle plain console for Native Image output * Respect Maven console colors for Native Image * Ground Gradle console color unit tests * Place test citations before issue annotations
1 parent 8d0f742 commit 444eeee

11 files changed

Lines changed: 219 additions & 9 deletions

File tree

common/utils/src/main/java/org/graalvm/buildtools/model/resources/NativeImageFlags.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242

4343
public abstract class NativeImageFlags {
4444
public static final String BUILD_OUTPUT_COLORFUL = "-H:+BuildOutputColorful";
45+
// Legacy console-color arguments are shared by both adapters. §root/FS-native-builds.2.
46+
public static final String BUILD_OUTPUT_COLORLESS = "-H:-BuildOutputColorful";
4547
public static final String COLOR = "--color";
4648
public static final String CONFIGURATION_FILE_DIRECTORIES = "-H:ConfigurationFileDirectories";
4749
public static final String LAYER_CREATE = "-H:LayerCreate";

docs/spec/functional/native-image-builds.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ Both plugins must construct the `native-image` command line through shared utili
3333
[§common/FS-common-libraries.1](../../../common/docs/functional-spec.md#1-shared-native-image-utilities) so escaping, quoting, and argument-file conversion stay identical.
3434
Plugin-specific string handling must not bypass those utilities.
3535

36+
When a build tool disables colored console output, its adapter must explicitly disable Native
37+
Image colors with the flag supported by the discovered Native Image version. When console colors
38+
are enabled, plugin-specific rich-output configuration may enable them explicitly. User-supplied
39+
build arguments retain precedence over the adapter's detected console mode.
40+
3641
When a user-configured option set exceeds platform argument limits, or when configuration requests
3742
it explicitly, the command line must be written as a Native Image argument file (`@<path>`).
3843

native-gradle-plugin/docs/functional/native-image-invocation.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ configuration directories, generated resources, reachability metadata, layer opt
2626
conversion must come from common utilities rather than Gradle-only string handling, keeping Gradle
2727
aligned with [§root/FS-option-precedence](../../../docs/spec/functional/option-precedence.md#fs-option-precedence-command-line-input-and-durable-configuration-produce-one-option-state).
2828

29+
When Gradle uses its plain console, the Native Image invocation must explicitly disable colorful
30+
build output. Otherwise, the `richOutput` option controls Native Image's color-enabled argument,
31+
adapting [§root/FS-native-builds.2](../../../docs/spec/functional/native-image-builds.md#2-command-line-construction).
32+
2933
For a layer created from declared JARs, the command line must use those JARs as its classpath so
3034
the layer input remains limited to the declaration. A layer created from packages must instead
3135
retain the binary classpath, which supplies the classes selected by those package names.

native-gradle-plugin/src/functionalTest/groovy/org/graalvm/buildtools/gradle/JavaApplicationFunctionalTest.groovy

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,13 @@ package org.graalvm.buildtools.gradle
4343

4444
import org.gradle.util.GradleVersion
4545
import org.graalvm.buildtools.gradle.fixtures.AbstractFunctionalTest
46+
import org.graalvm.buildtools.gradle.fixtures.GraalVMSupport
47+
import org.graalvm.buildtools.utils.NativeImageUtils
4648
import spock.lang.Ignore
4749
import spock.lang.IgnoreIf
4850
import spock.lang.Issue
4951
import spock.lang.Requires
52+
import spock.lang.Unroll
5053

5154
import java.nio.file.Files
5255

@@ -180,6 +183,36 @@ class JavaApplicationFunctionalTest extends AbstractFunctionalTest {
180183

181184
}
182185

186+
// Gradle console mode controls Native Image color arguments. §FS-native-invocation.3.
187+
@Issue("https://github.com/graalvm/native-build-tools/issues/366")
188+
@Unroll
189+
def "passes Gradle's #console console color mode to Native Image"() {
190+
given:
191+
withSample("java-application")
192+
buildFile << """
193+
graalvmNative {
194+
binaries.all {
195+
richOutput = true
196+
verbose = true
197+
}
198+
}
199+
""".stripIndent()
200+
201+
when:
202+
run 'nativeCompile', "--console=${console}"
203+
204+
then:
205+
tasks {
206+
succeeded ':nativeCompile'
207+
}
208+
outputContains expectedColorArgument
209+
210+
where:
211+
console | expectedColorArgument
212+
'plain' | (NativeImageUtils.getMajorJDKVersion(GraalVMSupport.getGraalVMHomeVersionString()) >= 21 ? '--color=never' : '-H:-BuildOutputColorful')
213+
'rich' | (NativeImageUtils.getMajorJDKVersion(GraalVMSupport.getGraalVMHomeVersionString()) >= 21 ? '--color=always' : '-H:+BuildOutputColorful')
214+
}
215+
183216
@Issue("https://github.com/graalvm/native-build-tools/issues/129")
184217
def "can build a native image with dependencies only needed by native image"() {
185218
def nativeApp = getExecutableFile("build/native/nativeCompile/java-application")

native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/internal/NativeImageCommandLineProvider.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public class NativeImageCommandLineProvider implements CommandLineArgumentProvid
8484
private final Provider<RegularFile> classpathJar;
8585
private final Provider<Boolean> useArgFile;
8686
private final Provider<Integer> majorJDKVersion;
87-
private final Provider<Boolean> useColors;
87+
private final Provider<Boolean> plainConsole;
8888

8989
public NativeImageCommandLineProvider(Provider<NativeImageOptions> options,
9090
Provider<String> executableName,
@@ -93,15 +93,15 @@ public NativeImageCommandLineProvider(Provider<NativeImageOptions> options,
9393
Provider<RegularFile> classpathJar,
9494
Provider<Boolean> useArgFile,
9595
Provider<Integer> majorJDKVersion,
96-
Provider<Boolean> useColors) {
96+
Provider<Boolean> plainConsole) {
9797
this.options = options;
9898
this.executableName = executableName;
9999
this.workingDirectory = workingDirectory;
100100
this.outputDirectory = outputDirectory;
101101
this.classpathJar = classpathJar;
102102
this.useArgFile = useArgFile;
103103
this.majorJDKVersion = majorJDKVersion;
104-
this.useColors = useColors;
104+
this.plainConsole = plainConsole;
105105
}
106106

107107
@Nested
@@ -192,8 +192,10 @@ public List<String> asArguments() {
192192
appendBooleanOption(cliArgs, options.getVerbose(), NativeImageFlags.VERBOSE);
193193
appendBooleanOption(cliArgs, options.getSharedLibrary(), NativeImageFlags.SHARED);
194194
appendBooleanOption(cliArgs, options.getQuickBuild(), NativeImageFlags.QUICK_BUILD);
195-
if (useColors.get()) {
196-
appendBooleanOption(cliArgs, options.getRichOutput(), majorJDKVersion.getOrElse(-1) >= 21 ? NativeImageFlags.COLOR : NativeImageFlags.BUILD_OUTPUT_COLORFUL);
195+
if (plainConsole.get()) {
196+
cliArgs.add(majorJDKVersion.getOrElse(-1) >= 21 ? NativeImageFlags.COLOR + "=never" : NativeImageFlags.BUILD_OUTPUT_COLORLESS);
197+
} else {
198+
appendBooleanOption(cliArgs, options.getRichOutput(), majorJDKVersion.getOrElse(-1) >= 21 ? NativeImageFlags.COLOR + "=always" : NativeImageFlags.BUILD_OUTPUT_COLORFUL);
197199
}
198200
appendBooleanOption(cliArgs, options.getPgoInstrument(), NativeImageFlags.PGO_INSTRUMENT);
199201

native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/tasks/BuildNativeImageTask.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import org.gradle.api.file.FileSystemOperations;
5656
import org.gradle.api.file.RegularFile;
5757
import org.gradle.api.file.RegularFileProperty;
58+
import org.gradle.api.logging.configuration.ConsoleOutput;
5859
import org.gradle.api.model.ObjectFactory;
5960
import org.gradle.api.plugins.JavaBasePlugin;
6061
import org.gradle.api.provider.MapProperty;
@@ -95,7 +96,7 @@
9596
public abstract class BuildNativeImageTask extends DefaultTask {
9697
private final Provider<String> graalvmHomeProvider;
9798
private final NativeImageExecutableLocator.Diagnostics diagnostics;
98-
private final boolean useColors;
99+
private final boolean plainConsole;
99100

100101
@Internal
101102
public abstract Property<NativeImageOptions> getOptions();
@@ -271,7 +272,7 @@ public BuildNativeImageTask() {
271272
ProviderFactory providers = getProject().getProviders();
272273
this.diagnostics = new NativeImageExecutableLocator.Diagnostics();
273274
this.graalvmHomeProvider = graalvmHomeProvider(providers, diagnostics);
274-
this.useColors = "plain".equals(getProject().getGradle().getStartParameter().getConsoleOutput());
275+
this.plainConsole = ConsoleOutput.Plain.equals(getProject().getGradle().getStartParameter().getConsoleOutput());
275276
getDisableToolchainDetection().convention(false);
276277
}
277278

@@ -287,7 +288,7 @@ private List<String> buildActualCommandLineArgs(int majorJDKVersion) {
287288
getClasspathJar(),
288289
getUseArgFile(),
289290
getProviders().provider(() -> majorJDKVersion),
290-
getProviders().provider(() -> useColors))
291+
getProviders().provider(() -> plainConsole))
291292
.asArguments();
292293
}
293294

native-gradle-plugin/src/test/groovy/org/graalvm/buildtools/gradle/tasks/NativeImageCommandLineProviderTest.groovy

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,4 +186,71 @@ class NativeImageCommandLineProviderTest extends AbstractPluginTest {
186186
args.contains("-cp")
187187
args.contains(appJar.absolutePath)
188188
}
189+
// Plain Gradle consoles disable Native Image colors with version-appropriate arguments. §FS-native-invocation.3
190+
@Issue("https://github.com/graalvm/native-build-tools/issues/366")
191+
def "disables colorful Native Image output for Gradle's plain console with JDK #nativeImageVersion"() {
192+
given:
193+
def project = newProject()
194+
project.plugins.apply(ApplicationPlugin)
195+
project.plugins.apply(NativeImagePlugin)
196+
def options = project.extensions.getByType(GraalVMExtension).binaries.getByName("main")
197+
options.richOutput.set(true)
198+
options.excludeConfigArgs.set([])
199+
options.configurationFileDirectories.setFrom([])
200+
201+
when:
202+
def args = new NativeImageCommandLineProvider(
203+
project.provider { options },
204+
project.provider { "main" },
205+
project.provider { testDirectory.toString() },
206+
project.provider { testDirectory.toString() },
207+
project.objects.fileProperty(),
208+
project.provider { false },
209+
project.provider { nativeImageVersion },
210+
project.provider { true }
211+
).asArguments()
212+
213+
then:
214+
args.contains(disabledColorArgument)
215+
!args.contains(NativeImageFlags.BUILD_OUTPUT_COLORFUL)
216+
217+
where:
218+
nativeImageVersion | disabledColorArgument
219+
17 | NativeImageFlags.BUILD_OUTPUT_COLORLESS
220+
21 | "--color=never"
221+
}
222+
223+
// Rich Gradle output enables Native Image colors with version-appropriate arguments. §FS-native-invocation.3
224+
@Issue("https://github.com/graalvm/native-build-tools/issues/366")
225+
def "uses the rich-output setting when Gradle enables colors with JDK #nativeImageVersion"() {
226+
given:
227+
def project = newProject()
228+
project.plugins.apply(ApplicationPlugin)
229+
project.plugins.apply(NativeImagePlugin)
230+
def options = project.extensions.getByType(GraalVMExtension).binaries.getByName("main")
231+
options.richOutput.set(true)
232+
options.excludeConfigArgs.set([])
233+
options.configurationFileDirectories.setFrom([])
234+
235+
when:
236+
def args = new NativeImageCommandLineProvider(
237+
project.provider { options },
238+
project.provider { "main" },
239+
project.provider { testDirectory.toString() },
240+
project.provider { testDirectory.toString() },
241+
project.objects.fileProperty(),
242+
project.provider { false },
243+
project.provider { nativeImageVersion },
244+
project.provider { false }
245+
).asArguments()
246+
247+
then:
248+
args.contains(enabledColorArgument)
249+
!args.contains(NativeImageFlags.BUILD_OUTPUT_COLORLESS)
250+
251+
where:
252+
nativeImageVersion | enabledColorArgument
253+
17 | NativeImageFlags.BUILD_OUTPUT_COLORFUL
254+
21 | "--color=always"
255+
}
189256
}

native-maven-plugin/docs/functional/native-image-builds.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,10 @@ mvn -Pnative -DquickBuild native:test
6666
mvn -Pnative native:write-args-file
6767
mvn -Pnative native:list-libraries-missing-metadata
6868
```
69+
70+
## 9. Console colors
71+
72+
The native-image invocation must follow Maven's detected console color state. It must use
73+
`--color=always` or `--color=never` on JDK 21 and later, and `-H:+BuildOutputColorful` or
74+
`-H:-BuildOutputColorful` on older versions. Explicit user build arguments come later and may
75+
override this detected default, adapting [§root/FS-native-builds.2](../../../docs/spec/functional/native-image-builds.md#2-command-line-construction).

native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/JavaApplicationFunctionalTest.groovy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,20 @@
4141

4242
package org.graalvm.buildtools.maven
4343

44+
import org.graalvm.buildtools.utils.NativeImageUtils
4445
import spock.lang.Issue
4546

4647
import static org.graalvm.buildtools.utils.SharedConstants.NATIVE_IMAGE_EXE;
4748

4849
class JavaApplicationFunctionalTest extends AbstractGraalVMMavenFunctionalTest {
50+
// Maven's disabled console colors propagate to Native Image. §FS-native-builds.9, §E2E-functional-tests.3.1.
4951
def "proper options are added to the native-image invocation"() {
5052
withSample("java-application")
5153

5254
when:
5355
mvn '-Pnative', '-DskipTests', '-DnativeDryRun', '-DuseArgFile=false',
5456
'-Dclasspath=/', '-Ddebug', '-Dfallback=false', '-Dverbose', '-DsharedLibrary',
57+
'-Dstyle.color=never',
5558
'-DquickBuild',
5659
'package'
5760

@@ -60,6 +63,8 @@ class JavaApplicationFunctionalTest extends AbstractGraalVMMavenFunctionalTest {
6063
outputContains NATIVE_IMAGE_EXE
6164
outputContains "-cp " // actual path is OS-specific (/ vs C:\)
6265
outputContains "-g --no-fallback --verbose --shared -Ob"
66+
def majorVersion = NativeImageUtils.getMajorJDKVersion(GraalVMSupport.getGraalVMHomeVersionString())
67+
outputContains(majorVersion >= 21 ? "--color=never" : "-H:-BuildOutputColorful")
6368
}
6469

6570
def "can build and execute a native image with the Maven plugin"() {

native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/AbstractNativeImageMojo.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.apache.maven.plugins.annotations.Parameter;
5252
import org.apache.maven.project.MavenProject;
5353
import org.apache.maven.toolchain.ToolchainManager;
54+
import org.apache.maven.shared.utils.logging.MessageUtils;
5455
import org.codehaus.plexus.logging.Logger;
5556
import org.graalvm.buildtools.maven.config.ExcludeConfigConfiguration;
5657
import org.graalvm.buildtools.model.resources.NativeImageFlags;
@@ -246,6 +247,8 @@ protected List<String> getBuildArgs() throws MojoExecutionException {
246247
cliArgs.add("-Ob");
247248
}
248249

250+
addConsoleColorArgument(cliArgs);
251+
249252
cliArgs.add("-o");
250253
cliArgs.add(outputDirectory.toPath().toAbsolutePath() + File.separator + imageName);
251254

@@ -297,6 +300,26 @@ protected List<String> getBuildArgs() throws MojoExecutionException {
297300
return Collections.unmodifiableList(actualCliArgs);
298301
}
299302

303+
// Maven's detected console mode controls version-appropriate Native Image color flags. §FS-native-builds.9.
304+
private void addConsoleColorArgument(List<String> cliArgs) throws MojoExecutionException {
305+
boolean colorEnabled = isColorEnabled();
306+
if (getNativeImageMajorVersion() >= 21) {
307+
cliArgs.add(NativeImageFlags.COLOR + (colorEnabled ? "=always" : "=never"));
308+
} else {
309+
cliArgs.add(colorEnabled ? NativeImageFlags.BUILD_OUTPUT_COLORFUL : NativeImageFlags.BUILD_OUTPUT_COLORLESS);
310+
}
311+
}
312+
313+
protected boolean isColorEnabled() {
314+
return MessageUtils.isColorEnabled();
315+
}
316+
317+
protected int getNativeImageMajorVersion() throws MojoExecutionException {
318+
Path executable = NativeImageConfigurationUtils.getNativeImageSupportingToolchain(
319+
logger, toolchainManager, session, enforceToolchain);
320+
return NativeImageUtils.getMajorJDKVersion(getVersionInformation(logger, executable));
321+
}
322+
300323
static List<String> processBuildArgs(List<String> buildArgs) {
301324
var result = new ArrayList<String>();
302325
for (String buildArg : buildArgs) {
@@ -578,7 +601,14 @@ protected static String getVersionInformation(Logger logger) throws MojoExecutio
578601
return nativeImageVersionInformation;
579602
}
580603

581-
Path nativeImageExecutable = NativeImageConfigurationUtils.getNativeImage(logger);
604+
return getVersionInformation(logger, NativeImageConfigurationUtils.getNativeImage(logger));
605+
}
606+
607+
private static String getVersionInformation(Logger logger, Path nativeImageExecutable) throws MojoExecutionException {
608+
if (nativeImageVersionInformation != null) {
609+
return nativeImageVersionInformation;
610+
}
611+
582612
try {
583613
ProcessBuilder processBuilder = new ProcessBuilder(nativeImageExecutable.toString());
584614
processBuilder.command().add("--version");

0 commit comments

Comments
 (0)