Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) 2025. Tony Robalik.
// SPDX-License-Identifier: Apache-2.0
package com.autonomousapps.jvm

import com.autonomousapps.jvm.projects.NoAbsolutePathsProject
import groovy.json.JsonSlurper

import java.util.zip.GZIPInputStream

import static com.autonomousapps.utils.Runner.build

final class NoAbsolutePathsInOutputsSpec extends AbstractJvmSpec {

def "intermediate outputs do not contain absolute file paths (#gradleVersion)"() {
given:
def project = new NoAbsolutePathsProject()
gradleProject = project.gradleProject

when:
build(gradleVersion, gradleProject.rootDir, 'buildHealth')

then: 'exploded-jars.json.gz does not contain jarFile key'
def explodedJarsFile = gradleProject
.singleArtifact('proj', 'reports/dependency-analysis/main/intermediates/exploded-jars.json.gz')
def explodedJarsJson = decompress(explodedJarsFile.asFile)
!explodedJarsJson.contains('"jarFile"')

and: 'dependency files do not contain files key with absolute paths'
def dependenciesDir = gradleProject.buildDir('proj').resolve('reports/dependency-analysis/main/dependencies')
dependenciesDir.toFile().exists()
def dependencyFiles = dependenciesDir.toFile().listFiles({ File f -> f.name.endsWith('.json') } as FileFilter)
dependencyFiles != null
dependencyFiles.length > 0
dependencyFiles.every { File f ->
def parsed = new JsonSlurper().parseText(f.text)
!hasFilesWithAbsolutePaths(parsed)
}

and: 'artifacts.json exists and is valid JSON'
def artifactsFile = gradleProject
.singleArtifact('proj', 'reports/dependency-analysis/main/intermediates/artifacts.json')
def artifacts = new JsonSlurper().parseText(artifactsFile.asFile.text)
artifacts != null

where:
gradleVersion << gradleVersions()
}

private static String decompress(File gzFile) {
new GZIPInputStream(new FileInputStream(gzFile)).withStream { gis ->
return gis.text
}
}

private static boolean hasFilesWithAbsolutePaths(Object parsed) {
if (parsed instanceof Map) {
def map = (Map) parsed
if (map.containsKey('files')) {
def files = map['files']
if (files instanceof Collection) {
return files.any { it instanceof String && (it.startsWith('/') || it.contains(':\\')) }
}
}
return map.values().any { hasFilesWithAbsolutePaths(it) }
} else if (parsed instanceof Collection) {
return ((Collection) parsed).any { hasFilesWithAbsolutePaths(it) }
}
return false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) 2025. Tony Robalik.
// SPDX-License-Identifier: Apache-2.0
package com.autonomousapps.jvm.projects

import com.autonomousapps.AbstractProject
import com.autonomousapps.kit.GradleProject
import com.autonomousapps.kit.Source
import com.autonomousapps.kit.SourceType
import com.autonomousapps.kit.gradle.Dependency

final class NoAbsolutePathsProject extends AbstractProject {

final GradleProject gradleProject

NoAbsolutePathsProject() {
this.gradleProject = build()
}

private GradleProject build() {
return newGradleProjectBuilder()
.withSubproject('proj') { s ->
s.sources = [KOTLIN_SOURCE]
s.withBuildScript { bs ->
bs.plugins = kotlin
bs.dependencies = [
new Dependency('implementation', 'org.apache.commons:commons-lang3:3.14.0'),
]
}
}
.write()
}

private static final Source KOTLIN_SOURCE = new Source(
SourceType.KOTLIN, 'Main', 'com/example',
"""\
package com.example

import org.apache.commons.lang3.StringUtils

class Main {
fun greet(name: String): String = StringUtils.capitalize(name)
}""".stripIndent()
)
}
16 changes: 4 additions & 12 deletions src/main/kotlin/com/autonomousapps/model/internal/Dependency.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,11 @@ import com.autonomousapps.model.ModuleCoordinates
import com.autonomousapps.model.ProjectCoordinates
import com.squareup.moshi.JsonClass
import dev.zacsweers.moshix.sealed.annotations.TypeLabel
import java.io.File

@JsonClass(generateAdapter = false, generator = "sealed:type")
internal sealed class Dependency(
open val coordinates: Coordinates,
open val capabilities: Map<String, Capability>,
// Can be empty because we don't get file for annotation processor dependencies.
// This property is also unused and was only added speculatively, so maybe it doesn't matter
open val files: Set<File>
) : Comparable<Dependency> {
override fun compareTo(other: Dependency): Int = coordinates.compareTo(other.coordinates)

Expand All @@ -32,29 +28,25 @@ internal data class ProjectDependency(
override val coordinates: ProjectCoordinates,
/** Map of [Capability] canonicalName to the capability. */
override val capabilities: Map<String, Capability>,
override val files: Set<File>
) : Dependency(coordinates, capabilities, files)
) : Dependency(coordinates, capabilities)

@TypeLabel("module")
@JsonClass(generateAdapter = false)
internal data class ModuleDependency(
override val coordinates: ModuleCoordinates,
override val capabilities: Map<String, Capability>,
override val files: Set<File>
) : Dependency(coordinates, capabilities, files)
) : Dependency(coordinates, capabilities)

@TypeLabel("flat")
@JsonClass(generateAdapter = false)
internal data class FlatDependency(
override val coordinates: FlatCoordinates,
override val capabilities: Map<String, Capability>,
override val files: Set<File>
) : Dependency(coordinates, capabilities, files)
) : Dependency(coordinates, capabilities)

@TypeLabel("included_build")
@JsonClass(generateAdapter = false)
internal data class IncludedBuildDependency(
override val coordinates: IncludedBuildCoordinates,
override val capabilities: Map<String, Capability>,
override val files: Set<File>
) : Dependency(coordinates, capabilities, files)
) : Dependency(coordinates, capabilities)
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
// SPDX-License-Identifier: Apache-2.0
package com.autonomousapps.model.internal.intermediates.producer

import com.autonomousapps.internal.utils.LexicographicIterableComparator
import com.autonomousapps.internal.utils.ifNotEmpty
import com.autonomousapps.model.Coordinates
import com.autonomousapps.model.internal.*
import com.autonomousapps.model.internal.intermediates.ExplodingJar
import com.squareup.moshi.JsonClass
import java.io.File

/**
* A library or project, along with the set of classes declared by, and other information contained within, this
Expand All @@ -16,7 +16,6 @@ import java.io.File
@JsonClass(generateAdapter = false)
internal data class ExplodedJar(
override val coordinates: Coordinates,
val jarFile: File,

/**
* True if this dependency contains only annotations. False otherwise.
Expand Down Expand Up @@ -58,7 +57,6 @@ internal data class ExplodedJar(
exploding: ExplodingJar,
) : this(
coordinates = artifact.coordinates,
jarFile = artifact.file,
isAnnotations = exploding.isCompileOnlyCandidate,
securityProviders = exploding.securityProviders,
androidLintRegistry = exploding.androidLintRegistry,
Expand All @@ -70,9 +68,9 @@ internal data class ExplodedJar(
)

override fun compareTo(other: ExplodedJar): Int {
return coordinates.compareTo(other.coordinates).let {
if (it == 0) jarFile.compareTo(other.jarFile) else it
}
return compareBy(ExplodedJar::coordinates)
.thenBy(LexicographicIterableComparator()) { it.binaryClasses }
.compare(this, other)
}

init {
Expand Down
10 changes: 4 additions & 6 deletions src/main/kotlin/com/autonomousapps/tasks/ArtifactsReportTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,11 @@ public abstract class ArtifactsReportTask : DefaultTask() {
public abstract val artifacts: Property<ArtifactCollection>

/**
* This is the "official" input for wiring task dependencies correctly, but is otherwise
* unused. This needs to use [InputFiles] and [PathSensitivity.ABSOLUTE] because the path to the
* jars really does matter here. Using [Classpath] is an error, as it looks only at content and
* not name or path, and we really do need to know the actual path to the artifact, even if its
* contents haven't changed.
* This is the "official" input for wiring task dependencies correctly, but is otherwise unused.
* [PathSensitivity.NONE] means only file content matters for caching, not the filesystem path.
* This enables cache reuse across machines with different Gradle home locations.
*/
@PathSensitive(PathSensitivity.ABSOLUTE)
@PathSensitive(PathSensitivity.NONE)
@InputFiles // TODO(tsr): can I avoid using `get()`?
public fun getClasspathArtifactFiles(): FileCollection = artifacts.get().artifactFiles

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import org.gradle.api.tasks.*
import org.gradle.workers.WorkAction
import org.gradle.workers.WorkParameters
import org.gradle.workers.WorkerExecutor
import java.io.File
import javax.inject.Inject

@CacheableTask
Expand Down Expand Up @@ -159,7 +158,7 @@ public abstract class SynthesizeDependenciesTask @Inject constructor(
physicalArtifacts.forEach { artifact ->
builders.merge(
artifact.coordinates,
DependencyBuilder(artifact.coordinates).apply { files.add(artifact.file) },
DependencyBuilder(artifact.coordinates),
DependencyBuilder::concat
)
}
Expand Down Expand Up @@ -254,10 +253,8 @@ public abstract class SynthesizeDependenciesTask @Inject constructor(
private class DependencyBuilder(val coordinates: Coordinates) {

val capabilities: MutableList<Capability> = mutableListOf()
val files: MutableSet<File> = sortedSetOf()

fun concat(other: DependencyBuilder): DependencyBuilder {
files.addAll(other.files)
other.capabilities.forEach { otherCapability ->
val existing = capabilities.find { it.javaClass.canonicalName == otherCapability.javaClass.canonicalName }
if (existing != null) {
Expand All @@ -274,10 +271,10 @@ public abstract class SynthesizeDependenciesTask @Inject constructor(
fun build(): Dependency {
val capabilities: Map<String, Capability> = capabilities.associateBy { it.javaClass.canonicalName }.toSortedMap()
return when (coordinates) {
is ProjectCoordinates -> ProjectDependency(coordinates, capabilities, files)
is ModuleCoordinates -> ModuleDependency(coordinates, capabilities, files)
is FlatCoordinates -> FlatDependency(coordinates, capabilities, files)
is IncludedBuildCoordinates -> IncludedBuildDependency(coordinates, capabilities, files)
is ProjectCoordinates -> ProjectDependency(coordinates, capabilities)
is ModuleCoordinates -> ModuleDependency(coordinates, capabilities)
is FlatCoordinates -> FlatDependency(coordinates, capabilities)
is IncludedBuildCoordinates -> IncludedBuildDependency(coordinates, capabilities)
}
}
}
Expand Down
Loading