diff --git a/src/functionalTest/groovy/com/autonomousapps/jvm/NoAbsolutePathsInOutputsSpec.groovy b/src/functionalTest/groovy/com/autonomousapps/jvm/NoAbsolutePathsInOutputsSpec.groovy new file mode 100644 index 000000000..686ed1205 --- /dev/null +++ b/src/functionalTest/groovy/com/autonomousapps/jvm/NoAbsolutePathsInOutputsSpec.groovy @@ -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 + } +} diff --git a/src/functionalTest/groovy/com/autonomousapps/jvm/projects/NoAbsolutePathsProject.groovy b/src/functionalTest/groovy/com/autonomousapps/jvm/projects/NoAbsolutePathsProject.groovy new file mode 100644 index 000000000..bd00f54ce --- /dev/null +++ b/src/functionalTest/groovy/com/autonomousapps/jvm/projects/NoAbsolutePathsProject.groovy @@ -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() + ) +} diff --git a/src/main/kotlin/com/autonomousapps/model/internal/Dependency.kt b/src/main/kotlin/com/autonomousapps/model/internal/Dependency.kt index 32c73235e..6c6c9e352 100644 --- a/src/main/kotlin/com/autonomousapps/model/internal/Dependency.kt +++ b/src/main/kotlin/com/autonomousapps/model/internal/Dependency.kt @@ -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, - // 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 ) : Comparable { override fun compareTo(other: Dependency): Int = coordinates.compareTo(other.coordinates) @@ -32,29 +28,25 @@ internal data class ProjectDependency( override val coordinates: ProjectCoordinates, /** Map of [Capability] canonicalName to the capability. */ override val capabilities: Map, - override val files: Set -) : Dependency(coordinates, capabilities, files) +) : Dependency(coordinates, capabilities) @TypeLabel("module") @JsonClass(generateAdapter = false) internal data class ModuleDependency( override val coordinates: ModuleCoordinates, override val capabilities: Map, - override val files: Set -) : Dependency(coordinates, capabilities, files) +) : Dependency(coordinates, capabilities) @TypeLabel("flat") @JsonClass(generateAdapter = false) internal data class FlatDependency( override val coordinates: FlatCoordinates, override val capabilities: Map, - override val files: Set -) : 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, - override val files: Set -) : Dependency(coordinates, capabilities, files) +) : Dependency(coordinates, capabilities) diff --git a/src/main/kotlin/com/autonomousapps/model/internal/intermediates/producer/ExplodedJar.kt b/src/main/kotlin/com/autonomousapps/model/internal/intermediates/producer/ExplodedJar.kt index 38c583b43..e2e245f5b 100644 --- a/src/main/kotlin/com/autonomousapps/model/internal/intermediates/producer/ExplodedJar.kt +++ b/src/main/kotlin/com/autonomousapps/model/internal/intermediates/producer/ExplodedJar.kt @@ -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 @@ -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. @@ -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, @@ -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 { diff --git a/src/main/kotlin/com/autonomousapps/tasks/ArtifactsReportTask.kt b/src/main/kotlin/com/autonomousapps/tasks/ArtifactsReportTask.kt index 48f6605c2..4158acdd7 100644 --- a/src/main/kotlin/com/autonomousapps/tasks/ArtifactsReportTask.kt +++ b/src/main/kotlin/com/autonomousapps/tasks/ArtifactsReportTask.kt @@ -38,13 +38,11 @@ public abstract class ArtifactsReportTask : DefaultTask() { public abstract val artifacts: Property /** - * 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 diff --git a/src/main/kotlin/com/autonomousapps/tasks/SynthesizeDependenciesTask.kt b/src/main/kotlin/com/autonomousapps/tasks/SynthesizeDependenciesTask.kt index cb6021d19..4f7abe32a 100644 --- a/src/main/kotlin/com/autonomousapps/tasks/SynthesizeDependenciesTask.kt +++ b/src/main/kotlin/com/autonomousapps/tasks/SynthesizeDependenciesTask.kt @@ -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 @@ -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 ) } @@ -254,10 +253,8 @@ public abstract class SynthesizeDependenciesTask @Inject constructor( private class DependencyBuilder(val coordinates: Coordinates) { val capabilities: MutableList = mutableListOf() - val files: MutableSet = 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) { @@ -274,10 +271,10 @@ public abstract class SynthesizeDependenciesTask @Inject constructor( fun build(): Dependency { val capabilities: Map = 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) } } }