Skip to content
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

Better method for managing clang-format binaries #673

Open
nedtwigg opened this issue Aug 25, 2020 · 14 comments
Open

Better method for managing clang-format binaries #673

nedtwigg opened this issue Aug 25, 2020 · 14 comments

Comments

@nedtwigg
Copy link
Member

We are able to call clang-format by shelling out to it on the system path, and we are able to cache its results by enforcing a version check on the binary. However, if it isn't on the path, or the version is wrong, we just show a helpful error message and let the user figure it out from there:

String howToInstall = "" +
"You can download clang-format from https://releases.llvm.org and " +
"then point Spotless to it with `pathToExe('/path/to/clang-format')` " +
"or you can use your platform's package manager:" +
"\n win: choco install llvm --version {version} (try dropping version if it fails)" +
"\n mac: brew install clang-format (TODO: how to specify version?)" +
"\n linux: apt install clang-format (try clang-format-{version} with dropped minor versions)";
String exeAbsPath = ForeignExe.nameAndVersion("clang-format", version)
.pathToExe(pathToExe)
.fixCantFind(howToInstall)
.fixWrongVersion(
"You can tell Spotless to use the version you already have with `clangFormat('{versionFound}')`" +
"or you can download the currently specified version, {version}.\n" + howToInstall)
.confirmVersionAndGetAbsolutePath();

It would be nice if we could handle this better, or at least have better instructions.

@nedtwigg
Copy link
Member Author

nedtwigg commented Aug 25, 2020

@akornilov feel free to ignore, but I see that you maintain the only llvm plugins in the gradle plugin portal. Any tips?

@akornilov
Copy link

akornilov commented Aug 26, 2020 via email

@nedtwigg
Copy link
Member Author

nedtwigg commented Aug 26, 2020

When a user declares com.google.guava:guava:20, they don't have to go download it and put it on their path, gradle handles that for them.

We'd like to do something similar for clangFormat('10.0.0') or clangFormat('10.0.1'). We compiled instructions (top) for using the package managers on each platform, but it's not seamless, and it's hard to control which version you get.

Do you have any suggestions for how to grab and cache llvm binaries, or better yet just the clang-format part of the binaries?

Thanks, and "not interested" is a fine answer, don't mean to bother 😁

@akornilov
Copy link

akornilov commented Aug 28, 2020 via email

@nedtwigg
Copy link
Member Author

Thanks, this is very helpful!!

Are you from the Gradle developers team? ...
What kind of software do you develop at the moment

I'm not affiliated with Gradle. I work on tooling for embedded systems, and Spotless is a side project that does autoformatting. I don't use llvm to compile, but I do want to use clang-format to enforce formatting rules on some C code.

my already created plugin for Gradle which allows you to use LLVM libraries

Perfect, this is exactly what I was looking for, thanks very much! I found your plugin at https://plugins.gradle.org/, but I couldn't figure out if it did installation for you, or if the user still needed to do that themselves. I think where I got stuck is that I didn't think to click the "wiki" section, where you have very good documentation. What I like about GitHub is that the README shows up really big, so it's easy to tell new users the details of the project. The SourceForge landing page doesn't leave much space for documentation, so it can be hard to figure out where to get started. But now I know where to start!

@ronhe
Copy link

ronhe commented Jun 17, 2022

Hi,

I recently started using Spotless (awesome project!), and found it very frustrating to use with clang-format. That's mainly due to the combination of Spotless' strict version check, and clang-format's os/distro specific versioning (i.e. 10.0.0-4ubuntu1).

If I understand correctly, the purpose of the strict version check is to allow safe caching. Then, why not instead of enforcing a specific version, just invalidating/ignoring the cache in case of a version mismatch? IMO this would make clang-format much easier to use.

Moreover, if the purpose of the strict version check is also to control the allowed version of clang-format, we could assert only the x.y.z part of the version.

@nedtwigg
Copy link
Member Author

the purpose of the strict version check is to allow safe caching

That's one aspect, but the other part is to make sure that the team and CI are all on the same page. You've got clang 1, I've got clang 2, and we keep formatting-over each other's code. That's bad. It's better to force us to install the same clang.

But it's definitely a problem if we are including a platform-specific identifier, that ought to get stripped off the check. The way to do that is to add .versionRegex("version (\\S*)(?:\\-)") to this hunk of code:

String exeAbsPath = ForeignExe.nameAndVersion("clang-format", version)
.pathToExe(pathToExe)
.fixCantFind(howToInstall)
.fixWrongVersion(

I might have the regex there wrong, the default is this:

private Pattern versionRegex = Pattern.compile("version (\\S*)");

We need to add a non-capturing group to grab the hyphen in the platform-specific part and keep it out of the version group.

@bh-tt
Copy link
Contributor

bh-tt commented Jul 8, 2022

Would it be possible to only fail the build if a clang-format call actually happens? The gradle plugin crashes the build during the configuration phase if it is not available or the wrong version, no matter which tasks are run.

Context: I have some CI scanning jobs on my projects and I cannot easily change the images used. I would love to use the spotless plugin but if I have to choose between vulnerability scanning and a nice format I will take vulnerability scanning.

@ronhe
Copy link

ronhe commented Jul 8, 2022

We need to add a non-capturing group to grab the hyphen in the platform-specific part and keep it out of the version group.

I'm afraid that this is not good enough, because the minor/patch parts of the version might also differ across distros.
For example, for clang-format-9, on Ubuntu 20.04 I get 9.0.1-12 while on Ubuntu 18.04 I get 9.0.0-2~ubuntu18.04.2.

I think that Spotless should either assert only the major version part, or it should allow the user to customize the version check by supplying a custom regex pattern.

@nedtwigg
Copy link
Member Author

nedtwigg commented Jul 8, 2022

Would it be possible to only fail the build if a clang-format call actually happens?

Yes, this is possible, but a bit tricky. You can see here that State takes a String exeAbsPath.

String exeAbsPath = ForeignExe.nameAndVersion("clang-format", version)
.pathToExe(pathToExe)
.fixCantFind(howToInstall)
.fixWrongVersion(
"You can tell Spotless to use the version you already have with {@code clangFormat('{versionFound}')}" +
"or you can download the currently specified version, {version}.\n" + howToInstall)
.confirmVersionAndGetAbsolutePath();
return new State(this, exeAbsPath);

This could instead be a Supplier<String>. This would also mean that the List<String> args field in State would have to be lazily constructed on first usage.

// used for executing
final transient List<String> args;
State(ClangFormatStep step, String exeAbsPath) {
this.version = step.version;
this.style = step.style;
args = new ArrayList<>(2);
args.add(exeAbsPath);
if (style != null) {
args.add("--style=" + style);
}

I think your feature request for this is a good one, and it's probably worth somehow generalizing this lazy construction of args into the ForeignExe class. The other usage of it, BlackStep, would benefit from the same thing. I'd be happy to merge a PR for this (doesn't matter if the PR is just for Clang, includes ForeignExe, or if it includes Black too, if it solves your problem then I'd merge it).

grab the hyphen ... and keep it out of the version group ... is not good enough, because the minor/patch parts of the version might also differ across distros

I'm happy to merge a PR which supports this too, the tricky part is docs. When you first encountered this issue of different clang-format-9's, and you looked at our docs, what do you wish it had said? Figure that out, and then building to it would be easy.

@nedtwigg
Copy link
Member Author

Thanks to @bh-tt, starting with plugin-gradle 6.9.0 and plugin-maven 2.24.0, the check for foreign-exes is now lazy.

Still happy to merge a PR to allow new version regex. One thought which just occurred to me - you could make your build configuration platform-specific, e.g.:

clang().version(if (ubuntu20) "9.0.1-12" else if (ubuntu18) "9.0.0-2~ubuntu18.04.2")

Not great, but a possible workaround until something better comes along...

@pjonsson
Copy link

pjonsson commented Jun 3, 2023

I find the comment "version is optional" right after clangFormat('10.0.1') in plugin-gradle/README.md misleading, removing the version makes it default to a clang-format that is too old for Ubuntu 22.04 LTS (clang-format-11 to 15 are available).

For those who want to detect the clang-format version, here's a snippet for build.gradle:

def clangFormatBinary = (String) project.findProperty('clangFormat') ?: 'clang-format'

spotless {
  java {
    def clangOutput = new ByteArrayOutputStream()
    exec {
      commandLine clangFormatBinary, '--version'
      standardOutput = clangOutput
    }
    def clangVersion = (String) (clangOutput.toString() =~ /\d+\.\d+\.\d+/)[0]
    clangFormat(clangVersion).pathToExe(clangFormatBinary).style('file')
  }
}

Run with ./gradlew spotlessCheck -PclangFormat=clang-format-15 if the clang-format-binary is called something other than clang-format.

@pjonsson
Copy link

@nedtwigg what is the recommended way of handling clang-format for both macOS and Linux? The clang-format binary has a major version number appended at the end on Ubuntu (clang-format-15), but it can also just be called clang-format. Different LTS releases of Ubuntu has different versions of clang-format available as packages, so there isn't an obvious smallest common denominator that can be used.

The snippet in my previous comment does what I want ("I tell Gradle the clang-format binary to use, Gradle figures out what version it is and how to use it without bothering me"), except that the snippet causes failures for people without clang-format installed on their computer. I understand people cannot format the code without clang-format, but ./gradlew tasks and ./gradlew run also fails for them, and I do have an interest in letting people at least run (=start) HEAD without having to install a complete development environment.

@nedtwigg
Copy link
Member Author

You could set a myproj_enable_spotless=true in your ~/.gradle/gradle.properties.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants