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

Generate files that are neither source code nor resources #1677

Open
swankjesse opened this issue Jan 8, 2024 · 5 comments
Open

Generate files that are neither source code nor resources #1677

swankjesse opened this issue Jan 8, 2024 · 5 comments

Comments

@swankjesse
Copy link

I wrote an annotation processor that collects all production classes with the @Serializable annotation, and checks for a corresponding test with the @SerializationTest annotation. It fails the build if a class is missing a test! It looks like this:

In src/commonMain/kotlin:

@Serializable
class Color (...)

In src/commonTest/kotlin:

@SerializationTest(Color::class)
class ColorTest {
  ...
}

This works nicely, but it wasn’t as easy to write as I’d hoped.

To be incremental, my processor must generate a file (which is used to track which .kt files were inputs to that file). I decided that the annotation processor should generate two files: classes-annotated-serializable.txt and classes-with-serialization-tests.txt, plus a helper Gradle task to diff the files and log an error if they differ.

That worked nicely, but I soon discovered that classes-annotated-serializable.txt was appearing in my production .jar file, which wasn’t what I wanted. I was surprised to learn that CodeGenerator.createNewFile() makes its outputs resources by default. I ended up manually stripping them out with Gradle:

    tasks.withType<AbstractArchiveTask> {
      exclude("**/classes-annotated-serializable.txt")
      exclude("**/classes-with-serialization-tests.txt")
    }

This worked for my use case.

But I don’t like that I have to generate a resource and then strip it out; I’d prefer a mechanism to generate this file in a directory that isn‘t for resources.

@swankjesse
Copy link
Author

I have a straw man solution. I propose a new overload of CodeGenerator.createNewFile():

    /**
     * The relative path should include 0 or more directories and a file
     * name complete with extension appropriate for the generated file
     * type. Some examples:
     * 
     *  * "com/example/project/MyGeneratedFile.kt"
     *  * "com/example/project/MyGeneratedFile.java"
     *  * "my-resource-directory/my-resource.txt"
     *  * "my-build-directory/my-build-output.txt"
     */
    fun createNewFile(
        dependencies: Dependencies,
        relativePath: String,
        type: GeneratedFileType,
    ): OutputStream

    enum class GeneratedFileType {
      KotlinSource,
      JavaSouce,
      Resource,
      BuildOutput,
    }

This mechanism would be more explicit than the current API that checks extension names. It would also satisfy my use case of generating files that are neither source code nor resources.

@hfhbd
Copy link

hfhbd commented Mar 27, 2024

@swankjesse Did you also find a Multiplatform solution to exclude the generated resources?

@ephemient
Copy link

I have a necessary workaround in my codebase

val resourcesDir = codeGenerator::class.java
    .getDeclaredMethod("extensionToDirectory", String::class.java)
    .apply { isAccessible = true }
    .invoke(codeGenerator, "json") as File
codeGenerator::class.java
    .getDeclaredMethod("createNewFile", Dependencies::class.java, String::class.java, File::class.java)
    .apply { isAccessible = true }
    .invoke(codeGenerator, dependencies, "data.json", resourcesDir.parentFile) as OutputStream

but would greatly prefer real support.

@St4B
Copy link

St4B commented Nov 17, 2024

Autoselecting paths without any flexibility to change them is problematic. I have a processor that generates Groovy files. With the current CodeGenerator implementation, they are added under the resources directory, which is incorrect. To avoid this, I have created a task to copy these files.

// Define a custom task to copy files
val copyGeneratedGroovyFiles by tasks.registering(Copy::class) {
    // Set the source and destination directories
    val generatedGroovyDir = "$buildDir/generated/ksp/main/groovy"
    val sourceDir = "$buildDir/generated/ksp/main/resources"

    // Specify the source and destination for copying
    from(sourceDir)
    into(generatedGroovyDir)
}

// Ensure the 'copyGeneratedGroovyFiles' task runs after the KSP task
tasks.withType<KspTask> {
    finalizedBy(copyGeneratedGroovyFiles)
}

So, I ended up with an extra step that isn't needed, which adds time overhead. Additionally, I am forced to duplicate these files to avoid affecting KSP caching

@martinbonnin
Copy link
Contributor

Autoselecting paths without any flexibility to change them is problematic.

@St4B I'd say not being able to change the output directories is fine as long as KSP gives us a public API to access them as Properties carrying their task dependency?

Or did you explicitely need to set the output directories of KSP?

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

No branches or pull requests

5 participants