Is there a way to lint build files? Say to ban "in...
# community-support
v
Is there a way to lint build files? Say to ban "inline" 3rd party dependency declarations, to force to use the version catalogue?
t
my team at cashapp built a Kotlin DSL linter. It's not as fine-grained as you're asking but there's room to deepen its model https://github.com/cashapp/kotlin-editor/tree/main/recipes/guardrails
v
wont work with groovy?
t
I specifically needed a Kotlin DSL solution, so it's just for that
v
i see thanks, shame
t
there is a way to do what you want, to answer your question. But I don't know of any open source solutions off the top of my head. I've seen a bunch of in-house solutions to linting build scripts. Typically involving regex
v
yea that was my plan b, i only allow certain stuff anyways as you probably do too ..declarative gradle cant come soon enough
t
i only allow certain stuff anyways as you probably do too
👍
..declarative gradle cant come soon enough
probably years before it's "production-ready." I get there with strict linting. My most recent project basically only has
plugins
,
dependencies
, and a custom DSL block in each script, as well as all deps through the version catalog, and some other requirements
v
how do you for the catalogue though? do you parse it to ast?
t
that part came before me and I haven't looked at it 😅 I think it's just regex you can look for things inside a dependencies block and check for well-known configuration names and forbid
"
v
regex ftw 😁
t
surprisingly effective 😐
v
noo
”
is what i enforce, hate
'
😁
t
haha
in Groovy DSL I try to always use
'
because I've been burned by
GStringImpl
in the past 🪦
v
btw do you really think dcl is so far away?
id imagine many are waiting
t
maybe it'll be incubating by Gradle 10. It's barely pre-alpha now it took them over 5 years to get from the beginnings of IP to it being incubating
but I dunno, sounds like they're pouring more resources into it the bigger problem is all the plugins it'll break. That'll be a very long tail
sooo yeah I think it'll be years before it's usable before a tiny subset of the community
v
will they? isnt it just about adding new dsl? or do you mean that it forces isolated projects?
t
I'm very sure it'll break plugins because it enforces particular constraints
beyond IP
v
beyond ip? wasn’t aware
thanks
👍 1
v
wont work with groovy?
If you use Groovy DSL, you should be able to simply use CodeNarc to lint the build scripts. You can also create custom rules for it that check exactly what you want to have or not to have.
v
will check, looks regex based as well
so i gave up on regexes, went groovy ast way..works without a problem
v
You can use regexes, but latest with a custom rule implementation you should be able to check the ast with CodeNarc
y
v
Ah, yeah, maybe also an option if Groovy DSL is used. Also does not support Kotlin DSL. But I wonder why they wrote a complete own linter instead of just providing CodeNarc rules. 🤷‍♂️
Besdies that the plugin is not CC-compatible
y
Yeah, it probably is not CC-compatible. I've never used it, but it has been around for a long time and there people very happy with it.
👌 1
t
I opened one of the earliest issues on that netflix plugin, asking about supporting Android projects for unused-dependency analysis. It's one of the reasons I started the dependency analysis plugin myself 🙃
j
nebula-lint is not CC-compatible, and likely never will be because it uses the project model as well as parsing the build scripts. I had an idea to make a new version of it that removes the project model from scope (use Gradle problems API from plugins instead), and uses UAST (same thing android lint uses) to parse groovy and kts build scripts to make it language agnostic. However, I was laid off last week, so I doubt the remaining team will get to this any time soon
❤️‍🩹 1
e
Android Lint includes checks of Gradle build scripts (both Groovy and Kotlin DSLs) and can run on non-Android projects; I have a custom rule that does something similar. it's probably annoyingly heavy-weight for anything that isn't already an Android project though
v
I ran into this issue with my custom linter v1 was just a task registered in root project, which would collect all
.gradle
files from the build - worked correctly, but was slow as that is single threaded and not cachable etc v2 im trying right now is the more standard design, of a plugin applied to every projects, and the task then runs per project, which is parallel and cacheable however now I need to whitelist where im looking for
.gradle
files
Copy code
val buildFilesInThisProject = target.fileTree(target.projectDir) {
    it.include("*.gradle", "gradle/**/*.gradle")
}
as to avoid root project then looking basically everywhere but now
foo/whatever.gradle
is not linted it cant even be depth based as
app/build.gradle
would be picked up by the root project one any ideas?
v
Don't make it detected, just check the build scripts of all projects. You can even request those IP-safe from the root project or also any other project as it is an immutable information after settings phase is over. As for parallel execution, make the checking a Worker API work item per file, then it can also run in parallel and even in parallel to other tasks, even without
--configuration-cache
or
--parallel
.
v
hmm this is new to me are you saying i should go back to v1 and parallelize the for loop?
v
That might be the better approach in your case, yes, especially if you do only need immutable information from the other projects like their configured build script, don't need to do the task for only a subset of the projects, and want to maximize parallelizability. If with "parallelize the for loop" you mean to move the logic to a worker api action, that is.
v
The base idea is just this
Copy code
target.tasks.register("lintBuildFiles", LintBuildFilesTask::class.java) {
    it.rootDirectory.set(target.layout.projectDirectory)
    it.buildFiles.from(target.fileTree(target.rootDir) { tree -> tree.include("**/*.gradle") })
}
Copy code
@DisableCachingByDefault(because = "Reports violations to the console and has no output files.")
abstract class LintBuildFilesTask : DefaultTask() {
    @get:InputFiles
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val buildFiles: ConfigurableFileCollection

    @get:Internal
    abstract val rootDirectory: DirectoryProperty

    @TaskAction
    fun lintBuildFiles() {
        val root = rootDirectory.get().asFile

        val sortedBuildFiles = buildFiles.files.sortedBy { it.relativeTo(root).invariantSeparatorsPath }
        for (buildFile in sortedBuildFiles) {
            ...
        }
    }
}
the trouble is, I have specific additional rules for jvm library, android library etc, so I need to know what type it is, so v1
Copy code
class LintBuildFilesPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        require(target == target.rootProject) { "Apply sk.ursus.gradle-lint to the root project." }

        val lintTask = target.tasks.register("lintBuildFiles", LintBuildFilesTask::class.java) {
            it.rootDirectory.set(target.layout.projectDirectory)
            it.buildFiles.from(target.fileTree(target.rootDir) { tree -> tree.include("**/*.gradle") })
        }

        target.allprojects { module ->
            if (module == target) return@allprojects

            val buildFile = module.buildFile
            val path = buildFile.relativeTo(target.rootDir).invariantSeparatorsPath

            module.pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
                if (!module.pluginManager.hasPlugin("com.android.application")) {
                    lintTask.registerPolicy(policy = ModulePolicy.LIBRARY, buildFile = buildFile, path = path)
                }
            }
            module.pluginManager.withPlugin("com.android.librar") {
                if (!module.pluginManager.hasPlugin("com.android.application")) {
                    lintTask.registerPolicy(policy = ModulePolicy.LIBRARY, buildFile = buildFile, path = path)
                }
            }
            module.pluginManager.withPlugin("com.android.application") {
                lintTask.registerPolicy(policy = ModulePolicy.APPLICATION, buildFile = buildFile, path = path)
            }
        }
    }
}
does this, it needs
allprojects
so the question is, if I have just one task which aggregates all the build files, will I be able to discriminate their project type if it is an project?
v
Ah, no, if you need that, you probably need a plugin per project indeed, so that the
pluginManager.withPlugin
can configure the project-own analysis task depending on which plugins get applied. Besides that the approach you just posted does double-cross-project-configuration, configuring the other projects to later configure the root project. Also your approach is plugin application order-sensitive which is bad. If a project first applies K/JVM and later Android Application, then you do two
registerPolicy
calls, if they are applied the other way around, you only do one call. And the Android library ID has a typo. Also, requiring root project application might be problematic unless you control all usages iirc, because if you apply that plugin in a precompiled Kotlin DSL script plugin, the accessor-generation might make problems. Also, the
@DisableCachingByDefault
is not really appropriate. Instead you would do
init { _outputs_.upToDateWhen *{* true *}* }
, then the task is cacheable. Because if it fails, it is anyway rerun next time, but as you say "the outputs are up to date" explicitly, the task can be up-to-date and also cached if the inputs are the same. An alternative if the violation calculation is resource-intense is, to have one task that does not fail the build but writes the violations to a file and another task that consumes that file output and fails the build with the console output from the file. Then the analysis task can be up-to-date or coming from cache, and the failing is more or less instantly if the inputs are the same and thus the violations are the same.
v
you're so hardcore 😄 impressive but yes I figured its bad, it was just a POC for the analysis itself but do you, if I were to ho plugin-per-project route, how to make sure I ingest all the build files?
v
Not sure what you mean. The plugin is applied to a project, so the task looks at exactly the configured
target.buildFile
and that's it.
If you are concerned about legacy script plugins (the things you use with "apply from"), then simply forbid them in your linting. They are a loaded gun pointed at your foot anyway and are highly discouraged due to their quirks and problems.
v
the issue is if i apply the linter to all projects, including root, then the collection of .gradle files will recurse over everything (because its root) unless i specify to only to look for .gradle files max deprh 1
🤔 1
which then might miss stuff
v
Again, I would not use a file-tree like you do anyway
It is explicitly configured which are the build scripts
So just ask Gradle using
target.buildFile
Even with the aggregating case you better should have done that imho
v
im a noob, so forgive my ignorance, but that wont get access to
arbitrary.gradle
right?
v
Why would you care it if it is not used as build script? Your use-case is checking the build scripts of your build, isn't it?
arbitrary.gradle
is just a file that ends in
.gradle
you can also store an image in it if you like.
j
what if the build script "includes" another script? you probably want to follow that
v
well I currently
arbitrary.gradle
use that to share utilities and stuff
j
(that should be a plugin though)
v
Again, if that is something you use with "apply from", it is a legacy script plugin.
v
i know but I don't wanna pay config time price of conv plugin via included build in order to enable IP to lower config time, and gain net nothing
v
Those have many quirks, are a loaded gun pointing at your foot, and are highly discouraged.
👆 1
So your linting should prevent their usage anyway, and instead you should put that logic into a proper convention plugin instead.
v
if I includeBuild convention it eats like 6 seconds of config time..
v
Use configuration cache, then you seldomly pay that price?
Don't get me wrong, if you want to use the legacy script plugins, do so. But again, they are a loaded gun pointing at your foot and sooner or later you will have a hole in your foot.
v
I mean then why am I battling with isolated projects in first place.. okay lets not get into this
v
If that is what you want to do, well, then you probably need to follow those references
But then your linting becomes way more expensive in the configuration time
Your script plugin could like anywhere, even outside the project
v
> Not sure what you mean. The plugin is applied to a project, so the task looks at exactly the configured target.buildFile and that's it. so to finish this thought, it will only give me project file, right? not every .gradle file in the project
v
And to properly configure the task inputs you would then need at configuration time parse the build script to find the references to define them as task inputs
Or you have one task per project where you apply the plugin and there only check the actual build script, and additionally a root-project task that checks any
.gradle
file that is not one of the actual build scripts or something like that.
v
getting hairy..maybe just ban arbitrary .gradle placement and force the legacy scripts into
gradle/
via CI means?
v
Whle then still not hitting script plugins outside the root project directory of course
maybe just ban arbitrary .gradle placement and force the legacy scripts into
gradle/
via CI means?
Whatever works for you 🤷‍♂️
Your linting could also restrict the legacy script plugin usage to only use ones from the same directory or lower, not descending into other projects directories though, and then use the file-tree matching approach per project again.
v
yep thats what I meant
v
Then each project gets all the
.gradle
files of only that project and not subprojects and you maybe get the setup you envision?
v
yea exactly, just thinking as how to measure the depth, simply by counting the
../
?
v
Don't measure the depth. Include all
*.gradle
files that are not part of the project directory of another project. And in the linting forbid to contain any
..
or absolute path or file object, but restrict it to applying from by String literal that must contain a relative path that does not contain any
..
, or something like that.
v
sounds good thank you a lot
👌 1