Skip to content

feat: introduce cli for invoking spotless from shell without any build tool #2419

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 21 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[![Gradle Plugin](https://img.shields.io/gradle-plugin-portal/v/com.diffplug.spotless?color=blue&label=gradle%20plugin)](plugin-gradle)
[![Maven Plugin](https://img.shields.io/maven-central/v/com.diffplug.spotless/spotless-maven-plugin?color=blue&label=maven%20plugin)](plugin-maven)
[![SBT Plugin](https://img.shields.io/badge/sbt%20plugin-0.1.3-blue)](https://github.com/moznion/sbt-spotless)
[![CLI](https://img.shields.io/badge/cli-0.0.1-blue)](cli)

Spotless can format <antlr | c | c# | c++ | css | flow | graphql | groovy | html | java | javascript | json | jsx | kotlin | less | license headers | markdown | objective-c | protobuf | python | scala | scss | shell | sql | typeScript | vue | yaml | anything> using <gradle | maven | sbt | anything>.

Expand Down Expand Up @@ -41,6 +42,13 @@ user@machine repo % mvn spotless:check
```

## [❇️ Spotless for SBT (external for now)](https://github.com/moznion/sbt-spotless)

## [❇️ Spotless Command Line Interface (CLI)](cli)

```console
user@machine repo % spotless --target '**/src/**/*.java' license-header --header='/* Myself $YEAR */' google-java-format
```

## [Other build systems](CONTRIBUTING.md#how-to-add-a-new-plugin-for-a-build-system)

## How it works (for potential contributors)
Expand Down
8 changes: 8 additions & 0 deletions cli/CHANGES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# spotless-cli releases

We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format

## [Unreleased]

## [0.0.1] - 2024-11-06
Anchor version number.
25 changes: 25 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# <img align="left" src="../_images/spotless_logo.png"> Spotless Command Line Interface CLI
*Keep your code Spotless with Gradle*

<!---freshmark shields
output = [
link(shield('Changelog', 'changelog', '{{versionLast}}', 'blue'), 'CHANGES.md'),
'',
link(shield('OS Win', 'OS', 'Windows', 'blueviolet'), 'README.md'),
link(shield('OS Linux', 'OS', 'Linux', 'blueviolet'), 'README.md'),
link(shield('OS macOS', 'OS', 'macOS', 'blueviolet'), 'README.md'),
].join('\n');
-->
[![Changelog](https://img.shields.io/badge/changelog-0.0.1-blue.svg)](CHANGES.md)

[![OS Win](https://img.shields.io/badge/OS-Windows-blueviolet.svg)](README.md)
[![OS Linux](https://img.shields.io/badge/OS-Linux-blueviolet.svg)](README.md)
[![OS macOS](https://img.shields.io/badge/OS-macOS-blueviolet.svg)](README.md)
<!---freshmark /shields -->

`spotless` is a command line interface (CLI) for the [spotless code formatter](../README.md).
It intends to be a simple alternative to its siblings: the plugins for [gradle](../plugin-gradle/README.md), [maven](../plugin-maven/README.md)
and others.

- TODO: add usage and examples
- TBD: can usage be generated automatically e.g. via freshmark?
193 changes: 193 additions & 0 deletions cli/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
plugins {
id 'org.graalvm.buildtools.native'
id 'com.gradleup.shadow'
}
apply from: rootProject.file('gradle/changelog.gradle')
ext.artifactId = project.artifactIdGradle
version = spotlessChangelog.versionNext
apply plugin: 'java-library'
apply plugin: 'application'
apply from: rootProject.file('gradle/java-setup.gradle')
apply from: rootProject.file('gradle/spotless-freshmark.gradle')

dependencies {
// todo, unify with plugin-gradle/build.gradle -- BEGIN
if (version.endsWith('-SNAPSHOT') || (rootProject.spotlessChangelog.versionNext == rootProject.spotlessChangelog.versionLast)) {
api projects.lib
api projects.libExtra
} else {
api "com.diffplug.spotless:spotless-lib:${rootProject.spotlessChangelog.versionLast}"
api "com.diffplug.spotless:spotless-lib-extra:${rootProject.spotlessChangelog.versionLast}"
}
implementation "com.diffplug.durian:durian-core:${VER_DURIAN}"
implementation "com.diffplug.durian:durian-io:${VER_DURIAN}"
implementation "com.diffplug.durian:durian-collect:${VER_DURIAN}"
implementation "org.eclipse.jgit:org.eclipse.jgit:${VER_JGIT}"

testImplementation projects.testlib
testImplementation "org.junit.jupiter:junit-jupiter:${VER_JUNIT}"
testImplementation "org.assertj:assertj-core:${VER_ASSERTJ}"
testImplementation "com.diffplug.durian:durian-testlib:${VER_DURIAN}"
testImplementation 'org.owasp.encoder:encoder:1.3.1'
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
// todo, unify with plugin-gradle/build.gradle -- END

implementation "info.picocli:picocli:${VER_PICOCLI}"
annotationProcessor "info.picocli:picocli-codegen:${VER_PICOCLI}"
}

dependencies {
[
'com.google.googlejavaformat:google-java-format:1.24.0'
].each {
implementation it
}
}

apply from: rootProject.file('gradle/special-tests.gradle')
tasks.withType(Test).configureEach {
testLogging.showStandardStreams = true
}

compileJava {
// options for picocli codegen
// https://github.com/remkop/picocli/tree/main/picocli-codegen#222-other-options
options.compilerArgs += [
"-Aproject=${project.group}/${project.name}",
"-Aother.resource.bundles=application",
// patterns require double-escaping (one escape is removed by groovy, the other one is needed in the resulting json file)
"-Aother.resource.patterns=.*\\\\.properties,.*\\\\.json,.*\\\\.js"
]
}

tasks.withType(org.graalvm.buildtools.gradle.tasks.GenerateResourcesConfigFile).configureEach {
notCompatibleWithConfigurationCache('https://github.com/britter/maven-plugin-development/issues/8')
}
tasks.withType(org.graalvm.buildtools.gradle.tasks.BuildNativeImageTask).configureEach {
notCompatibleWithConfigurationCache('https://github.com/britter/maven-plugin-development/issues/8')
}

tasks.withType(ProcessResources).configureEach(new ApplicationPropertiesProcessResourcesAction(project.version))

class ApplicationPropertiesProcessResourcesAction implements Action<ProcessResources> {

private final String cliVersion

ApplicationPropertiesProcessResourcesAction(String cliVersion) {
this.cliVersion = cliVersion
}

@Override
void execute(ProcessResources processResources) {
def localCliVersion = cliVersion // prevent issues with decorated closure
processResources.filesMatching("application.properties") {
filter(
org.apache.tools.ant.filters.ReplaceTokens,
tokens: [
'cli.version': localCliVersion
]
)
}
}
}

application {
mainClass = 'com.diffplug.spotless.cli.SpotlessCLI'
applicationName = 'spotless'
archivesBaseName = 'spotless-cli'
}


def nativeCompileMetaDir = project.layout.buildDirectory.dir('nativeCompile/src/main/resources/native-image/' + project.group + '/' + project.name)

// use tasks 'nativeCompile' and 'nativeRun' to compile and run the native image
graalvmNative {
agent {
enabled = !project.hasProperty('skipGraalAgent') // we would love to make this dynamic, but it's not possible
defaultMode = "standard"
metadataCopy {
inputTaskNames.add('test')
inputTaskNames.add('testNpm')
mergeWithExisting = false
outputDirectories.add(nativeCompileMetaDir.get().asFile.path)
}
tasksToInstrumentPredicate = new java.util.function.Predicate<Task>() {
@Override
boolean test(Task task) {
// if (project.hasProperty('agent')) {
println ("Instrumenting task: " + task.name + " " + task.name == 'test' + "proj: " + task.project.hasProperty('agent'))
return task.name == 'test' || task.name == 'testNpm'
// }
// return false
}
}
}
binaries {
main {
imageName = 'spotless'
mainClass = 'com.diffplug.spotless.cli.SpotlessCLI'
sharedLibrary = false
useFatJar = true // use shadowJar as input to have same classpath

// optimizations, see https://www.graalvm.org/latest/reference-manual/native-image/optimizations-and-performance/
//buildArgs.add('-O3') // on production builds

// the following options are required for GJF
// see: <https://github.com/google/google-java-format/issues/894#issuecomment-1430408909>
buildArgs.add('-J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED')
buildArgs.add('-J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED')
buildArgs.add('-J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED')
buildArgs.add('-J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED')
buildArgs.add('-J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED')

buildArgs.add('--initialize-at-build-time=com.sun.tools.javac.file.Locations')

buildArgs.add('-H:IncludeResourceBundles=com.sun.tools.javac.resources.compiler')
buildArgs.add('-H:IncludeResourceBundles=com.sun.tools.javac.resources.javac')
}
}
}


tasks.named('metadataCopy') {
dependsOn('test', 'testNpm')
}

tasks.named('nativeCompile') {
dependsOn('shadowJar')
classpathJar = tasks.shadowJar.archiveFile.get().asFile
}


tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) {
dependsOn('metadataCopy') // produces graalvm agent info
from(nativeCompileMetaDir.get().asFile.path) {
into('META-INF/native-image/' + project.group + '/' + project.name)
}
}

gradle.taskGraph.whenReady { TaskExecutionGraph graph ->
// println "Graph: " + graph.allTasks*.name
if (graph.hasTask(':cli:nativeCompile') || graph.hasTask(':cli:metadataCopy') || graph.hasTask(':cli:shadowJar')) {
// enable graalvm agent using property here instead of command line `-Pagent=standard`
// this collects information about reflective access and resources used by the application (e.g. GJF)
project.ext.agent = 'standard'
}
}

tasks.withType(Test).configureEach {
if (it.name == 'test' || it.name == 'testNpm') {
it.outputs.dir(nativeCompileMetaDir)
if (project.hasProperty('agent')) {
it.inputs.property('agent', project.property('agent')) // make sure to re-run tests if agent changes
}
}
if (it.name == 'testCliProcess' || it.name == 'testCliProcessNpm') {
it.dependsOn('shadowJar')
it.systemProperty 'spotless.cli.shadowJar', tasks.shadowJar.archiveFile.get().asFile
}
if (it.name == 'testCliNative' || it.name == 'testCliNativeNpm') {
it.dependsOn('nativeCompile')
it.systemProperty 'spotless.cli.nativeImage', tasks.nativeCompile.outputFile.get().asFile
}
}
26 changes: 26 additions & 0 deletions cli/src/main/java/com/diffplug/spotless/cli/SpotlessAction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2024 DiffPlug
*
* 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.
*/
package com.diffplug.spotless.cli;

import java.util.List;

import javax.annotation.Nonnull;

import com.diffplug.spotless.FormatterStep;

public interface SpotlessAction extends SpotlessCommand {
Integer executeSpotlessAction(@Nonnull List<FormatterStep> formatterSteps);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2024 DiffPlug
*
* 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.
*/
package com.diffplug.spotless.cli;

import com.diffplug.spotless.cli.core.SpotlessActionContext;
import com.diffplug.spotless.cli.core.SpotlessCommandLineStream;

public interface SpotlessActionContextProvider {

SpotlessActionContext spotlessActionContext(SpotlessCommandLineStream commandLineStream);
}
Loading
Loading