Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2bc324c
feat: `ktfmt` binary via `native-image`
sgammon Nov 15, 2025
c837e48
chore: pr cleanup
sgammon Nov 17, 2025
51c9db0
chore: ci support for native image
sgammon Nov 17, 2025
10b1a39
fix: duplicative identical ci hooks on `push` and `pull_request`
sgammon Nov 17, 2025
3cb9885
formatting
ZacSweers May 24, 2026
ceef9bf
Fix CI location + simple JDK
ZacSweers May 24, 2026
be8e012
Keep native build a separate job
ZacSweers May 24, 2026
1d962c2
Regenerate from latest kotlinc
ZacSweers May 24, 2026
e95e401
Gradle cleanup + fix args not getting propagated
ZacSweers May 24, 2026
2713a16
Better arch defaults
ZacSweers May 24, 2026
3f23b67
Extract graalvm build-logic
ZacSweers May 24, 2026
21cfbc9
Convert KotlinCoreEnvironmentCompanion to kotlin
ZacSweers May 24, 2026
880b120
Add a CI smoke test
ZacSweers May 24, 2026
4cda03b
Docs
ZacSweers May 24, 2026
a885471
Extract reusable script
ZacSweers May 24, 2026
9ad6c71
Add CI publishing
ZacSweers May 24, 2026
4daaca6
Ignore metadata json in PR diffs as they're generated
ZacSweers May 24, 2026
9a4f133
CI cleanup
ZacSweers May 24, 2026
61847b1
Add staleness checks to native_smoke_test.sh
ZacSweers May 24, 2026
262ff76
Docs and mkdir
ZacSweers May 24, 2026
34b2ede
Add docs for future updates and posterity
ZacSweers May 24, 2026
e36bdb0
Add regenerate_native_metadata.sh
ZacSweers May 24, 2026
9c742fb
Remove unnecessary application class
ZacSweers May 24, 2026
65f4135
Remove now-unnecessary proxy-config
ZacSweers May 25, 2026
c8163e0
Align args with GJF + fix CI
ZacSweers May 25, 2026
67a8038
Make ktfmtFile generation windows-friendly
ZacSweers May 25, 2026
293aa2e
More windows fixes
ZacSweers May 25, 2026
f20dcba
windows (??)
ZacSweers May 25, 2026
e2a88c8
Another windows attempt
ZacSweers May 25, 2026
42a3ea9
formatting
ZacSweers May 28, 2026
c4e2556
Apparently this is the correct formatting
ZacSweers May 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# GraalVM native-image reachability metadata is generated by the tracing agent (see core/README.md
# and regenerate_native_metadata.sh). Mark it generated so code review collapses the diff and it's
# excluded from language stats.
core/src/main/native-image/resources/**/*.json linguist-generated=true
55 changes: 55 additions & 0 deletions .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ on:
push:
paths-ignore:
- '**.md'
branches:
- main
pull_request:
paths-ignore:
- '**.md'
Expand Down Expand Up @@ -31,3 +33,56 @@ jobs:
run: ./gradlew :lambda:build --stacktrace --no-daemon
- name: Build ktfmt
run: ./gradlew :ktfmt:build --stacktrace --no-daemon

native:
# The linux build is blocking so stale GraalVM reachability metadata (which is OS-independent)
# fails CI. macOS and Windows stay non-blocking, since cross-platform toolchain hiccups
# shouldn't fail the run. Release binaries are built separately (publish_artifacts_on_release.yaml).
# Linux uses the oldest available ubuntu for broader glibc compatibility.
name: Native Image on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.os != 'ubuntu-22.04' }}
# On Windows runners the workspace is on D: but java.io.tmpdir defaults to C:. native-build-tools
# writes the native-image @argfile under java.io.tmpdir, then relativizes it against the build's
# working directory (on D:), which fails with "'other' has different root". Point temp at the
# workspace drive so both share a root. Empty (no-op) on other OSes.
env:
TMP: ${{ matrix.os == 'windows-latest' && format('{0}\tmp', github.workspace) || '' }}
TEMP: ${{ matrix.os == 'windows-latest' && format('{0}\tmp', github.workspace) || '' }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Create temp dir on workspace drive (Windows)
if: matrix.os == 'windows-latest'
run: New-Item -ItemType Directory -Force -Path "${{ github.workspace }}\tmp"

# Gradle runs on the project's JDK (17); native-image is provided separately via GRAALVM_HOME
# (set-java-home: false keeps JAVA_HOME pointed at the JDK above).
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: 17
distribution: zulu

- name: Set up GraalVM
uses: graalvm/setup-graalvm@v1
with:
java-version: '25'
distribution: 'graalvm-community'
github-token: ${{ secrets.GITHUB_TOKEN }}
set-java-home: 'false'
native-image-job-reports: 'true'

- name: Build
shell: bash
run: ./gradlew :ktfmt:nativeCompile --stacktrace --no-daemon

- name: Smoke test
shell: bash
run: ./native_smoke_test.sh
70 changes: 70 additions & 0 deletions .github/workflows/publish_artifacts_on_release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,76 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# Build the GraalVM native binary per platform and attach it to the GitHub Release. Modeled on
# google-java-format's release workflow. Uses GraalVM Community (Serial GC only, which matches the
# build's default) so there are no Oracle GraalVM licensing considerations.
upload_native_binaries:
name: Native binary (${{ matrix.os }})
runs-on: ${{ matrix.os }}
permissions:
contents: write # Required to upload release assets
strategy:
fail-fast: false
matrix:
# Build linux on the oldest available ubuntu for broader glibc compatibility. Keep the keys
# of the SUFFIX map below in sync with this list.
os: [ubuntu-22.04, ubuntu-22.04-arm, macos-latest, windows-latest]
env:
SUFFIX: ${{ fromJson('{"ubuntu-22.04":"linux-x86-64","ubuntu-22.04-arm":"linux-arm64","macos-latest":"darwin-arm64","windows-latest":"windows-x86-64"}')[matrix.os] }}
EXTENSION: ${{ matrix.os == 'windows-latest' && '.exe' || '' }}
# On Windows the workspace is on D: but java.io.tmpdir defaults to C:. native-build-tools writes
# the native-image @argfile under java.io.tmpdir, then relativizes it against the build's working
# directory (on D:), which fails with "'other' has different root". Point temp at the workspace
# drive so both share a root. Empty (no-op) on other OSes.
TMP: ${{ matrix.os == 'windows-latest' && format('{0}\tmp', github.workspace) || '' }}
TEMP: ${{ matrix.os == 'windows-latest' && format('{0}\tmp', github.workspace) || '' }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.events.release.tag_name || inputs.release_tag || github.ref }}

- name: Create temp dir on workspace drive (Windows)
if: matrix.os == 'windows-latest'
run: New-Item -ItemType Directory -Force -Path "${{ github.workspace }}\tmp"

# Gradle runs on JDK 17; native-image is provided via GRAALVM_HOME (set-java-home: false).
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: 17
distribution: zulu

- name: Set up GraalVM
uses: graalvm/setup-graalvm@v1
with:
java-version: '25'
distribution: 'graalvm-community'
github-token: ${{ secrets.GITHUB_TOKEN }}
set-java-home: 'false'
native-image-job-reports: 'true'

- name: Build native binary
shell: bash
run: ./gradlew :ktfmt:nativeCompile -Pktfmt.native.release=true --stacktrace --no-daemon

- name: Smoke test
shell: bash
run: ./native_smoke_test.sh

- name: Stage binary
shell: bash
run: |
mkdir -p release
cp "core/build/native/nativeCompile/ktfmt${EXTENSION}" "release/ktfmt_${SUFFIX}${EXTENSION}"

- name: Upload native binary to GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.events.release.tag_name || inputs.release_tag }}
files: release/ktfmt_${{ env.SUFFIX }}${{ env.EXTENSION }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

deploy_website:
runs-on: ubuntu-latest
steps:
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ release-ktfmt-website/
**/build/
!src/**/build/
.claude/

# Native Image profiles are large
core/src/main/native-image/profiles/*.iprof

1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
- Remove forced breaking of `context` function types (https://github.com/facebook/ktfmt/pull/613)
- Preserve user-authored line breaks inside lambda bodies by default, rather than have ktfmt impose anything. This can be particularly useful for DSL syntax like Compose UI or Kotlin Gradle script. The behavior follows the `FormattingOptions.preserveLambdaBreaks` setting of the chosen style. (https://github.com/facebook/ktfmt/pull/614)
- Add a `FormattingOptions.Builder` API for tools to avoid breaking ABI changes with new options. (https://github.com/facebook/ktfmt/pull/614)
- Add standalone native binaries built with GraalVM `native-image`. These have identical CLI behavior to the JVM version but with near-instant startup and no JVM required (https://github.com/facebook/ktfmt/issues/441)

## [0.62]
### Added
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,32 @@ val state = remember { mutableStateOf(false) }
or limited `.editorconfig` support). This is a deliberate design decision to unify our code formatting on a
single format.*

### Native binary

`ktfmt` is also available as a standalone native binary, compiled ahead-of-time with
[GraalVM `native-image`](https://www.graalvm.org/latest/reference-manual/native-image/). It behaves
exactly like the JVM CLI (same options) but starts near-instantly and needs no JVM installed, which
is especially handy for pre-commit hooks and formatting only changed files. Pre-built binaries for
each supported platform are attached to the [releases page](https://github.com/facebook/ktfmt/releases):

```
$ ktfmt [--kotlinlang-style | --google-style] [files...]
```

#### Building the native binary from source

Building requires a [GraalVM](https://www.graalvm.org/downloads/) JDK (one that includes
`native-image`) discoverable via `GRAALVM_HOME` or `PATH`:

```
$ ./gradlew :ktfmt:nativeCompile
$ ./core/build/native/nativeCompile/ktfmt --version
```

The build defaults to the Serial GC and a broadly-compatible `-march` target. Several knobs are
exposed via `-Pktfmt.native.*` properties (GC, target architecture, PGO, static linking, …); see
[`build-logic/src/main/kotlin/ktfmt.native-image.gradle.kts`](build-logic/src/main/kotlin/ktfmt.native-image.gradle.kts).

### using Gradle

A [Gradle plugin (ktfmt-gradle)](https://github.com/cortinico/ktfmt-gradle) is available on the
Expand Down
30 changes: 30 additions & 0 deletions build-logic/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

plugins { `kotlin-dsl` }

repositories {
gradlePluginPortal()
mavenCentral()
}

dependencies {
// Makes the GraalVM Native Build Tools plugin (and its `graalvmNative` DSL) available to the
// convention plugins defined in this build. Version is read from the shared catalog.
implementation(
libs.plugins.graalvm.get().run { "$pluginId:$pluginId.gradle.plugin:$version" }
)
}
33 changes: 33 additions & 0 deletions build-logic/settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

dependencyResolutionManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
// The shared catalog declares a `ktfmt` library whose version is injected at runtime by the
// root build's settings. Supply a placeholder so the catalog validates when consumed here;
// the `ktfmt` library itself is never used by build-logic.
version("ktfmt", "0.0.0")
}
}
}

rootProject.name = "build-logic"
Loading