Skip to content

Commit 2f71c6d

Browse files
committed
Tell the native image platform without palantir's library
The Gradle plugin used com.palantir.gradle.utils:platform, at run time in the builds it is applied to, to decide whether a native image exists for the machine and which classifier to resolve. NativePlatform now does that itself: os.name, os.arch and, on Linux, ldd --version to tell glibc from musl, read through Gradle's ProviderFactory as before, with the names open-java-format-native publishes its images under and buildSrc's open-java-format.native-platform gives the ones the build produces. The published platforms are listed exactly: the five classifiers on Maven Central. Before, any glibc Linux and any macOS counted, so a 32-bit x86 JVM went on to look for an image that does not exist. NativePlatformTest pins those classifiers and extensions, the spellings of os.arch, glibc against musl, ldd running only on Linux, and the refusal of platforms it cannot name. One test asks Gradle for the platform of the JVM running it, which on CI's Linux jobs runs the real ldd. The plugin no longer depends on platform, exec, providers and safe-exec-commandline.
1 parent aa2c4b3 commit 2f71c6d

6 files changed

Lines changed: 291 additions & 41 deletions

File tree

‎gradle-open-java-format/build.gradle‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ dependencies {
2323
implementation libs.guava
2424
implementation project(':open-java-format-spi')
2525
implementation project(':open-java-format-jdk-bootstrap')
26-
implementation libs.palantir.platform
2726
implementation libs.ideaConfiguration.gradlePlugin
2827

2928
testImplementation project(':open-java-format')

‎gradle-open-java-format/src/main/java/com/palantir/javaformat/gradle/NativeImageFormatProviderPlugin.java‎

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,18 @@
1717
package com.palantir.javaformat.gradle;
1818

1919
import com.google.common.base.Preconditions;
20-
import com.palantir.platform.Architecture;
21-
import com.palantir.platform.GradleOperatingSystem;
22-
import com.palantir.platform.OperatingSystem;
2320
import java.util.Collections;
21+
import javax.inject.Inject;
2422
import org.gradle.api.Plugin;
2523
import org.gradle.api.Project;
2624
import org.gradle.api.artifacts.type.ArtifactTypeDefinition;
2725
import org.gradle.api.provider.Provider;
28-
import org.gradle.api.tasks.Nested;
26+
import org.gradle.api.provider.ProviderFactory;
2927

3028
public abstract class NativeImageFormatProviderPlugin implements Plugin<Project> {
3129

32-
@Nested
33-
protected abstract GradleOperatingSystem getOs();
30+
@Inject
31+
protected abstract ProviderFactory getProviderFactory();
3432

3533
static final String NATIVE_CONFIGURATION_NAME = "palantirJavaFormatNative";
3634

@@ -40,38 +38,27 @@ public void apply(Project rootProject) {
4038
rootProject == rootProject.getRootProject(),
4139
"May only apply dev.openjavaformat.java-format-provider to the root project");
4240

43-
Provider<OperatingSystem> operatingSystem = getOs().getOperatingSystem();
41+
Provider<NativePlatform> platform = NativePlatform.current(getProviderFactory());
4442
String implementationVersion = JavaFormatExtension.class.getPackage().getImplementationVersion();
4543
rootProject.getConfigurations().register(NATIVE_CONFIGURATION_NAME, conf -> {
4644
conf.setDescription("Internal configuration for resolving the open-java-format native image");
4745
conf.setCanBeConsumed(false);
4846
conf.setCanBeResolved(true);
4947
conf.defaultDependencies(deps -> {
50-
deps.addAllLater(operatingSystem.map(os -> Collections.singletonList(rootProject
48+
deps.addAllLater(platform.map(p -> Collections.singletonList(rootProject
5149
.getDependencies()
5250
.create(String.format(
53-
"dev.openjavaformat:open-java-format-native:%s:nativeImage-%s_%s@%s",
54-
implementationVersion,
55-
os.uiName(),
56-
Architecture.get().uiName(),
57-
getExtension(os))))));
51+
"dev.openjavaformat:open-java-format-native:%s:%s@%s",
52+
implementationVersion, p.classifier(), p.extension())))));
5853
});
5954
conf.getAttributes().attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "executable-nativeImage");
6055
});
6156
rootProject.getDependencies().registerTransform(ExecutableTransform.class, transformSpec -> {
6257
transformSpec
6358
.getFrom()
6459
.attributeProvider(
65-
ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE,
66-
operatingSystem.map(NativeImageFormatProviderPlugin::getExtension));
60+
ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, platform.map(NativePlatform::extension));
6761
transformSpec.getTo().attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "executable-nativeImage");
6862
});
6963
}
70-
71-
static String getExtension(OperatingSystem operatingSystem) {
72-
if (operatingSystem.equals(OperatingSystem.WINDOWS)) {
73-
return "exe";
74-
}
75-
return "bin";
76-
}
7764
}

‎gradle-open-java-format/src/main/java/com/palantir/javaformat/gradle/NativeImageSupport.java‎

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,36 +16,22 @@
1616

1717
package com.palantir.javaformat.gradle;
1818

19-
import com.palantir.platform.Architecture;
20-
import com.palantir.platform.GradleOperatingSystem;
21-
import com.palantir.platform.OperatingSystem;
2219
import javax.inject.Inject;
2320
import org.gradle.api.provider.ProviderFactory;
24-
import org.gradle.api.tasks.Nested;
2521

2622
public abstract class NativeImageSupport {
2723

28-
@Nested
29-
protected abstract GradleOperatingSystem getOs();
30-
3124
@Inject
3225
protected abstract ProviderFactory getProviderFactory();
3326

3427
public boolean isNativeImageConfigured() {
3528
return isNativeFlagEnabled() && isNativeImageSupported();
3629
}
3730

38-
/**
39-
* The platforms a native image is published for, and therefore the only ones where it can be
40-
* resolved. macOS is supported on both architectures: the x86-64 image used to be excluded
41-
* because nobody built it, and .github/workflows/ci.yml now does. Windows has an image for
42-
* x86-64 only, and musl none: no job produces them.
43-
*/
31+
/** Whether this platform has a published native image; see NativePlatform's PUBLISHED. */
4432
private boolean isNativeImageSupported() {
45-
return getOs().getOperatingSystem()
46-
.map(os -> os.equals(OperatingSystem.LINUX_GLIBC)
47-
|| os.equals(OperatingSystem.MACOS)
48-
|| (os.equals(OperatingSystem.WINDOWS) && Architecture.get() == Architecture.X86_64))
33+
return NativePlatform.current(getProviderFactory())
34+
.map(NativePlatform::isPublished)
4935
.get();
5036
}
5137

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* (c) Copyright 2026 Palantir Technologies Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.palantir.javaformat.gradle;
18+
19+
import java.util.Locale;
20+
import java.util.Set;
21+
import java.util.function.Supplier;
22+
import org.gradle.api.provider.Provider;
23+
import org.gradle.api.provider.ProviderFactory;
24+
import org.gradle.process.ExecOutput;
25+
26+
/**
27+
* A platform in the names open-java-format-native publishes its images under: the classifier
28+
* {@code nativeImage-<os>_<arch>}. buildSrc's open-java-format.native-platform names the images the build
29+
* produces the same way, and NativePlatformTest pins the names of the published ones.
30+
*/
31+
record NativePlatform(OperatingSystem operatingSystem, Architecture architecture) {
32+
33+
enum OperatingSystem {
34+
MACOS,
35+
LINUX_GLIBC,
36+
LINUX_MUSL,
37+
WINDOWS
38+
}
39+
40+
enum Architecture {
41+
X86,
42+
X86_64,
43+
AARCH64
44+
}
45+
46+
/**
47+
* The platforms a native image is published for, and therefore the only ones where it can be resolved.
48+
* macOS has an image for both architectures: the x86-64 one used to be missing because nobody built it,
49+
* and .github/workflows/ci.yml now does. Windows has one for x86-64 only, and musl none: no job produces
50+
* them.
51+
*/
52+
private static final Set<NativePlatform> PUBLISHED = Set.of(
53+
new NativePlatform(OperatingSystem.LINUX_GLIBC, Architecture.X86_64),
54+
new NativePlatform(OperatingSystem.LINUX_GLIBC, Architecture.AARCH64),
55+
new NativePlatform(OperatingSystem.MACOS, Architecture.X86_64),
56+
new NativePlatform(OperatingSystem.MACOS, Architecture.AARCH64),
57+
new NativePlatform(OperatingSystem.WINDOWS, Architecture.X86_64));
58+
59+
/** The platform of the JVM that runs Gradle. */
60+
static Provider<NativePlatform> current(ProviderFactory providers) {
61+
ExecOutput ldd = providers.exec(spec -> {
62+
spec.commandLine("ldd", "--version");
63+
// musl's ldd prints its version to stderr and exits with 1.
64+
spec.setIgnoreExitValue(true);
65+
});
66+
Supplier<String> lddVersion = () -> ldd.getStandardOutput().getAsText().get()
67+
+ ldd.getStandardError().getAsText().get();
68+
return providers
69+
.systemProperty("os.name")
70+
.zip(providers.systemProperty("os.arch"), (osName, osArch) -> of(osName, osArch, lddVersion));
71+
}
72+
73+
/** The platform for the given os.name and os.arch; lddVersion is asked only on Linux. */
74+
static NativePlatform of(String osName, String osArch, Supplier<String> lddVersion) {
75+
return new NativePlatform(operatingSystem(osName, lddVersion), architecture(osArch));
76+
}
77+
78+
boolean isPublished() {
79+
return PUBLISHED.contains(this);
80+
}
81+
82+
String classifier() {
83+
return "nativeImage-" + uiName(operatingSystem) + "_" + uiName(architecture);
84+
}
85+
86+
/** native-image names the Windows binary .exe, and the image is published with that extension. */
87+
String extension() {
88+
return operatingSystem == OperatingSystem.WINDOWS ? "exe" : "bin";
89+
}
90+
91+
private static OperatingSystem operatingSystem(String osName, Supplier<String> lddVersion) {
92+
String name = osName.toLowerCase(Locale.ROOT);
93+
if (name.startsWith("mac")) {
94+
return OperatingSystem.MACOS;
95+
}
96+
if (name.startsWith("windows")) {
97+
return OperatingSystem.WINDOWS;
98+
}
99+
if (name.startsWith("linux")) {
100+
// A glibc binary does not run on musl, and ldd is what tells the two apart.
101+
String libc = lddVersion.get().toLowerCase(Locale.ROOT);
102+
if (libc.contains("glibc") || libc.contains("gnu libc")) {
103+
return OperatingSystem.LINUX_GLIBC;
104+
}
105+
if (libc.contains("musl")) {
106+
return OperatingSystem.LINUX_MUSL;
107+
}
108+
throw new UnsupportedOperationException("Cannot tell glibc from musl: ldd --version printed " + libc);
109+
}
110+
throw new UnsupportedOperationException("No native image name for the operating system " + osName);
111+
}
112+
113+
private static Architecture architecture(String osArch) {
114+
return switch (osArch.toLowerCase(Locale.ROOT)) {
115+
case "x86_64", "x64", "amd64" -> Architecture.X86_64;
116+
case "arm", "arm64", "aarch64" -> Architecture.AARCH64;
117+
case "x86", "i686" -> Architecture.X86;
118+
default -> throw new UnsupportedOperationException("No native image name for the architecture " + osArch);
119+
};
120+
}
121+
122+
private static String uiName(Enum<?> value) {
123+
return value.name().toLowerCase(Locale.ROOT).replace('_', '-');
124+
}
125+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* (c) Copyright 2026 Palantir Technologies Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.palantir.javaformat.gradle;
18+
19+
import static java.nio.charset.StandardCharsets.UTF_8;
20+
import static org.assertj.core.api.Assertions.assertThat;
21+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
22+
23+
import com.palantir.javaformat.gradle.NativePlatform.Architecture;
24+
import com.palantir.javaformat.gradle.NativePlatform.OperatingSystem;
25+
import java.io.IOException;
26+
import java.io.InputStream;
27+
import java.io.UncheckedIOException;
28+
import java.nio.file.Path;
29+
import java.util.function.Supplier;
30+
import org.gradle.api.Project;
31+
import org.gradle.testfixtures.ProjectBuilder;
32+
import org.junit.jupiter.api.Test;
33+
import org.junit.jupiter.api.io.TempDir;
34+
import org.junit.jupiter.params.ParameterizedTest;
35+
import org.junit.jupiter.params.provider.CsvSource;
36+
37+
final class NativePlatformTest {
38+
39+
private static final String GLIBC = "ldd (Ubuntu GLIBC 2.39-0ubuntu8) 2.39";
40+
41+
// The classifiers open-java-format-native has on Maven Central, and the names buildSrc's
42+
// open-java-format.native-platform gives the images it builds.
43+
@CsvSource(
44+
delimiter = '|',
45+
value = {
46+
"Linux | aarch64 | ldd (GNU libc) 2.41 | nativeImage-linux-glibc_aarch64 | bin",
47+
"Linux | amd64 | ldd (Ubuntu GLIBC 2.39-0ubuntu8) 2.39 | nativeImage-linux-glibc_x86-64 | bin",
48+
"Mac OS X | aarch64 | | nativeImage-macos_aarch64 | bin",
49+
"Mac OS X | x86_64 | | nativeImage-macos_x86-64 | bin",
50+
"Windows 11 | amd64 | | nativeImage-windows_x86-64 | exe"
51+
})
52+
@ParameterizedTest
53+
void names_each_published_image(
54+
String osName, String osArch, String lddVersion, String classifier, String extension) {
55+
NativePlatform platform = NativePlatform.of(osName, osArch, lddOnLinuxOnly(lddVersion));
56+
57+
assertThat(platform.isPublished()).isTrue();
58+
assertThat(platform.classifier()).isEqualTo(classifier);
59+
assertThat(platform.extension()).isEqualTo(extension);
60+
}
61+
62+
@CsvSource(
63+
delimiter = '|',
64+
value = {
65+
"Linux | i686 | ldd (Ubuntu GLIBC 2.39-0ubuntu8) 2.39 | nativeImage-linux-glibc_x86",
66+
"Linux | x86_64 | musl libc (x86_64) | nativeImage-linux-musl_x86-64",
67+
"Windows 11 | aarch64 | | nativeImage-windows_aarch64"
68+
})
69+
@ParameterizedTest
70+
void knows_there_is_no_image_for_other_platforms(
71+
String osName, String osArch, String lddVersion, String classifier) {
72+
NativePlatform platform = NativePlatform.of(osName, osArch, lddOnLinuxOnly(lddVersion));
73+
74+
assertThat(platform.isPublished()).isFalse();
75+
assertThat(platform.classifier()).isEqualTo(classifier);
76+
}
77+
78+
@Test
79+
void tells_glibc_from_musl_with_ldd() {
80+
assertThat(NativePlatform.of("Linux", "amd64", () -> GLIBC).operatingSystem())
81+
.isEqualTo(OperatingSystem.LINUX_GLIBC);
82+
assertThat(NativePlatform.of("Linux", "amd64", () -> "ldd (GNU libc) 2.41")
83+
.operatingSystem())
84+
.isEqualTo(OperatingSystem.LINUX_GLIBC);
85+
// musl's ldd prints its version to stderr, after an empty stdout.
86+
assertThat(NativePlatform.of("Linux", "amd64", () -> "musl libc (x86_64)\nVersion 1.2.5\n")
87+
.operatingSystem())
88+
.isEqualTo(OperatingSystem.LINUX_MUSL);
89+
}
90+
91+
@CsvSource({
92+
"aarch64, AARCH64",
93+
"AMD64, X86_64",
94+
"amd64, X86_64",
95+
"arm, AARCH64",
96+
"arm64, AARCH64",
97+
"i686, X86",
98+
"x64, X86_64",
99+
"x86, X86",
100+
"x86_64, X86_64"
101+
})
102+
@ParameterizedTest
103+
void reads_every_spelling_of_os_arch(String osArch, Architecture architecture) {
104+
assertThat(NativePlatform.of("Mac OS X", osArch, NativePlatformTest::noLdd)
105+
.architecture())
106+
.isEqualTo(architecture);
107+
}
108+
109+
@Test
110+
void refuses_what_it_cannot_name() {
111+
assertThatThrownBy(() -> NativePlatform.of("FreeBSD", "amd64", NativePlatformTest::noLdd))
112+
.isInstanceOf(UnsupportedOperationException.class)
113+
.hasMessageContaining("FreeBSD");
114+
assertThatThrownBy(() -> NativePlatform.of("Linux", "riscv64", () -> GLIBC))
115+
.isInstanceOf(UnsupportedOperationException.class)
116+
.hasMessageContaining("riscv64");
117+
assertThatThrownBy(() -> NativePlatform.of("Linux", "amd64", () -> "ldd: unrecognized option '--version'"))
118+
.isInstanceOf(UnsupportedOperationException.class)
119+
.hasMessageContaining("Cannot tell glibc from musl");
120+
}
121+
122+
// On Linux this runs the real ldd through Gradle's ProviderFactory, which CI's ubuntu jobs exercise.
123+
@Test
124+
void reads_the_platform_of_the_jvm_running_gradle(@TempDir Path projectDir) {
125+
Project project =
126+
ProjectBuilder.builder().withProjectDir(projectDir.toFile()).build();
127+
128+
assertThat(NativePlatform.current(project.getProviders()).get())
129+
.isEqualTo(NativePlatform.of(
130+
System.getProperty("os.name"), System.getProperty("os.arch"), NativePlatformTest::runLdd));
131+
}
132+
133+
/** The table's ldd output, or for an empty cell, an ldd that must not run: the platform is not Linux. */
134+
private static Supplier<String> lddOnLinuxOnly(String lddVersion) {
135+
return lddVersion == null ? NativePlatformTest::noLdd : () -> lddVersion;
136+
}
137+
138+
private static String noLdd() {
139+
throw new AssertionError("ldd must only run on Linux");
140+
}
141+
142+
private static String runLdd() {
143+
try {
144+
Process process = new ProcessBuilder("ldd", "--version")
145+
.redirectErrorStream(true)
146+
.start();
147+
try (InputStream output = process.getInputStream()) {
148+
return new String(output.readAllBytes(), UTF_8);
149+
}
150+
} catch (IOException e) {
151+
throw new UncheckedIOException(e);
152+
}
153+
}
154+
}

0 commit comments

Comments
 (0)